From d68133f83c930ead57d520d9523314261b8a57a7 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 5 Dec 2025 17:27:05 -0800 Subject: [PATCH 01/32] feat: add separate audio file --- .../NitroScreenRecorder.kt | 29 +++- .../nitroscreenrecorder/RecorderUtils.kt | 141 ++++++++++++++++- .../ScreenRecordingService.kt | 42 +++++- .../BroadcastWriter.swift | 130 +++++++++++++++- .../SampleHandler.swift | 53 +++++-- ios/NitroScreenRecorder.swift | 142 +++++++++++++++++- src/NitroScreenRecorder.nitro.ts | 2 + src/functions.ts | 3 + src/types.ts | 58 ++++++- 9 files changed, 573 insertions(+), 27 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt index 3b7d4de..2ea13ae 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt @@ -33,6 +33,7 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { private var globalRecordingService: ScreenRecordingService? = null private var isServiceBound = false private var lastGlobalRecording: File? = null + private var lastGlobalAudioRecording: File? = null private var globalRecordingErrorCallback: ((RecordingError) -> Unit)? = null private val screenRecordingListeners = @@ -64,12 +65,14 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { fun notifyGlobalRecordingFinished( file: File, + audioFile: File?, event: ScreenRecordingEvent, enabledMic: Boolean ) { - Log.d(TAG, "๐Ÿ notifyGlobalRecordingFinished called with file: ${file.absolutePath}") + Log.d(TAG, "๐Ÿ notifyGlobalRecordingFinished called with file: ${file.absolutePath}, audioFile: ${audioFile?.absolutePath}") instance?.let { recorder -> recorder.lastGlobalRecording = file + recorder.lastGlobalAudioRecording = audioFile recorder.notifyListeners(event) } } @@ -275,6 +278,7 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { enableCamera: Boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, + separateAudioFile: Boolean, onRecordingFinished: (ScreenRecordingFile) -> Unit ) { // no-op @@ -297,7 +301,7 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { // --- Global Recording Methods --- - override fun startGlobalRecording(enableMic: Boolean, onRecordingError: (RecordingError) -> Unit) { + override fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: (RecordingError) -> Unit) { if (globalRecordingService?.isCurrentlyRecording() == true) { Log.w(TAG, "โš ๏ธ Global recording already in progress") return @@ -317,7 +321,8 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { action = ScreenRecordingService.ACTION_START_RECORDING putExtra(ScreenRecordingService.EXTRA_RESULT_CODE, resultCode) putExtra(ScreenRecordingService.EXTRA_RESULT_DATA, resultData) - putExtra(ScreenRecordingService.EXTRA_ENABLE_MIC, enableMic) // Use the parameter instead of hardcoded true + putExtra(ScreenRecordingService.EXTRA_ENABLE_MIC, enableMic) + putExtra(ScreenRecordingService.EXTRA_SEPARATE_AUDIO, separateAudioFile) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -362,12 +367,27 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { override fun retrieveLastGlobalRecording(): ScreenRecordingFile? { return lastGlobalRecording?.let { file -> if (file.exists()) { + // Build audio file info if available + val audioFile = lastGlobalAudioRecording?.let { audioFile -> + if (audioFile.exists()) { + AudioRecordingFile( + path = "file://${audioFile.absolutePath}", + name = audioFile.name, + size = audioFile.length().toDouble(), + duration = RecorderUtils.getAudioDuration(audioFile) + ) + } else { + null + } + } + ScreenRecordingFile( path = "file://${file.absolutePath}", name = file.name, size = file.length().toDouble(), duration = RecorderUtils.getVideoDuration(file), - enabledMicrophone = true // Assume true for global recordings + enabledMicrophone = true, // Assume true for global recordings + audioFile = audioFile ) } else { null @@ -381,5 +401,6 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { val globalDir = File(ctx.filesDir, "recordings") RecorderUtils.clearDirectory(globalDir) lastGlobalRecording = null + lastGlobalAudioRecording = null } } diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/RecorderUtils.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/RecorderUtils.kt index e0ce4ea..2a5843f 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/RecorderUtils.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/RecorderUtils.kt @@ -102,6 +102,25 @@ object RecorderUtils { return file } + /** + * Creates a new audio file in the specified directory. + */ + fun createAudioOutputFile(directory: File, prefix: String): File { + Log.d(TAG, "๐Ÿ“ Creating audio output file with prefix '$prefix'...") + if (!directory.exists()) { + Log.d(TAG, "๐Ÿ“ Creating directory: ${directory.absolutePath}") + directory.mkdirs() + } + + val timestamp = + SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + // Use .m4a extension for extracted audio (AAC in MPEG-4 container) + val fileName = "${prefix}_$timestamp.m4a" + val file = File(directory, fileName) + Log.d(TAG, "๐Ÿ“ Created audio output file: ${file.absolutePath}") + return file + } + /** * Retrieves the duration of a video file in **seconds**. */ @@ -178,13 +197,131 @@ object RecorderUtils { } /** - * Deletes all .mp4 files in a given directory. + * Retrieves the duration of an audio file in **seconds**. + */ + fun getAudioDuration(file: File): Double { + if (!file.exists()) return 0.0 + return try { + val retriever = MediaMetadataRetriever().apply { + setDataSource(file.absolutePath) + } + val seconds = retriever + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + ?.toDouble() + ?.div(1_000.0) ?: 0.0 + retriever.release() + seconds + } catch (e: Exception) { + Log.w(TAG, "Could not get audio duration: ${e.message}") + 0.0 + } + } + + /** + * Extracts the audio track from a video file and saves it as a separate M4A file. + * This allows having audio in both the video AND a separate audio file. + * + * @param videoFile The source video file containing audio + * @param audioOutputFile The destination file for the extracted audio + * @return true if extraction was successful, false otherwise + */ + fun extractAudioFromVideo(videoFile: File, audioOutputFile: File): Boolean { + if (!videoFile.exists()) { + Log.e(TAG, "โŒ Video file does not exist: ${videoFile.absolutePath}") + return false + } + + var extractor: android.media.MediaExtractor? = null + var muxer: android.media.MediaMuxer? = null + + try { + Log.d(TAG, "๐ŸŽต Extracting audio from video: ${videoFile.name}") + + extractor = android.media.MediaExtractor() + extractor.setDataSource(videoFile.absolutePath) + + // Find the audio track + var audioTrackIndex = -1 + var audioFormat: android.media.MediaFormat? = null + + for (i in 0 until extractor.trackCount) { + val format = extractor.getTrackFormat(i) + val mime = format.getString(android.media.MediaFormat.KEY_MIME) + if (mime?.startsWith("audio/") == true) { + audioTrackIndex = i + audioFormat = format + break + } + } + + if (audioTrackIndex == -1 || audioFormat == null) { + Log.w(TAG, "โš ๏ธ No audio track found in video file") + return false + } + + Log.d(TAG, "๐Ÿ“ Found audio track at index $audioTrackIndex") + + // Select the audio track + extractor.selectTrack(audioTrackIndex) + + // Create muxer for output + muxer = android.media.MediaMuxer( + audioOutputFile.absolutePath, + android.media.MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 + ) + + val outputTrackIndex = muxer.addTrack(audioFormat) + muxer.start() + + // Allocate buffer for reading samples + val maxBufferSize = audioFormat.getInteger(android.media.MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 1024) + val buffer = java.nio.ByteBuffer.allocate(maxBufferSize) + val bufferInfo = android.media.MediaCodec.BufferInfo() + + // Copy all audio samples + while (true) { + val sampleSize = extractor.readSampleData(buffer, 0) + if (sampleSize < 0) { + break + } + + bufferInfo.offset = 0 + bufferInfo.size = sampleSize + bufferInfo.presentationTimeUs = extractor.sampleTime + bufferInfo.flags = extractor.sampleFlags + + muxer.writeSampleData(outputTrackIndex, buffer, bufferInfo) + extractor.advance() + } + + Log.d(TAG, "โœ… Audio extraction complete: ${audioOutputFile.name}") + return true + + } catch (e: Exception) { + Log.e(TAG, "โŒ Error extracting audio: ${e.message}") + e.printStackTrace() + // Clean up failed output file + audioOutputFile.delete() + return false + } finally { + try { + muxer?.stop() + muxer?.release() + } catch (e: Exception) { + Log.w(TAG, "Warning during muxer cleanup: ${e.message}") + } + extractor?.release() + } + } + + /** + * Deletes all .mp4 and .m4a files in a given directory. */ fun clearDirectory(directory: File) { Log.d(TAG, "๐Ÿงน Clearing directory: ${directory.absolutePath}") if (directory.exists() && directory.isDirectory) { directory.listFiles()?.forEach { file -> - if (file.isFile && file.name.endsWith(".mp4")) { + if (file.isFile && (file.name.endsWith(".mp4") || file.name.endsWith(".m4a"))) { if (file.delete()) { Log.d(TAG, "๐Ÿ—‘๏ธ Deleted file: ${file.name}") } else { diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingService.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingService.kt index 8ecc74f..2efdc3c 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingService.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingService.kt @@ -24,6 +24,10 @@ class ScreenRecordingService : Service() { private var isRecording = false private var currentRecordingFile: File? = null private var enableMic = false + + // Separate audio file extraction (done post-recording) + private var separateAudioFile = false + private var currentAudioFile: File? = null private var screenWidth = 0 private var screenHeight = 0 @@ -50,6 +54,7 @@ class ScreenRecordingService : Service() { const val EXTRA_RESULT_CODE = "RESULT_CODE" const val EXTRA_RESULT_DATA = "RESULT_DATA" const val EXTRA_ENABLE_MIC = "ENABLE_MIC" + const val EXTRA_SEPARATE_AUDIO = "SEPARATE_AUDIO" } inner class LocalBinder : Binder() { @@ -88,14 +93,15 @@ class ScreenRecordingService : Service() { intent.getIntExtra(EXTRA_RESULT_CODE, Activity.RESULT_CANCELED) val resultData = intent.getParcelableExtra(EXTRA_RESULT_DATA) val enableMicrophone = intent.getBooleanExtra(EXTRA_ENABLE_MIC, false) + val separateAudio = intent.getBooleanExtra(EXTRA_SEPARATE_AUDIO, false) Log.d( TAG, - "๐ŸŽฌ Start recording: resultCode=$resultCode, enableMic=$enableMicrophone" + "๐ŸŽฌ Start recording: resultCode=$resultCode, enableMic=$enableMicrophone, separateAudio=$separateAudio" ) if (resultData != null) { - startRecording(resultCode, resultData, enableMicrophone) + startRecording(resultCode, resultData, enableMicrophone, separateAudio) } else { Log.e(TAG, "โŒ ResultData is null, cannot start recording") } @@ -141,11 +147,12 @@ class ScreenRecordingService : Service() { fun startRecording( resultCode: Int, resultData: Intent, - enableMicrophone: Boolean + enableMicrophone: Boolean, + separateAudio: Boolean = false ) { Log.d( TAG, - "๐ŸŽฌ startRecording called: resultCode=$resultCode, enableMic=$enableMicrophone" + "๐ŸŽฌ startRecording called: resultCode=$resultCode, enableMic=$enableMicrophone, separateAudio=$separateAudio" ) if (isRecording) { @@ -155,6 +162,7 @@ class ScreenRecordingService : Service() { try { this.enableMic = enableMicrophone + this.separateAudioFile = separateAudio startForeground(NOTIFICATION_ID, createForegroundNotification(false)) @@ -173,6 +181,8 @@ class ScreenRecordingService : Service() { currentRecordingFile = RecorderUtils.createOutputFile(recordingsDir, "global_recording") + // Record video with audio embedded normally + // If separateAudioFile is requested, we'll extract it after recording stops mediaRecorder = RecorderUtils.setupMediaRecorder( this, enableMicrophone, @@ -230,6 +240,7 @@ class ScreenRecordingService : Service() { } var recordingFile: File? = null + var audioFile: File? = null try { mediaRecorder?.stop() @@ -241,12 +252,29 @@ class ScreenRecordingService : Service() { recordingFile = RecorderUtils.optimizeForStreaming(it) } + // Extract audio to separate file if requested and mic was enabled + if (separateAudioFile && enableMic && recordingFile != null) { + val base = applicationContext.externalCacheDir ?: applicationContext.filesDir + val recordingsDir = File(base, "recordings") + currentAudioFile = RecorderUtils.createAudioOutputFile(recordingsDir, "global_recording_audio") + + val extracted = RecorderUtils.extractAudioFromVideo(recordingFile!!, currentAudioFile!!) + if (extracted) { + audioFile = currentAudioFile + Log.d(TAG, "๐ŸŽต Audio extracted to separate file: ${audioFile?.absolutePath}") + } else { + Log.w(TAG, "โš ๏ธ Failed to extract audio from video") + currentAudioFile?.delete() + currentAudioFile = null + } + } + val event = ScreenRecordingEvent( type = RecordingEventType.GLOBAL, reason = RecordingEventReason.ENDED ) recordingFile?.let { - NitroScreenRecorder.notifyGlobalRecordingFinished(it, event, enableMic) + NitroScreenRecorder.notifyGlobalRecordingFinished(it, audioFile, event, enableMic) } Log.d(TAG, "๐ŸŽ‰ Global screen recording stopped successfully") @@ -276,6 +304,10 @@ class ScreenRecordingService : Service() { virtualDisplay = null mediaRecorder?.release() mediaRecorder = null + + // Reset audio file state + currentAudioFile = null + separateAudioFile = false // Unregister callback before stopping MediaProjection mediaProjection?.unregisterCallback(mediaProjectionCallback) diff --git a/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift index 2d726b8..44d82b1 100644 --- a/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift +++ b/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -41,8 +41,14 @@ enum Error: Swift.Error { public final class BroadcastWriter { private var assetWriterSessionStarted: Bool = false + private var audioAssetWriterSessionStarted: Bool = false private let assetWriterQueue: DispatchQueue private let assetWriter: AVAssetWriter + + // Separate audio writer + private var separateAudioWriter: AVAssetWriter? + private let separateAudioFile: Bool + private let audioOutputURL: URL? private lazy var videoInput: AVAssetWriterInput = { [unowned self] in let videoWidth = screenSize.width * screenScale @@ -118,6 +124,22 @@ public final class BroadcastWriter { input.expectsMediaDataInRealTime = true return input }() + + // Separate audio file input (for microphone audio only) + private lazy var separateAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() private lazy var inputs: [AVAssetWriterInput] = [ videoInput, @@ -130,9 +152,11 @@ public final class BroadcastWriter { public init( outputURL url: URL, + audioOutputURL: URL? = nil, assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), screenSize: CGSize, - screenScale: CGFloat + screenScale: CGFloat, + separateAudioFile: Bool = false ) throws { assetWriterQueue = queue assetWriter = try .init(url: url, fileType: .mp4) @@ -140,6 +164,14 @@ public final class BroadcastWriter { self.screenSize = screenSize self.screenScale = screenScale + self.separateAudioFile = separateAudioFile + self.audioOutputURL = audioOutputURL + + // Initialize separate audio writer if needed + if separateAudioFile, let audioURL = audioOutputURL { + separateAudioWriter = try .init(url: audioURL, fileType: .m4a) + separateAudioWriter?.shouldOptimizeForNetworkUse = true + } } public func start() throws { @@ -162,6 +194,21 @@ public final class BroadcastWriter { try assetWriter.error.map { throw $0 } + + // Start separate audio writer if enabled + if separateAudioFile, let audioWriter = separateAudioWriter { + let audioStatus = audioWriter.status + guard audioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(audioStatus) + } + try audioWriter.error.map { throw $0 } + if audioWriter.canAdd(separateAudioInput) { + audioWriter.add(separateAudioInput) + } + try audioWriter.error.map { throw $0 } + audioWriter.startWriting() + try audioWriter.error.map { throw $0 } + } } } @@ -206,6 +253,12 @@ public final class BroadcastWriter { capture = captureAudioOutput case .audioMic: capture = captureMicrophoneOutput + // Also write to separate audio file if enabled + if separateAudioFile { + assetWriterQueue.sync { + _ = captureSeparateAudioOutput(sampleBuffer) + } + } @unknown default: debugPrint(#file, "Unknown type of sample buffer, \(sampleBufferType)") capture = { _ in false } @@ -224,7 +277,18 @@ public final class BroadcastWriter { // TODO: Resume } + /// Result containing both video and optional audio URLs + public struct FinishResult { + public let videoURL: URL + public let audioURL: URL? + } + public func finish() throws -> URL { + let result = try finishWithAudio() + return result.videoURL + } + + public func finishWithAudio() throws -> FinishResult { return try assetWriterQueue.sync { let group: DispatchGroup = .init() @@ -264,7 +328,38 @@ public final class BroadcastWriter { } group.wait() try error.map { throw $0 } - return assetWriter.outputURL + + // Finish separate audio writer if enabled + var audioURL: URL? = nil + if separateAudioFile, let audioWriter = separateAudioWriter { + if separateAudioInput.isReadyForMoreMediaData { + separateAudioInput.markAsFinished() + } + + if audioWriter.status == .writing { + let audioGroup = DispatchGroup() + audioGroup.enter() + + var audioError: Swift.Error? + audioWriter.finishWriting { + defer { audioGroup.leave() } + if let e = audioWriter.error { + audioError = e + return + } + if audioWriter.status != .completed { + audioError = Error.wrongAssetWriterStatus(audioWriter.status) + } + } + audioGroup.wait() + + if audioError == nil { + audioURL = audioWriter.outputURL + } + } + } + + return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) } } } @@ -280,6 +375,16 @@ extension BroadcastWriter { assetWriter.startSession(atSourceTime: sourceTime) assetWriterSessionStarted = true } + + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + audioWriter.startSession(atSourceTime: sourceTime) + audioAssetWriterSessionStarted = true + } fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard videoInput.isReadyForMoreMediaData else { @@ -305,4 +410,25 @@ extension BroadcastWriter { } return microphoneInput.append(sampleBuffer) } + + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let audioWriter = separateAudioWriter else { + return false + } + + // Check if audio writer is still writing + guard audioWriter.status == .writing else { + debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") + return false + } + + // Start session if needed + startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard separateAudioInput.isReadyForMoreMediaData else { + debugPrint("separateAudioInput is not ready") + return false + } + return separateAudioInput.append(sampleBuffer) + } } diff --git a/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift index 43c3d7a..f8feae7 100644 --- a/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift +++ b/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -35,15 +35,23 @@ final class SampleHandler: RPBroadcastSampleHandler { private var writer: BroadcastWriter? private let fileManager: FileManager = .default private let nodeURL: URL + private let audioNodeURL: URL private var sawMicBuffers = false + private var separateAudioFile: Bool = false // MARK: โ€“ Init override init() { + let uuid = UUID().uuidString nodeURL = fileManager.temporaryDirectory - .appendingPathComponent(UUID().uuidString) + .appendingPathComponent(uuid) .appendingPathExtension(for: .mpeg4Movie) + + audioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_audio") + .appendingPathExtension("m4a") fileManager.removeFileIfExists(url: nodeURL) + fileManager.removeFileIfExists(url: audioNodeURL) super.init() } @@ -95,6 +103,11 @@ final class SampleHandler: RPBroadcastSampleHandler { return } + // Check if separate audio file is requested + if let userDefaults = UserDefaults(suiteName: groupID) { + separateAudioFile = userDefaults.bool(forKey: "SeparateAudioFileEnabled") + } + // Clean up old recordings cleanupOldRecordings(in: groupID) @@ -103,8 +116,10 @@ final class SampleHandler: RPBroadcastSampleHandler { do { writer = try .init( outputURL: nodeURL, + audioOutputURL: separateAudioFile ? audioNodeURL : nil, screenSize: screen.bounds.size, - screenScale: screen.scale + screenScale: screen.scale, + separateAudioFile: separateAudioFile ) try writer?.start() } catch { @@ -160,10 +175,10 @@ final class SampleHandler: RPBroadcastSampleHandler { override func broadcastFinished() { guard let writer else { return } - // Finish writing - let outputURL: URL + // Finish writing - use finishWithAudio to get both video and audio URLs + let result: BroadcastWriter.FinishResult do { - outputURL = try writer.finish() + result = try writer.finishWithAudio() } catch { // Writer failed, but we can't call finishBroadcastWithError here // as we're already in the finish process @@ -185,18 +200,38 @@ final class SampleHandler: RPBroadcastSampleHandler { return } - // Move file to shared container - let destination = containerURL.appendingPathComponent(outputURL.lastPathComponent) + // Move video file to shared container + let videoDestination = containerURL.appendingPathComponent(result.videoURL.lastPathComponent) do { - try fileManager.moveItem(at: outputURL, to: destination) + try fileManager.moveItem(at: result.videoURL, to: videoDestination) } catch { // File move failed, but we can't error out at this point return } + + // Move audio file to shared container if it exists + if let audioURL = result.audioURL { + let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) + do { + try fileManager.moveItem(at: audioURL, to: audioDestination) + // Store audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") + } catch { + // Audio file move failed, but video is already saved + debugPrint("Failed to move audio file: \(error)") + } + } else { + // Clear audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAudioFileName") + } - // Persist microphone state + // Persist microphone state and audio file state UserDefaults(suiteName: groupID)? .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") + UserDefaults(suiteName: groupID)? + .set(separateAudioFile, forKey: "LastBroadcastHadSeparateAudio") } } diff --git a/ios/NitroScreenRecorder.swift b/ios/NitroScreenRecorder.swift index 65ae330..6426f6b 100644 --- a/ios/NitroScreenRecorder.swift +++ b/ios/NitroScreenRecorder.swift @@ -33,6 +33,11 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { private var recordingEventListeners: [ScreenRecordingListenerType] = [] public var broadcastPickerEventListeners: [Listener] = [] private var nextListenerId: Double = 0 + + // Separate audio file recording + private var separateAudioFileEnabled: Bool = false + private var audioRecorder: AVAudioRecorder? + private var audioFileURL: URL? // App state tracking for broadcast modal private var isBroadcastModalShowing: Bool = false @@ -242,6 +247,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { enableCamera: Bool, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, + separateAudioFile: Bool, onRecordingFinished: @escaping RecordingFinishedCallback ) throws { safelyClearInAppRecordingFiles() @@ -278,6 +284,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } self.onInAppRecordingFinishedCallback = onRecordingFinished + self.separateAudioFileEnabled = separateAudioFile recorder.isMicrophoneEnabled = enableMic recorder.isCameraEnabled = enableCamera @@ -286,14 +293,21 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { recorder.cameraPosition = device } inAppRecordingActive = true + + // Start separate audio recording if enabled and mic is enabled + if separateAudioFile && enableMic { + startSeparateAudioRecording() + } + recorder.startRecording { [weak self] error in guard let self = self else { return } if let error = error { print("โŒ Error starting in-app recording:", error.localizedDescription) inAppRecordingActive = false + self.stopSeparateAudioRecording() return } - print("โœ… In-app recording started (mic:\(enableMic) camera:\(enableCamera))") + print("โœ… In-app recording started (mic:\(enableMic) camera:\(enableCamera) separateAudio:\(separateAudioFile))") if enableCamera { DispatchQueue.main.async { @@ -302,10 +316,75 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } } } + + private func startSeparateAudioRecording() { + let fileName = "audio_capture_\(UUID().uuidString).m4a" + audioFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) + + guard let audioURL = audioFileURL else { return } + + // Remove any existing file + try? FileManager.default.removeItem(at: audioURL) + + let audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVSampleRateKey: 44100.0, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, + AVEncoderBitRateKey: 128000 + ] + + do { + // Configure audio session + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) + try audioSession.setActive(true) + + audioRecorder = try AVAudioRecorder(url: audioURL, settings: audioSettings) + audioRecorder?.record() + print("โœ… Separate audio recording started: \(audioURL.path)") + } catch { + print("โŒ Failed to start separate audio recording: \(error.localizedDescription)") + audioRecorder = nil + audioFileURL = nil + } + } + + private func stopSeparateAudioRecording() -> AudioRecordingFile? { + guard let recorder = audioRecorder, let audioURL = audioFileURL else { + return nil + } + + recorder.stop() + audioRecorder = nil + + // Get audio file info + do { + let attrs = try FileManager.default.attributesOfItem(atPath: audioURL.path) + let asset = AVURLAsset(url: audioURL) + let duration = CMTimeGetSeconds(asset.duration) + + let audioFile = AudioRecordingFile( + path: audioURL.absoluteString, + name: audioURL.lastPathComponent, + size: attrs[.size] as? Double ?? 0, + duration: duration + ) + + print("โœ… Separate audio recording stopped: \(audioURL.path)") + return audioFile + } catch { + print("โŒ Failed to get audio file info: \(error.localizedDescription)") + return nil + } + } public func stopInAppRecording() throws -> Promise { return Promise.async { return await withCheckedContinuation { continuation in + // Stop separate audio recording first if enabled + let audioFile = self.separateAudioFileEnabled ? self.stopSeparateAudioRecording() : nil + // build a unique temp URL let fileName = "screen_capture_\(UUID().uuidString).mp4" let outputURL = FileManager.default.temporaryDirectory @@ -340,11 +419,16 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { name: outputURL.lastPathComponent, size: attrs[.size] as? Double ?? 0, duration: duration, - enabledMicrophone: self.recorder.isMicrophoneEnabled + enabledMicrophone: self.recorder.isMicrophoneEnabled, + audioFile: audioFile ) print("โœ… Recording finished and saved to:", outputURL.path) + if let audioFile = audioFile { + print("โœ… Separate audio file saved to:", audioFile.path) + } self.onInAppRecordingFinishedCallback?(file) + self.separateAudioFileEnabled = false continuation.resume(returning: file) } catch { print("โš ๏ธ Failed to build ScreenRecordingFile:", error.localizedDescription) @@ -358,6 +442,12 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { public func cancelInAppRecording() throws -> Promise { return Promise.async { return await withCheckedContinuation { continuation in + // Stop separate audio recording if active + if self.separateAudioFileEnabled { + _ = self.stopSeparateAudioRecording() + self.separateAudioFileEnabled = false + } + // If a recording session is in progress, stop it and write out to a temp URL if self.recorder.isRecording { let tempURL = FileManager.default.temporaryDirectory @@ -452,7 +542,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } } - func startGlobalRecording(enableMic: Bool, onRecordingError: @escaping (RecordingError) -> Void) + func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (RecordingError) -> Void) throws { guard !isGlobalRecordingActive else { @@ -488,6 +578,10 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { return } + // Store the separateAudioFile preference for the broadcast extension to read + self.separateAudioFileEnabled = separateAudioFile + UserDefaults(suiteName: appGroupId)?.set(separateAudioFile, forKey: "SeparateAudioFileEnabled") + // Present the broadcast picker presentGlobalBroadcastModal(enableMicrophone: enableMic) @@ -612,13 +706,53 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { let micEnabled = UserDefaults(suiteName: appGroupId)? .bool(forKey: "LastBroadcastMicrophoneWasEnabled") ?? false + + // Check for and retrieve separate audio file + var audioFile: AudioRecordingFile? = nil + let hadSeparateAudio = UserDefaults(suiteName: appGroupId)?.bool(forKey: "LastBroadcastHadSeparateAudio") ?? false + + if hadSeparateAudio, + let audioFileName = UserDefaults(suiteName: appGroupId)?.string(forKey: "LastBroadcastAudioFileName") { + let audioSourceURL = docsURL.appendingPathComponent(audioFileName) + + if fm.fileExists(atPath: audioSourceURL.path) { + // Copy audio file to caches + var audioDestinationURL = recordingsDir.appendingPathComponent(audioFileName) + if fm.fileExists(atPath: audioDestinationURL.path) { + let ts = Int(Date().timeIntervalSince1970) + let base = audioSourceURL.deletingPathExtension().lastPathComponent + audioDestinationURL = recordingsDir.appendingPathComponent("\(base)-\(ts).m4a") + } + + do { + try fm.copyItem(at: audioSourceURL, to: audioDestinationURL) + + let audioAttrs = try fm.attributesOfItem(atPath: audioDestinationURL.path) + let audioSize = (audioAttrs[.size] as? NSNumber)?.doubleValue ?? 0.0 + + let audioAsset = AVURLAsset(url: audioDestinationURL) + let audioDuration = CMTimeGetSeconds(audioAsset.duration) + + audioFile = AudioRecordingFile( + path: audioDestinationURL.absoluteString, + name: audioDestinationURL.lastPathComponent, + size: audioSize, + duration: audioDuration + ) + print("โœ… Retrieved separate audio file: \(audioDestinationURL.path)") + } catch { + print("โš ๏ธ Failed to copy audio file: \(error.localizedDescription)") + } + } + } return ScreenRecordingFile( path: destinationURL.absoluteString, name: destinationURL.lastPathComponent, size: size, duration: duration, - enabledMicrophone: micEnabled + enabledMicrophone: micEnabled, + audioFile: audioFile ) } diff --git a/src/NitroScreenRecorder.nitro.ts b/src/NitroScreenRecorder.nitro.ts index 7aab6ab..15b00a5 100644 --- a/src/NitroScreenRecorder.nitro.ts +++ b/src/NitroScreenRecorder.nitro.ts @@ -55,6 +55,7 @@ export interface NitroScreenRecorder enableCamera: boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, + separateAudioFile: boolean, onRecordingFinished: (file: ScreenRecordingFile) => void // onRecordingError: (error: RecordingError) => void ): void; @@ -67,6 +68,7 @@ export interface NitroScreenRecorder startGlobalRecording( enableMic: boolean, + separateAudioFile: boolean, onRecordingError: (error: RecordingError) => void ): void; stopGlobalRecording( diff --git a/src/functions.ts b/src/functions.ts index 19faad6..989f4de 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -141,6 +141,7 @@ export async function startInAppRecording( input.options.enableCamera, input.options.cameraPreviewStyle ?? {}, input.options.cameraDevice, + input.options.separateAudioFile ?? false, input.onRecordingFinished // input.onRecordingError ); @@ -150,6 +151,7 @@ export async function startInAppRecording( input.options.enableCamera, {}, 'front', + input.options.separateAudioFile ?? false, input.onRecordingFinished // input.onRecordingError ); @@ -222,6 +224,7 @@ export function startGlobalRecording(input: GlobalRecordingInput): void { } return NitroScreenRecorderHybridObject.startGlobalRecording( input?.options?.enableMic ?? false, + input?.options?.separateAudioFile ?? false, input?.onRecordingError ); } diff --git a/src/types.ts b/src/types.ts index 376242b..f5534ce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -118,12 +118,24 @@ export type InAppRecordingOptions = cameraPreviewStyle: RecorderCameraStyle; /** Which camera to use */ cameraDevice: CameraDevice; + /** + * Whether to write audio to a separate file alongside the video. + * When enabled, the audioFile property will be populated in ScreenRecordingFile. + * @default false + */ + separateAudioFile?: boolean; } | { /** Camera is disabled - no camera options needed */ enableCamera: false; /** Whether to record microphone audio */ enableMic: boolean; + /** + * Whether to write audio to a separate file alongside the video. + * When enabled, the audioFile property will be populated in ScreenRecordingFile. + * @default false + */ + separateAudioFile?: boolean; }; /** @@ -157,6 +169,18 @@ export type InAppRecordingInput = { export type GlobalRecordingInputOptions = { /** Whether to record microphone audio during the global recording. */ enableMic: boolean; + /** + * Whether to write audio to a separate file alongside the video. + * When enabled, the audioFile property will be populated in ScreenRecordingFile. + * The separate audio file will contain microphone audio (if enabled). + * + * On both iOS and Android, the video will contain embedded audio AND + * a separate audio file will be created. On Android, the audio is extracted + * from the video after recording stops. + * + * @default false + */ + separateAudioFile?: boolean; }; /** @@ -182,6 +206,30 @@ export type GlobalRecordingInput = { onRecordingError: (error: RecordingError) => void; }; +/** + * Represents a separate audio file recorded alongside the video. + * + * @example + * ```typescript + * const audioFile: AudioRecordingFile = { + * path: '/path/to/recording.m4a', + * name: 'screen_recording_2024_01_15.m4a', + * size: 1048576, // 1MB in bytes + * duration: 30.5 // 30.5 seconds + * }; + * ``` + */ +export interface AudioRecordingFile { + /** Full file system path to the audio file */ + path: string; + /** Display name of the audio file */ + name: string; + /** File size in bytes */ + size: number; + /** Audio duration in seconds */ + duration: number; +} + /** * Represents a completed screen recording file with metadata. * Contains all information needed to access and display the recording. @@ -193,7 +241,13 @@ export type GlobalRecordingInput = { * name: 'screen_recording_2024_01_15.mp4', * size: 15728640, // 15MB in bytes * duration: 30.5, // 30.5 seconds - * enabledMicrophone: true + * enabledMicrophone: true, + * audioFile: { + * path: '/path/to/recording.m4a', + * name: 'screen_recording_2024_01_15.m4a', + * size: 1048576, + * duration: 30.5 + * } * }; * ``` */ @@ -208,6 +262,8 @@ export interface ScreenRecordingFile { duration: number; /** Whether microphone audio was recorded */ enabledMicrophone: boolean; + /** Optional separate audio file (when separateAudioFile option is enabled) */ + audioFile?: AudioRecordingFile; } /** From 56033c863070016fb1e3c1bf30b48dc08ad28456 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 5 Dec 2025 17:34:37 -0800 Subject: [PATCH 02/32] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c312069..feb3946 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.6.1", + "version": "0.7.0", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", From c0a4717a6ae985aed591d861c63ae836a80bbbaa Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 5 Dec 2025 17:50:30 -0800 Subject: [PATCH 03/32] chore: add entire build --- .gitignore | 2 - lib/commonjs/NitroScreenRecorder.nitro.js | 6 + lib/commonjs/NitroScreenRecorder.nitro.js.map | 1 + lib/commonjs/expo-plugin/@types.js | 2 + .../android/withAndroidScreenRecording.js | 212 +++++++++ .../eas/getEasManagedCredentials.js | 43 ++ .../expo-plugin/ios/withBroadcastExtension.js | 27 ++ .../ios/withBroadcastExtensionFiles.js | 89 ++++ .../ios/withBroadcastExtensionPodfile.js | 21 + .../ios/withBroadcastExtensionXcodeProject.js | 139 ++++++ .../ios/withEasManagedCredentials.js | 12 + .../ios/withMainAppAppGroupEntitlement.js | 32 ++ .../ios/withMainAppAppGroupInfoPlist.js | 24 + .../ios/withMainAppEntitlementsFile.js | 98 ++++ .../expo-plugin/support/BEUpdateManager.js | 47 ++ .../expo-plugin/support/BEUpdateManager.ts | 68 +++ .../expo-plugin/support/FileManager.js | 75 +++ .../expo-plugin/support/FileManager.ts | 42 ++ .../expo-plugin/support/ScreenRecorderLog.js | 17 + .../expo-plugin/support/ScreenRecorderLog.ts | 15 + .../BroadcastExtension-Bridging-Header.h | 1 + .../BroadcastExtension-Info.plist | 47 ++ .../BroadcastExtension-PrivacyInfo.xcprivacy | 26 ++ .../BroadcastExtension.entitlements | 10 + .../broadcastExtensionFiles/BroadcastHelper.h | 7 + .../broadcastExtensionFiles/BroadcastHelper.m | 8 + .../BroadcastWriter.swift | 434 ++++++++++++++++++ .../SampleHandler.swift | 244 ++++++++++ .../expo-plugin/support/iosConstants.js | 60 +++ .../expo-plugin/support/iosConstants.ts | 66 +++ .../expo-plugin/support/updatePodfile.js | 24 + .../expo-plugin/support/updatePodfile.ts | 31 ++ .../support/validatePluginProps.js | 54 +++ .../support/validatePluginProps.ts | 95 ++++ .../expo-plugin/withScreenRecorder.js | 43 ++ lib/commonjs/functions.js | 336 ++++++++++++++ lib/commonjs/functions.js.map | 1 + lib/commonjs/hooks/index.js | 17 + lib/commonjs/hooks/index.js.map | 1 + lib/commonjs/hooks/useCameraMicPermissions.js | 69 +++ .../hooks/useCameraMicPermissions.js.map | 1 + lib/commonjs/hooks/useGlobalRecording.js | 105 +++++ lib/commonjs/hooks/useGlobalRecording.js.map | 1 + lib/commonjs/index.js | 39 ++ lib/commonjs/index.js.map | 1 + lib/commonjs/package.json | 1 + lib/commonjs/types.js | 2 + lib/commonjs/types.js.map | 1 + lib/module/NitroScreenRecorder.nitro.js | 4 + lib/module/NitroScreenRecorder.nitro.js.map | 1 + lib/module/expo-plugin/@types.d.ts | 58 +++ lib/module/expo-plugin/@types.js | 2 + .../android/withAndroidScreenRecording.d.ts | 3 + .../android/withAndroidScreenRecording.js | 212 +++++++++ .../eas/getEasManagedCredentials.d.ts | 5 + .../eas/getEasManagedCredentials.js | 43 ++ .../ios/withBroadcastExtension.d.ts | 3 + .../expo-plugin/ios/withBroadcastExtension.js | 27 ++ .../ios/withBroadcastExtensionFiles.d.ts | 8 + .../ios/withBroadcastExtensionFiles.js | 89 ++++ .../ios/withBroadcastExtensionPodfile.d.ts | 3 + .../ios/withBroadcastExtensionPodfile.js | 21 + .../withBroadcastExtensionXcodeProject.d.ts | 3 + .../ios/withBroadcastExtensionXcodeProject.js | 139 ++++++ .../ios/withEasManagedCredentials.d.ts | 3 + .../ios/withEasManagedCredentials.js | 12 + .../ios/withMainAppAppGroupEntitlement.d.ts | 6 + .../ios/withMainAppAppGroupEntitlement.js | 32 ++ .../ios/withMainAppAppGroupInfoPlist.d.ts | 3 + .../ios/withMainAppAppGroupInfoPlist.js | 24 + .../ios/withMainAppEntitlementsFile.d.ts | 7 + .../ios/withMainAppEntitlementsFile.js | 98 ++++ .../expo-plugin/support/BEUpdateManager.d.ts | 20 + .../expo-plugin/support/BEUpdateManager.js | 47 ++ .../expo-plugin/support/BEUpdateManager.ts | 68 +++ .../expo-plugin/support/FileManager.d.ts | 9 + lib/module/expo-plugin/support/FileManager.js | 75 +++ lib/module/expo-plugin/support/FileManager.ts | 42 ++ .../support/ScreenRecorderLog.d.ts | 5 + .../expo-plugin/support/ScreenRecorderLog.js | 17 + .../expo-plugin/support/ScreenRecorderLog.ts | 15 + .../BroadcastExtension-Bridging-Header.h | 1 + .../BroadcastExtension-Info.plist | 47 ++ .../BroadcastExtension-PrivacyInfo.xcprivacy | 26 ++ .../BroadcastExtension.entitlements | 10 + .../broadcastExtensionFiles/BroadcastHelper.h | 7 + .../broadcastExtensionFiles/BroadcastHelper.m | 8 + .../BroadcastWriter.swift | 434 ++++++++++++++++++ .../SampleHandler.swift | 244 ++++++++++ .../expo-plugin/support/iosConstants.d.ts | 16 + .../expo-plugin/support/iosConstants.js | 60 +++ .../expo-plugin/support/iosConstants.ts | 66 +++ .../expo-plugin/support/updatePodfile.d.ts | 2 + .../expo-plugin/support/updatePodfile.js | 24 + .../expo-plugin/support/updatePodfile.ts | 31 ++ .../support/validatePluginProps.d.ts | 5 + .../support/validatePluginProps.js | 54 +++ .../support/validatePluginProps.ts | 95 ++++ .../expo-plugin/withScreenRecorder.d.ts | 4 + lib/module/expo-plugin/withScreenRecorder.js | 43 ++ lib/module/functions.js | 320 +++++++++++++ lib/module/functions.js.map | 1 + lib/module/hooks/index.js | 4 + lib/module/hooks/index.js.map | 1 + lib/module/hooks/useCameraMicPermissions.js | 64 +++ .../hooks/useCameraMicPermissions.js.map | 1 + lib/module/hooks/useGlobalRecording.js | 100 ++++ lib/module/hooks/useGlobalRecording.js.map | 1 + lib/module/index.js | 6 + lib/module/index.js.map | 1 + lib/module/types.js | 2 + lib/module/types.js.map | 1 + lib/typescript/NitroScreenRecorder.nitro.d.ts | 32 ++ .../NitroScreenRecorder.nitro.d.ts.map | 1 + lib/typescript/expo-plugin/@types.d.ts | 58 +++ lib/typescript/expo-plugin/@types.js | 2 + .../android/withAndroidScreenRecording.d.ts | 3 + .../android/withAndroidScreenRecording.js | 212 +++++++++ .../eas/getEasManagedCredentials.d.ts | 5 + .../eas/getEasManagedCredentials.js | 43 ++ .../ios/withBroadcastExtension.d.ts | 3 + .../expo-plugin/ios/withBroadcastExtension.js | 27 ++ .../ios/withBroadcastExtensionFiles.d.ts | 8 + .../ios/withBroadcastExtensionFiles.js | 89 ++++ .../ios/withBroadcastExtensionPodfile.d.ts | 3 + .../ios/withBroadcastExtensionPodfile.js | 21 + .../withBroadcastExtensionXcodeProject.d.ts | 3 + .../ios/withBroadcastExtensionXcodeProject.js | 139 ++++++ .../ios/withEasManagedCredentials.d.ts | 3 + .../ios/withEasManagedCredentials.js | 12 + .../ios/withMainAppAppGroupEntitlement.d.ts | 6 + .../ios/withMainAppAppGroupEntitlement.js | 32 ++ .../ios/withMainAppAppGroupInfoPlist.d.ts | 3 + .../ios/withMainAppAppGroupInfoPlist.js | 24 + .../ios/withMainAppEntitlementsFile.d.ts | 7 + .../ios/withMainAppEntitlementsFile.js | 98 ++++ .../expo-plugin/support/BEUpdateManager.d.ts | 20 + .../expo-plugin/support/BEUpdateManager.js | 47 ++ .../expo-plugin/support/BEUpdateManager.ts | 68 +++ .../expo-plugin/support/FileManager.d.ts | 9 + .../expo-plugin/support/FileManager.js | 75 +++ .../expo-plugin/support/FileManager.ts | 42 ++ .../support/ScreenRecorderLog.d.ts | 5 + .../expo-plugin/support/ScreenRecorderLog.js | 17 + .../expo-plugin/support/ScreenRecorderLog.ts | 15 + .../BroadcastExtension-Bridging-Header.h | 1 + .../BroadcastExtension-Info.plist | 47 ++ .../BroadcastExtension-PrivacyInfo.xcprivacy | 26 ++ .../BroadcastExtension.entitlements | 10 + .../broadcastExtensionFiles/BroadcastHelper.h | 7 + .../broadcastExtensionFiles/BroadcastHelper.m | 8 + .../BroadcastWriter.swift | 434 ++++++++++++++++++ .../SampleHandler.swift | 244 ++++++++++ .../expo-plugin/support/iosConstants.d.ts | 16 + .../expo-plugin/support/iosConstants.js | 60 +++ .../expo-plugin/support/iosConstants.ts | 66 +++ .../expo-plugin/support/updatePodfile.d.ts | 2 + .../expo-plugin/support/updatePodfile.js | 24 + .../expo-plugin/support/updatePodfile.ts | 31 ++ .../support/validatePluginProps.d.ts | 5 + .../support/validatePluginProps.js | 54 +++ .../support/validatePluginProps.ts | 95 ++++ .../expo-plugin/withScreenRecorder.d.ts | 4 + .../expo-plugin/withScreenRecorder.js | 43 ++ lib/typescript/functions.d.ts | 206 +++++++++ lib/typescript/functions.d.ts.map | 1 + lib/typescript/hooks/index.d.ts | 2 + lib/typescript/hooks/index.d.ts.map | 1 + .../hooks/useCameraMicPermissions.d.ts | 46 ++ .../hooks/useCameraMicPermissions.d.ts.map | 1 + lib/typescript/hooks/useGlobalRecording.d.ts | 97 ++++ .../hooks/useGlobalRecording.d.ts.map | 1 + lib/typescript/index.d.ts | 4 + lib/typescript/index.d.ts.map | 1 + lib/typescript/types.d.ts | 326 +++++++++++++ lib/typescript/types.d.ts.map | 1 + 176 files changed, 8474 insertions(+), 2 deletions(-) create mode 100644 lib/commonjs/NitroScreenRecorder.nitro.js create mode 100644 lib/commonjs/NitroScreenRecorder.nitro.js.map create mode 100644 lib/commonjs/expo-plugin/@types.js create mode 100644 lib/commonjs/expo-plugin/android/withAndroidScreenRecording.js create mode 100644 lib/commonjs/expo-plugin/eas/getEasManagedCredentials.js create mode 100644 lib/commonjs/expo-plugin/ios/withBroadcastExtension.js create mode 100644 lib/commonjs/expo-plugin/ios/withBroadcastExtensionFiles.js create mode 100644 lib/commonjs/expo-plugin/ios/withBroadcastExtensionPodfile.js create mode 100644 lib/commonjs/expo-plugin/ios/withBroadcastExtensionXcodeProject.js create mode 100644 lib/commonjs/expo-plugin/ios/withEasManagedCredentials.js create mode 100644 lib/commonjs/expo-plugin/ios/withMainAppAppGroupEntitlement.js create mode 100644 lib/commonjs/expo-plugin/ios/withMainAppAppGroupInfoPlist.js create mode 100644 lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js create mode 100644 lib/commonjs/expo-plugin/support/BEUpdateManager.js create mode 100644 lib/commonjs/expo-plugin/support/BEUpdateManager.ts create mode 100644 lib/commonjs/expo-plugin/support/FileManager.js create mode 100644 lib/commonjs/expo-plugin/support/FileManager.ts create mode 100644 lib/commonjs/expo-plugin/support/ScreenRecorderLog.js create mode 100644 lib/commonjs/expo-plugin/support/ScreenRecorderLog.ts create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift create mode 100644 lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift create mode 100644 lib/commonjs/expo-plugin/support/iosConstants.js create mode 100644 lib/commonjs/expo-plugin/support/iosConstants.ts create mode 100644 lib/commonjs/expo-plugin/support/updatePodfile.js create mode 100644 lib/commonjs/expo-plugin/support/updatePodfile.ts create mode 100644 lib/commonjs/expo-plugin/support/validatePluginProps.js create mode 100644 lib/commonjs/expo-plugin/support/validatePluginProps.ts create mode 100644 lib/commonjs/expo-plugin/withScreenRecorder.js create mode 100644 lib/commonjs/functions.js create mode 100644 lib/commonjs/functions.js.map create mode 100644 lib/commonjs/hooks/index.js create mode 100644 lib/commonjs/hooks/index.js.map create mode 100644 lib/commonjs/hooks/useCameraMicPermissions.js create mode 100644 lib/commonjs/hooks/useCameraMicPermissions.js.map create mode 100644 lib/commonjs/hooks/useGlobalRecording.js create mode 100644 lib/commonjs/hooks/useGlobalRecording.js.map create mode 100644 lib/commonjs/index.js create mode 100644 lib/commonjs/index.js.map create mode 100644 lib/commonjs/package.json create mode 100644 lib/commonjs/types.js create mode 100644 lib/commonjs/types.js.map create mode 100644 lib/module/NitroScreenRecorder.nitro.js create mode 100644 lib/module/NitroScreenRecorder.nitro.js.map create mode 100644 lib/module/expo-plugin/@types.d.ts create mode 100644 lib/module/expo-plugin/@types.js create mode 100644 lib/module/expo-plugin/android/withAndroidScreenRecording.d.ts create mode 100644 lib/module/expo-plugin/android/withAndroidScreenRecording.js create mode 100644 lib/module/expo-plugin/eas/getEasManagedCredentials.d.ts create mode 100644 lib/module/expo-plugin/eas/getEasManagedCredentials.js create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtension.d.ts create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtension.js create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtensionFiles.d.ts create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtensionFiles.js create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.js create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts create mode 100644 lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.js create mode 100644 lib/module/expo-plugin/ios/withEasManagedCredentials.d.ts create mode 100644 lib/module/expo-plugin/ios/withEasManagedCredentials.js create mode 100644 lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts create mode 100644 lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.js create mode 100644 lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts create mode 100644 lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.js create mode 100644 lib/module/expo-plugin/ios/withMainAppEntitlementsFile.d.ts create mode 100644 lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js create mode 100644 lib/module/expo-plugin/support/BEUpdateManager.d.ts create mode 100644 lib/module/expo-plugin/support/BEUpdateManager.js create mode 100644 lib/module/expo-plugin/support/BEUpdateManager.ts create mode 100644 lib/module/expo-plugin/support/FileManager.d.ts create mode 100644 lib/module/expo-plugin/support/FileManager.js create mode 100644 lib/module/expo-plugin/support/FileManager.ts create mode 100644 lib/module/expo-plugin/support/ScreenRecorderLog.d.ts create mode 100644 lib/module/expo-plugin/support/ScreenRecorderLog.js create mode 100644 lib/module/expo-plugin/support/ScreenRecorderLog.ts create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift create mode 100644 lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift create mode 100644 lib/module/expo-plugin/support/iosConstants.d.ts create mode 100644 lib/module/expo-plugin/support/iosConstants.js create mode 100644 lib/module/expo-plugin/support/iosConstants.ts create mode 100644 lib/module/expo-plugin/support/updatePodfile.d.ts create mode 100644 lib/module/expo-plugin/support/updatePodfile.js create mode 100644 lib/module/expo-plugin/support/updatePodfile.ts create mode 100644 lib/module/expo-plugin/support/validatePluginProps.d.ts create mode 100644 lib/module/expo-plugin/support/validatePluginProps.js create mode 100644 lib/module/expo-plugin/support/validatePluginProps.ts create mode 100644 lib/module/expo-plugin/withScreenRecorder.d.ts create mode 100644 lib/module/expo-plugin/withScreenRecorder.js create mode 100644 lib/module/functions.js create mode 100644 lib/module/functions.js.map create mode 100644 lib/module/hooks/index.js create mode 100644 lib/module/hooks/index.js.map create mode 100644 lib/module/hooks/useCameraMicPermissions.js create mode 100644 lib/module/hooks/useCameraMicPermissions.js.map create mode 100644 lib/module/hooks/useGlobalRecording.js create mode 100644 lib/module/hooks/useGlobalRecording.js.map create mode 100644 lib/module/index.js create mode 100644 lib/module/index.js.map create mode 100644 lib/module/types.js create mode 100644 lib/module/types.js.map create mode 100644 lib/typescript/NitroScreenRecorder.nitro.d.ts create mode 100644 lib/typescript/NitroScreenRecorder.nitro.d.ts.map create mode 100644 lib/typescript/expo-plugin/@types.d.ts create mode 100644 lib/typescript/expo-plugin/@types.js create mode 100644 lib/typescript/expo-plugin/android/withAndroidScreenRecording.d.ts create mode 100644 lib/typescript/expo-plugin/android/withAndroidScreenRecording.js create mode 100644 lib/typescript/expo-plugin/eas/getEasManagedCredentials.d.ts create mode 100644 lib/typescript/expo-plugin/eas/getEasManagedCredentials.js create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtension.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtension.js create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.js create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.js create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.js create mode 100644 lib/typescript/expo-plugin/ios/withEasManagedCredentials.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withEasManagedCredentials.js create mode 100644 lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.js create mode 100644 lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.js create mode 100644 lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.d.ts create mode 100644 lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js create mode 100644 lib/typescript/expo-plugin/support/BEUpdateManager.d.ts create mode 100644 lib/typescript/expo-plugin/support/BEUpdateManager.js create mode 100644 lib/typescript/expo-plugin/support/BEUpdateManager.ts create mode 100644 lib/typescript/expo-plugin/support/FileManager.d.ts create mode 100644 lib/typescript/expo-plugin/support/FileManager.js create mode 100644 lib/typescript/expo-plugin/support/FileManager.ts create mode 100644 lib/typescript/expo-plugin/support/ScreenRecorderLog.d.ts create mode 100644 lib/typescript/expo-plugin/support/ScreenRecorderLog.js create mode 100644 lib/typescript/expo-plugin/support/ScreenRecorderLog.ts create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift create mode 100644 lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift create mode 100644 lib/typescript/expo-plugin/support/iosConstants.d.ts create mode 100644 lib/typescript/expo-plugin/support/iosConstants.js create mode 100644 lib/typescript/expo-plugin/support/iosConstants.ts create mode 100644 lib/typescript/expo-plugin/support/updatePodfile.d.ts create mode 100644 lib/typescript/expo-plugin/support/updatePodfile.js create mode 100644 lib/typescript/expo-plugin/support/updatePodfile.ts create mode 100644 lib/typescript/expo-plugin/support/validatePluginProps.d.ts create mode 100644 lib/typescript/expo-plugin/support/validatePluginProps.js create mode 100644 lib/typescript/expo-plugin/support/validatePluginProps.ts create mode 100644 lib/typescript/expo-plugin/withScreenRecorder.d.ts create mode 100644 lib/typescript/expo-plugin/withScreenRecorder.js create mode 100644 lib/typescript/functions.d.ts create mode 100644 lib/typescript/functions.d.ts.map create mode 100644 lib/typescript/hooks/index.d.ts create mode 100644 lib/typescript/hooks/index.d.ts.map create mode 100644 lib/typescript/hooks/useCameraMicPermissions.d.ts create mode 100644 lib/typescript/hooks/useCameraMicPermissions.d.ts.map create mode 100644 lib/typescript/hooks/useGlobalRecording.d.ts create mode 100644 lib/typescript/hooks/useGlobalRecording.d.ts.map create mode 100644 lib/typescript/index.d.ts create mode 100644 lib/typescript/index.d.ts.map create mode 100644 lib/typescript/types.d.ts create mode 100644 lib/typescript/types.d.ts.map diff --git a/.gitignore b/.gitignore index 83ca870..4e0faa7 100644 --- a/.gitignore +++ b/.gitignore @@ -77,8 +77,6 @@ android/keystores/debug.keystore # Turborepo .turbo/ -# generated by bob -lib/ # React Native Codegen ios/generated diff --git a/lib/commonjs/NitroScreenRecorder.nitro.js b/lib/commonjs/NitroScreenRecorder.nitro.js new file mode 100644 index 0000000..eed3ec5 --- /dev/null +++ b/lib/commonjs/NitroScreenRecorder.nitro.js @@ -0,0 +1,6 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +//# sourceMappingURL=NitroScreenRecorder.nitro.js.map \ No newline at end of file diff --git a/lib/commonjs/NitroScreenRecorder.nitro.js.map b/lib/commonjs/NitroScreenRecorder.nitro.js.map new file mode 100644 index 0000000..b57cda8 --- /dev/null +++ b/lib/commonjs/NitroScreenRecorder.nitro.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroScreenRecorder.nitro.ts"],"mappings":"","ignoreList":[]} diff --git a/lib/commonjs/expo-plugin/@types.js b/lib/commonjs/expo-plugin/@types.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/commonjs/expo-plugin/@types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/commonjs/expo-plugin/android/withAndroidScreenRecording.js b/lib/commonjs/expo-plugin/android/withAndroidScreenRecording.js new file mode 100644 index 0000000..ec865bc --- /dev/null +++ b/lib/commonjs/expo-plugin/android/withAndroidScreenRecording.js @@ -0,0 +1,212 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withAndroidScreenRecording = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const withAndroidScreenRecording = (config) => { + // Add permissions and services to AndroidManifest.xml + config = (0, config_plugins_1.withAndroidManifest)(config, (mod) => { + var _a; + ScreenRecorderLog_1.ScreenRecorderLog.log('Adding screen recording permissions and services to AndroidManifest.xml'); + const androidManifest = mod.modResults; + if (!((_a = androidManifest.manifest.application) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Cannot find in AndroidManifest.xml'); + } + const application = androidManifest.manifest.application[0]; + if (!application.service) { + application.service = []; + } + // Add only the Global ScreenRecordingService + const serviceName = 'com.margelo.nitro.nitroscreenrecorder.ScreenRecordingService'; + const existingService = application.service.find((service) => { var _a; return ((_a = service.$) === null || _a === void 0 ? void 0 : _a['android:name']) === serviceName; }); + if (!existingService) { + application.service.push({ + $: { + 'android:name': serviceName, + 'android:enabled': 'true', + 'android:exported': 'false', + 'android:foregroundServiceType': 'mediaProjection', + }, + }); + ScreenRecorderLog_1.ScreenRecorderLog.log(`โœ… Added Global ScreenRecordingService to AndroidManifest.xml`); + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log(`โ„น๏ธ Global ScreenRecordingService already exists in AndroidManifest.xml`); + } + return mod; + }); + // Modify MainActivity to handle activity results (still needed for Global Recording) + config = (0, config_plugins_1.withMainActivity)(config, (mod) => { + ScreenRecorderLog_1.ScreenRecorderLog.log('Modifying MainActivity for screen recording activity results'); + const { modResults } = mod; + let mainActivityContent = modResults.contents; + const isKotlin = mainActivityContent.includes('class MainActivity') && + (mainActivityContent.includes('override fun') || + mainActivityContent.includes('kotlin')); + if (isKotlin) { + mainActivityContent = + addKotlinScreenRecordingSupport(mainActivityContent); + } + else { + mainActivityContent = addJavaScreenRecordingSupport(mainActivityContent); + } + modResults.contents = mainActivityContent; + return mod; + }); + return config; +}; +exports.withAndroidScreenRecording = withAndroidScreenRecording; +// This function remains unchanged as it's still needed for Global Recording +function addKotlinScreenRecordingSupport(content) { + // Required imports + const requiredImports = [ + 'import com.margelo.nitro.nitroscreenrecorder.NitroScreenRecorder', + 'import android.content.Intent', + 'import android.util.Log', + ]; + // Add imports if not present + requiredImports.forEach((importStatement) => { + if (!content.includes(importStatement)) { + const importRegex = /(import\s+.*\n)/g; + let lastImportMatch; + let match; + while ((match = importRegex.exec(content)) !== null) { + lastImportMatch = match; + } + if (lastImportMatch) { + const insertPosition = lastImportMatch.index + lastImportMatch[0].length; + content = + content.slice(0, insertPosition) + + importStatement + + '\n' + + content.slice(insertPosition); + } + } + }); + // Add onActivityResult method if not present + if (!content.includes('onActivityResult')) { + const classEndRegex = /(\s*)\}(\s*)$/; + const match = content.match(classEndRegex); + if (match && match.index !== undefined) { + const onActivityResultMethod = ` + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + Log.d("MainActivity", "onActivityResult: requestCode=$requestCode, resultCode=$resultCode") + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data) + } catch (e: Exception) { + Log.e("MainActivity", "Error handling activity result: \${e.message}") + e.printStackTrace() + } + } +`; + const insertPosition = match.index; + content = + content.slice(0, insertPosition) + + onActivityResultMethod + + content.slice(insertPosition); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added onActivityResult method to Kotlin MainActivity'); + } + } + else { + if (!content.includes('NitroScreenRecorder.handleActivityResult')) { + const onActivityResultRegex = /(override\s+fun\s+onActivityResult\s*\([^)]*\)\s*\{[^}]*)(super\.onActivityResult[^}]*)/; + const match = content.match(onActivityResultRegex); + if (match && match[1] && match[2]) { + const screenRecordingHandler = ` + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data) + } catch (e: Exception) { + Log.e("MainActivity", "Error handling activity result: \${e.message}") + e.printStackTrace() + }`; + content = content.replace(onActivityResultRegex, match[1] + match[2] + screenRecordingHandler); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added screen recording handler to existing onActivityResult method'); + } + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log('โ„น๏ธ Screen recording handler already exists in onActivityResult method'); + } + } + return content; +} +// This function remains unchanged as it's still needed for Global Recording +function addJavaScreenRecordingSupport(content) { + const requiredImports = [ + 'import android.content.Intent;', + 'import com.margelo.nitro.nitroscreenrecorder.NitroScreenRecorder;', + 'import android.util.Log;', + ]; + requiredImports.forEach((importStatement) => { + if (!content.includes(importStatement)) { + const importRegex = /(import\s+.*;\s*\n)/g; + let lastImportMatch; + let match; + while ((match = importRegex.exec(content)) !== null) { + lastImportMatch = match; + } + if (lastImportMatch) { + const insertPosition = lastImportMatch.index + lastImportMatch[0].length; + content = + content.slice(0, insertPosition) + + importStatement + + '\n' + + content.slice(insertPosition); + } + } + }); + if (!content.includes('onActivityResult')) { + const classEndRegex = /(\s*)\}(\s*)$/; + const match = content.match(classEndRegex); + if (match && match.index !== undefined) { + const onActivityResultMethod = ` + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.d("MainActivity", "onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode); + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data); + } catch (Exception e) { + Log.e("MainActivity", "Error handling activity result: " + e.getMessage()); + e.printStackTrace(); + } + } +`; + const insertPosition = match.index; + content = + content.slice(0, insertPosition) + + onActivityResultMethod + + content.slice(insertPosition); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added onActivityResult method to Java MainActivity'); + } + } + else { + if (!content.includes('NitroScreenRecorder.handleActivityResult')) { + const onActivityResultRegex = /(@Override\s+public\s+void\s+onActivityResult\s*\([^)]*\)\s*\{[^}]*)(super\.onActivityResult[^}]*)/; + const match = content.match(onActivityResultRegex); + if (match && match[1] && match[2]) { + const screenRecordingHandler = ` + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data); + } catch (Exception e) { + Log.e("MainActivity", "Error handling activity result: " + e.getMessage()); + e.printStackTrace(); + }`; + content = content.replace(onActivityResultRegex, match[1] + match[2] + screenRecordingHandler); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added screen recording handler to existing onActivityResult method'); + } + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log('โ„น๏ธ Screen recording handler already exists in onActivityResult method'); + } + } + return content; +} diff --git a/lib/commonjs/expo-plugin/eas/getEasManagedCredentials.js b/lib/commonjs/expo-plugin/eas/getEasManagedCredentials.js new file mode 100644 index 0000000..f427f48 --- /dev/null +++ b/lib/commonjs/expo-plugin/eas/getEasManagedCredentials.js @@ -0,0 +1,43 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = getEasManagedCredentialsConfigExtra; +const iosConstants_1 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +function getEasManagedCredentialsConfigExtra(config, props) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; + const providedExtensionBundleId = !!props.iosExtensionBundleIdentifier; + if (!providedExtensionBundleId && !((_a = config.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier)) { + (0, assert_1.default)((_b = config.ios) === null || _b === void 0 ? void 0 : _b.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + } + const extensionTargetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + return { + ...config.extra, + eas: { + ...(_c = config.extra) === null || _c === void 0 ? void 0 : _c.eas, + build: { + ...(_e = (_d = config.extra) === null || _d === void 0 ? void 0 : _d.eas) === null || _e === void 0 ? void 0 : _e.build, + experimental: { + ...(_h = (_g = (_f = config.extra) === null || _f === void 0 ? void 0 : _f.eas) === null || _g === void 0 ? void 0 : _g.build) === null || _h === void 0 ? void 0 : _h.experimental, + ios: { + ...(_m = (_l = (_k = (_j = config.extra) === null || _j === void 0 ? void 0 : _j.eas) === null || _k === void 0 ? void 0 : _k.build) === null || _l === void 0 ? void 0 : _l.experimental) === null || _m === void 0 ? void 0 : _m.ios, + appExtensions: [ + ...((_t = (_s = (_r = (_q = (_p = (_o = config.extra) === null || _o === void 0 ? void 0 : _o.eas) === null || _p === void 0 ? void 0 : _p.build) === null || _q === void 0 ? void 0 : _q.experimental) === null || _r === void 0 ? void 0 : _r.ios) === null || _s === void 0 ? void 0 : _s.appExtensions) !== null && _t !== void 0 ? _t : []), + { + targetName: extensionTargetName, + bundleIdentifier: (0, iosConstants_1.getBroadcastExtensionBundleIdentifier)((_u = config === null || config === void 0 ? void 0 : config.ios) === null || _u === void 0 ? void 0 : _u.bundleIdentifier, props), + entitlements: { + 'com.apple.security.application-groups': [ + (0, iosConstants_1.getAppGroup)((_v = config === null || config === void 0 ? void 0 : config.ios) === null || _v === void 0 ? void 0 : _v.bundleIdentifier, props), + ], + }, + }, + ], + }, + }, + }, + }, + }; +} diff --git a/lib/commonjs/expo-plugin/ios/withBroadcastExtension.js b/lib/commonjs/expo-plugin/ios/withBroadcastExtension.js new file mode 100644 index 0000000..ed15dec --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withBroadcastExtension.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtension = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +// Local helpers / subโ€‘mods โ–ถ๏ธ +const withMainAppAppGroupInfoPlist_1 = require("./withMainAppAppGroupInfoPlist"); +const withMainAppAppGroupEntitlement_1 = require("./withMainAppAppGroupEntitlement"); +const withBroadcastExtensionFiles_1 = require("./withBroadcastExtensionFiles"); +const withBroadcastExtensionXcodeProject_1 = require("./withBroadcastExtensionXcodeProject"); +const withBroadcastExtensionPodfile_1 = require("./withBroadcastExtensionPodfile"); +const withEasManagedCredentials_1 = require("./withEasManagedCredentials"); +const withMainAppEntitlementsFile_1 = require("./withMainAppEntitlementsFile"); +const withBroadcastExtension = (config, props) => { + return (0, config_plugins_1.withPlugins)(config, [ + /** Mainโ€‘app tweaks */ + [withMainAppAppGroupInfoPlist_1.withMainAppAppGroupInfoPlist, props], + [withMainAppEntitlementsFile_1.withMainAppEntitlementsFile, props], + [withMainAppAppGroupEntitlement_1.withMainAppAppGroupEntitlement, props], + /** Broadcast extension target */ + [withBroadcastExtensionFiles_1.withBroadcastExtensionFiles, props], + [withBroadcastExtensionXcodeProject_1.withBroadcastExtensionXcodeProject, props], + [withBroadcastExtensionPodfile_1.withBroadcastExtensionPodfile, props], + /** Extras for EAS build */ + [withEasManagedCredentials_1.withEasManagedCredentials, props], + ]); +}; +exports.withBroadcastExtension = withBroadcastExtension; diff --git a/lib/commonjs/expo-plugin/ios/withBroadcastExtensionFiles.js b/lib/commonjs/expo-plugin/ios/withBroadcastExtensionFiles.js new file mode 100644 index 0000000..6a2745b --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withBroadcastExtensionFiles.js @@ -0,0 +1,89 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionFiles = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const iosConstants_1 = require("../support/iosConstants"); +const FileManager_1 = require("../support/FileManager"); +const BEUpdateManager_1 = __importDefault(require("../support/BEUpdateManager")); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const SAMPLE_HANDLER_FILE = 'SampleHandler.swift'; +/** + * Copies the ReplayKit Broadcast Upload Extension templates into the iOS + * project and patches them so their App Group + bundle versions match the + * host app. Mirrors OneSignal's NSE flow for consistency. + */ +const withBroadcastExtensionFiles = (config, props) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + async (mod) => { + var _a, _b, _c, _d; + const iosPath = path.join(mod.modRequest.projectRoot, 'ios'); + const targetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + const sourceDir = path.join(__dirname, '..', 'support', 'broadcastExtensionFiles'); + fs.mkdirSync(`${iosPath}/${targetName}`, { + recursive: true, + }); + for (const extFile of iosConstants_1.BROADCAST_EXT_ALL_FILES) { + const targetFile = `${iosPath}/${targetName}/${extFile}`; + await FileManager_1.FileManager.copyFile(`${sourceDir}/${extFile}`, targetFile); + } + const sourceSamplePath = `${sourceDir}/${SAMPLE_HANDLER_FILE}`; + const targetSamplePath = `${iosPath}/${targetName}/${SAMPLE_HANDLER_FILE}`; + await FileManager_1.FileManager.copyFile(sourceSamplePath, targetSamplePath); + ScreenRecorderLog_1.ScreenRecorderLog.log(`Copied broadcast extension files to ${iosPath}/${targetName}`); + /* ------------------------------------------------------------ */ + /* 2๏ธโƒฃ Patch entitlements & Info.plist placeholders */ + /* ------------------------------------------------------------ */ + const updater = new BEUpdateManager_1.default(iosPath, props); + const mainAppBundleId = (_a = mod.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + if (!mainAppBundleId) { + throw new Error('Failed to find main app bundle id!'); + } + const groupIdentifier = (0, iosConstants_1.getAppGroup)(mainAppBundleId, props); + await updater.updateEntitlements(groupIdentifier); + await updater.updateInfoPlist((_c = (_b = mod.ios) === null || _b === void 0 ? void 0 : _b.buildNumber) !== null && _c !== void 0 ? _c : iosConstants_1.DEFAULT_BUNDLE_VERSION, groupIdentifier); + await updater.updateBundleShortVersion((_d = mod.version) !== null && _d !== void 0 ? _d : iosConstants_1.DEFAULT_BUNDLE_SHORT_VERSION); + ScreenRecorderLog_1.ScreenRecorderLog.log('Patched broadcast extension entitlements and Info.plist with app group and version values.'); + return mod; + }, + ]); +}; +exports.withBroadcastExtensionFiles = withBroadcastExtensionFiles; diff --git a/lib/commonjs/expo-plugin/ios/withBroadcastExtensionPodfile.js b/lib/commonjs/expo-plugin/ios/withBroadcastExtensionPodfile.js new file mode 100644 index 0000000..6b6e659 --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withBroadcastExtensionPodfile.js @@ -0,0 +1,21 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionPodfile = void 0; +const path_1 = __importDefault(require("path")); +const config_plugins_1 = require("@expo/config-plugins"); +const updatePodfile_1 = require("../support/updatePodfile"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const withBroadcastExtensionPodfile = (config, props) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + async (mod) => { + const iosRoot = path_1.default.join(mod.modRequest.projectRoot, 'ios'); + await (0, updatePodfile_1.updatePodfile)(iosRoot, props).catch(ScreenRecorderLog_1.ScreenRecorderLog.error); + return mod; + }, + ]); +}; +exports.withBroadcastExtensionPodfile = withBroadcastExtensionPodfile; diff --git a/lib/commonjs/expo-plugin/ios/withBroadcastExtensionXcodeProject.js b/lib/commonjs/expo-plugin/ios/withBroadcastExtensionXcodeProject.js new file mode 100644 index 0000000..d1561f2 --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withBroadcastExtensionXcodeProject.js @@ -0,0 +1,139 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionXcodeProject = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const assert_1 = __importDefault(require("assert")); +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Helper: pull DEVELOPMENT_TEAM from the main-app targetโ€™s build settings +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getMainAppDevelopmentTeam(pbx, l) { + var _a, _b; + const configs = pbx.pbxXCBuildConfigurationSection(); + for (const key in configs) { + const config = configs[key]; + const bs = config.buildSettings; + if (!bs || !bs.PRODUCT_NAME) + continue; + const productName = (_a = bs.PRODUCT_NAME) === null || _a === void 0 ? void 0 : _a.replace(/"/g, ''); + // Ignore other extensions/widgets + if (productName && + (productName.includes('Extension') || productName.includes('Widget'))) { + continue; + } + const developmentTeam = (_b = bs.DEVELOPMENT_TEAM) === null || _b === void 0 ? void 0 : _b.replace(/"/g, ''); + if (developmentTeam) { + l.log(`Found DEVELOPMENT_TEAM='${developmentTeam}' from main app configuration.`); + return developmentTeam; + } + } + l.error('No DEVELOPMENT_TEAM found in main app build settings. Developer will need to manually add Dev Team.'); + return null; +} +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Main Expo config-plugin +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const withBroadcastExtensionXcodeProject = (config, props) => { + return (0, config_plugins_1.withXcodeProject)(config, (newConfig) => { + var _a, _b, _c, _d; + const xcodeProject = newConfig.modResults; + const extensionTargetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + const appIdentifier = (_a = newConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + (0, assert_1.default)(appIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const bundleIdentifier = (0, iosConstants_1.getBroadcastExtensionBundleIdentifier)(appIdentifier, props); + /* ------------------------------------------------------------------ */ + /* 0. Resolve DEVELOPMENT_TEAM (props override > auto-detect > none) */ + /* ------------------------------------------------------------------ */ + const detectedDevTeam = getMainAppDevelopmentTeam(xcodeProject, ScreenRecorderLog_1.ScreenRecorderLog); + const devTeam = detectedDevTeam !== null && detectedDevTeam !== void 0 ? detectedDevTeam : undefined; + /* ------------------------------------------------------------------ */ + /* 1. Bail out early if target/group already exist */ + /* ------------------------------------------------------------------ */ + const existingTarget = xcodeProject.pbxTargetByName(extensionTargetName); + if (existingTarget) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${extensionTargetName} already exists in project. Skippingโ€ฆ`); + return newConfig; + } + const existingGroups = xcodeProject.hash.project.objects.PBXGroup; + const groupExists = Object.values(existingGroups).some((group) => group && group.name === extensionTargetName); + if (groupExists) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${extensionTargetName} group already exists in project. Skippingโ€ฆ`); + return newConfig; + } + /* ------------------------------------------------------------------ */ + /* 2. Create target, group & build phases (COMBINED APPROACH) */ + /* ------------------------------------------------------------------ */ + const pbx = xcodeProject; + // 2.1 Create PBXGroup for the extension (OneSignal style - single group creation) + const extGroup = pbx.addPbxGroup(iosConstants_1.BROADCAST_EXT_ALL_FILES, extensionTargetName, extensionTargetName); + // 2.2 Add the new PBXGroup to the top level group + const groups = pbx.hash.project.objects.PBXGroup; + Object.keys(groups).forEach(function (key) { + if (typeof groups[key] === 'object' && + groups[key].name === undefined && + groups[key].path === undefined) { + pbx.addToPbxGroup(extGroup.uuid, key); + } + }); + // 2.3 WORK AROUND for addTarget BUG (from OneSignal) + // Xcode projects don't contain these if there is only one target + const projObjects = pbx.hash.project.objects; + projObjects.PBXTargetDependency = projObjects.PBXTargetDependency || {}; + projObjects.PBXContainerItemProxy = projObjects.PBXContainerItemProxy || {}; + // 2.4 Create native target + const target = pbx.addTarget(extensionTargetName, 'app_extension', extensionTargetName); + // 2.5 Add build phases to the new target (OneSignal approach) + pbx.addBuildPhase(iosConstants_1.BROADCAST_EXT_SOURCE_FILES, // Add source files directly to the build phase + 'PBXSourcesBuildPhase', 'Sources', target.uuid); + pbx.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid); + pbx.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid); + // 2.6 Link ReplayKit + pbx.addFramework('ReplayKit.framework', { + target: target.uuid, + sourceTree: 'SDKROOT', + link: true, + }); + /* ------------------------------------------------------------------ */ + /* 3. Build-settings tweaks */ + /* ------------------------------------------------------------------ */ + const configurations = xcodeProject.pbxXCBuildConfigurationSection(); + for (const key in configurations) { + const cfg = configurations[key]; + const b = cfg.buildSettings; + if (!b) + continue; + if (b.PRODUCT_NAME === `"${extensionTargetName}"`) { + b.CLANG_ENABLE_MODULES = 'YES'; + b.INFOPLIST_FILE = `"${extensionTargetName}/BroadcastExtension-Info.plist"`; + b.CODE_SIGN_ENTITLEMENTS = `"${extensionTargetName}/BroadcastExtension.entitlements"`; + b.CODE_SIGN_STYLE = 'Automatic'; + b.CURRENT_PROJECT_VERSION = + (_c = (_b = newConfig.ios) === null || _b === void 0 ? void 0 : _b.buildNumber) !== null && _c !== void 0 ? _c : iosConstants_1.DEFAULT_BUNDLE_VERSION; + b.MARKETING_VERSION = (_d = newConfig.version) !== null && _d !== void 0 ? _d : iosConstants_1.DEFAULT_BUNDLE_SHORT_VERSION; + b.PRODUCT_BUNDLE_IDENTIFIER = `"${bundleIdentifier}"`; + b.SWIFT_VERSION = '5.0'; + b.SWIFT_EMIT_LOC_STRINGS = 'YES'; + b.SWIFT_OBJC_BRIDGING_HEADER = `"${extensionTargetName}/BroadcastExtension-Bridging-Header.h"`; + b.HEADER_SEARCH_PATHS = `"$(SRCROOT)/${extensionTargetName}"`; + b.TARGETED_DEVICE_FAMILY = iosConstants_1.TARGETED_DEVICE_FAMILY; + if (devTeam) + b.DEVELOPMENT_TEAM = devTeam; + } + } + /* ------------------------------------------------------------------ */ + /* 4. Apply DevelopmentTeam to both targets */ + /* ------------------------------------------------------------------ */ + if (devTeam) { + xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam); + const broadcastTarget = xcodeProject.pbxTargetByName(extensionTargetName); + xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam, broadcastTarget); + } + ScreenRecorderLog_1.ScreenRecorderLog.log(`Successfully created ${extensionTargetName} target with files`); + return newConfig; + }); +}; +exports.withBroadcastExtensionXcodeProject = withBroadcastExtensionXcodeProject; diff --git a/lib/commonjs/expo-plugin/ios/withEasManagedCredentials.js b/lib/commonjs/expo-plugin/ios/withEasManagedCredentials.js new file mode 100644 index 0000000..d89e837 --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withEasManagedCredentials.js @@ -0,0 +1,12 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withEasManagedCredentials = void 0; +const getEasManagedCredentials_1 = __importDefault(require("../eas/getEasManagedCredentials")); +const withEasManagedCredentials = (config, props) => { + config.extra = (0, getEasManagedCredentials_1.default)(config, props); + return config; +}; +exports.withEasManagedCredentials = withEasManagedCredentials; diff --git a/lib/commonjs/expo-plugin/ios/withMainAppAppGroupEntitlement.js b/lib/commonjs/expo-plugin/ios/withMainAppAppGroupEntitlement.js new file mode 100644 index 0000000..0824d0a --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withMainAppAppGroupEntitlement.js @@ -0,0 +1,32 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppAppGroupEntitlement = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +/** + * Add "App Group" permission + */ +const withMainAppAppGroupEntitlement = (config, props) => { + const APP_GROUP_KEY = 'com.apple.security.application-groups'; + return (0, config_plugins_1.withEntitlementsPlist)(config, (newConfig) => { + var _a, _b; + // Ensure we have an array, preserving any existing entries + if (!Array.isArray(newConfig.modResults[APP_GROUP_KEY])) { + newConfig.modResults[APP_GROUP_KEY] = []; + } + const modResultsArray = newConfig.modResults[APP_GROUP_KEY]; + (0, assert_1.default)((_a = newConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const entitlement = (0, iosConstants_1.getAppGroup)((_b = newConfig === null || newConfig === void 0 ? void 0 : newConfig.ios) === null || _b === void 0 ? void 0 : _b.bundleIdentifier, props); + // Check if our entitlement already exists + if (modResultsArray.includes(entitlement)) { + return newConfig; + } + modResultsArray.push(entitlement); + return newConfig; + }); +}; +exports.withMainAppAppGroupEntitlement = withMainAppAppGroupEntitlement; diff --git a/lib/commonjs/expo-plugin/ios/withMainAppAppGroupInfoPlist.js b/lib/commonjs/expo-plugin/ios/withMainAppAppGroupInfoPlist.js new file mode 100644 index 0000000..af3cf18 --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withMainAppAppGroupInfoPlist.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppAppGroupInfoPlist = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const iosConstants_2 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +const withMainAppAppGroupInfoPlist = (config, props) => { + return (0, config_plugins_1.withInfoPlist)(config, (modConfig) => { + var _a; + const appIdentifier = (_a = modConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + (0, assert_1.default)(appIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const appGroup = (0, iosConstants_1.getAppGroup)(appIdentifier, props); + const broadcastExtensionBundleId = (0, iosConstants_2.getBroadcastExtensionBundleIdentifier)(appIdentifier, props); + modConfig.modResults.BroadcastExtensionAppGroupIdentifier = appGroup; + modConfig.modResults.BroadcastExtensionBundleIdentifier = + broadcastExtensionBundleId; + return modConfig; + }); +}; +exports.withMainAppAppGroupInfoPlist = withMainAppAppGroupInfoPlist; diff --git a/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js b/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js new file mode 100644 index 0000000..e6da01b --- /dev/null +++ b/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js @@ -0,0 +1,98 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppEntitlementsFile = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +/** + * Add the main app's entitlements file to the Xcode project navigator + * This ensures the .entitlements file is visible in Xcode's file tree + */ +const withMainAppEntitlementsFile = (config) => { + return (0, config_plugins_1.withXcodeProject)(config, (newConfig) => { + const xcodeProject = newConfig.modResults; + const projectName = newConfig.name; + const entitlementsFileName = `${projectName}.entitlements`; + const entitlementsPath = `${projectName}/${entitlementsFileName}`; + // Check if the entitlements file is already added to the project + const files = xcodeProject.hash.project.objects.PBXFileReference; + const entitlementsFileExists = Object.values(files).some((file) => file && file.path === `"${entitlementsFileName}"`); + if (entitlementsFileExists) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${entitlementsFileName} already exists in project. Skipping...`); + return newConfig; + } + // Find the main app group (try multiple approaches) + const groups = xcodeProject.hash.project.objects.PBXGroup; + let mainAppGroupKey = null; + // Debug: log all group names to understand the structure + ScreenRecorderLog_1.ScreenRecorderLog.log('Available groups:'); + for (const key in groups) { + const group = groups[key]; + if (group && group.name) { + ScreenRecorderLog_1.ScreenRecorderLog.log(` - ${group.name} (key: ${key})`); + } + } + // Try different variations of the project name + const searchNames = [ + `"${projectName}"`, // Quoted version + projectName, // Unquoted version + `"${projectName}/"`, // With trailing slash + `${projectName}/`, // Unquoted with trailing slash + ]; + for (const searchName of searchNames) { + for (const key in groups) { + const group = groups[key]; + if (group && group.name === searchName) { + mainAppGroupKey = key; + ScreenRecorderLog_1.ScreenRecorderLog.log(`Found main app group with name: ${searchName}`); + break; + } + } + if (mainAppGroupKey) + break; + } + // If still not found, try to find the group that contains AppDelegate or main source files + if (!mainAppGroupKey) { + ScreenRecorderLog_1.ScreenRecorderLog.log('Trying to find main app group by looking for AppDelegate...'); + for (const key in groups) { + const group = groups[key]; + if (group && group.children) { + // Check if this group contains typical main app files + const hasMainAppFiles = group.children.some((childKey) => { + var _a, _b, _c; + const file = files[childKey]; + return (file && + (((_a = file.path) === null || _a === void 0 ? void 0 : _a.includes('AppDelegate')) || + ((_b = file.path) === null || _b === void 0 ? void 0 : _b.includes('Info.plist')) || + ((_c = file.name) === null || _c === void 0 ? void 0 : _c.includes('AppDelegate')))); + }); + if (hasMainAppFiles) { + mainAppGroupKey = key; + ScreenRecorderLog_1.ScreenRecorderLog.log(`Found main app group by AppDelegate: ${group.name || 'unnamed'}`); + break; + } + } + } + } + if (!mainAppGroupKey) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Could not find main app group for ${projectName}. Available groups logged above.`); + return newConfig; + } + // Add the entitlements file to the project + try { + // Create the file reference + const fileRef = xcodeProject.addFile(entitlementsPath, mainAppGroupKey, { + lastKnownFileType: 'text.plist.entitlements', + defaultEncoding: 4, + target: undefined, + }); + if (fileRef) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Successfully added ${entitlementsFileName} to Xcode project navigator`); + } + } + catch (error) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Error adding entitlements file to project: ${error}`); + } + return newConfig; + }); +}; +exports.withMainAppEntitlementsFile = withMainAppEntitlementsFile; diff --git a/lib/commonjs/expo-plugin/support/BEUpdateManager.js b/lib/commonjs/expo-plugin/support/BEUpdateManager.js new file mode 100644 index 0000000..1401c50 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/BEUpdateManager.js @@ -0,0 +1,47 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const FileManager_1 = require("./FileManager"); +const iosConstants_1 = require("./iosConstants"); +// project `ios/${BROADCAST_EXT_TARGET_NAME}` directory +const entitlementsFileName = `BroadcastExtension.entitlements`; +const plistFileName = `BroadcastExtension-Info.plist`; +class BEUpdaterManager { + constructor(iosPath, props) { + this.extensionPath = ''; + const targetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + this.extensionPath = `${iosPath}/${targetName}`; + } + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + async updateEntitlements(groupIdentifier) { + const entitlementsFilePath = `${this.extensionPath}/${entitlementsFileName}`; + let entitlementsFile = await FileManager_1.FileManager.readFile(entitlementsFilePath); + entitlementsFile = entitlementsFile.replace(iosConstants_1.GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + await FileManager_1.FileManager.writeFile(entitlementsFilePath, entitlementsFile); + } + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + async updateInfoPlist(version, groupIdentifier) { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager_1.FileManager.readFile(plistFilePath); + plistFile = plistFile + .replace(iosConstants_1.BUNDLE_VERSION_TEMPLATE_REGEX, version) + .replace(iosConstants_1.GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + await FileManager_1.FileManager.writeFile(plistFilePath, plistFile); + } + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + async updateBundleShortVersion(version) { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager_1.FileManager.readFile(plistFilePath); + plistFile = plistFile.replace(iosConstants_1.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, version); + await FileManager_1.FileManager.writeFile(plistFilePath, plistFile); + } +} +exports.default = BEUpdaterManager; diff --git a/lib/commonjs/expo-plugin/support/BEUpdateManager.ts b/lib/commonjs/expo-plugin/support/BEUpdateManager.ts new file mode 100644 index 0000000..6ee0411 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/BEUpdateManager.ts @@ -0,0 +1,68 @@ +import { type ConfigProps } from '../@types'; +import { FileManager } from './FileManager'; +import { + BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, + BUNDLE_VERSION_TEMPLATE_REGEX, + getBroadcastExtensionTargetName, + GROUP_IDENTIFIER_TEMPLATE_REGEX, +} from './iosConstants'; + +// project `ios/${BROADCAST_EXT_TARGET_NAME}` directory +const entitlementsFileName = `BroadcastExtension.entitlements`; +const plistFileName = `BroadcastExtension-Info.plist`; + +export default class BEUpdaterManager { + private extensionPath = ''; + + constructor(iosPath: string, props: ConfigProps) { + const targetName = getBroadcastExtensionTargetName(props); + this.extensionPath = `${iosPath}/${targetName}`; + } + + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + async updateEntitlements(groupIdentifier: string): Promise { + const entitlementsFilePath = `${this.extensionPath}/${entitlementsFileName}`; + let entitlementsFile = await FileManager.readFile(entitlementsFilePath); + + entitlementsFile = entitlementsFile.replace( + GROUP_IDENTIFIER_TEMPLATE_REGEX, + groupIdentifier + ); + + await FileManager.writeFile(entitlementsFilePath, entitlementsFile); + } + + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + async updateInfoPlist( + version: string, + groupIdentifier: string + ): Promise { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager.readFile(plistFilePath); + + plistFile = plistFile + .replace(BUNDLE_VERSION_TEMPLATE_REGEX, version) + .replace(GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + + await FileManager.writeFile(plistFilePath, plistFile); + } + + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + async updateBundleShortVersion(version: string): Promise { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager.readFile(plistFilePath); + + plistFile = plistFile.replace(BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, version); + + await FileManager.writeFile(plistFilePath, plistFile); + } +} diff --git a/lib/commonjs/expo-plugin/support/FileManager.js b/lib/commonjs/expo-plugin/support/FileManager.js new file mode 100644 index 0000000..9581566 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/FileManager.js @@ -0,0 +1,75 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileManager = void 0; +const fs = __importStar(require("fs")); +const ScreenRecorderLog_1 = require("./ScreenRecorderLog"); +/** + * FileManager contains static *awaitable* file-system functions + */ +class FileManager { + static async readFile(path) { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err || !data) { + ScreenRecorderLog_1.ScreenRecorderLog.error("Couldn't read file:" + path); + reject(err); + return; + } + resolve(data); + }); + }); + } + static async writeFile(path, contents) { + return new Promise((resolve, reject) => { + fs.writeFile(path, contents, 'utf8', (err) => { + if (err) { + ScreenRecorderLog_1.ScreenRecorderLog.error("Couldn't write file:" + path); + reject(err); + return; + } + resolve(); + }); + }); + } + static async copyFile(path1, path2) { + const fileContents = await FileManager.readFile(path1); + await FileManager.writeFile(path2, fileContents); + } + static dirExists(path) { + return fs.existsSync(path); + } +} +exports.FileManager = FileManager; diff --git a/lib/commonjs/expo-plugin/support/FileManager.ts b/lib/commonjs/expo-plugin/support/FileManager.ts new file mode 100644 index 0000000..1d61d78 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/FileManager.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import { ScreenRecorderLog } from './ScreenRecorderLog'; + +/** + * FileManager contains static *awaitable* file-system functions + */ +export class FileManager { + static async readFile(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err || !data) { + ScreenRecorderLog.error("Couldn't read file:" + path); + reject(err); + return; + } + resolve(data); + }); + }); + } + + static async writeFile(path: string, contents: string): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(path, contents, 'utf8', (err) => { + if (err) { + ScreenRecorderLog.error("Couldn't write file:" + path); + reject(err); + return; + } + resolve(); + }); + }); + } + + static async copyFile(path1: string, path2: string): Promise { + const fileContents = await FileManager.readFile(path1); + await FileManager.writeFile(path2, fileContents); + } + + static dirExists(path: string): boolean { + return fs.existsSync(path); + } +} diff --git a/lib/commonjs/expo-plugin/support/ScreenRecorderLog.js b/lib/commonjs/expo-plugin/support/ScreenRecorderLog.js new file mode 100644 index 0000000..ae6080f --- /dev/null +++ b/lib/commonjs/expo-plugin/support/ScreenRecorderLog.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ScreenRecorderLog = void 0; +class ScreenRecorderLog { + static log(message, ...optional) { + const green = '\x1b[32m'; + const reset = '\x1b[0m'; + console.log(`${green}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } + static error(message, ...optional) { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + console.error(`${red}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } +} +exports.ScreenRecorderLog = ScreenRecorderLog; +ScreenRecorderLog.PLUGIN = 'react-native-nitro-screen-recorder'; diff --git a/lib/commonjs/expo-plugin/support/ScreenRecorderLog.ts b/lib/commonjs/expo-plugin/support/ScreenRecorderLog.ts new file mode 100644 index 0000000..edc5325 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/ScreenRecorderLog.ts @@ -0,0 +1,15 @@ +export class ScreenRecorderLog { + private static readonly PLUGIN = 'react-native-nitro-screen-recorder'; + + static log(message: string, ...optional: any[]) { + const green = '\x1b[32m'; + const reset = '\x1b[0m'; + console.log(`${green}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } + + static error(message: string, ...optional: any[]) { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + console.error(`${red}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } +} diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h new file mode 100644 index 0000000..187805c --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h @@ -0,0 +1 @@ +#import "BroadcastHelper.h" \ No newline at end of file diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist new file mode 100644 index 0000000..3bff812 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleName + $(PRODUCT_NAME) + + CFBundleDisplayName + $(PRODUCT_NAME) + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + + CFBundleExecutable + $(EXECUTABLE_NAME) + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + CFBundleShortVersionString + $(MARKETING_VERSION) + + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SampleHandler + + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + + BroadcastExtensionAppGroupIdentifier + {{GROUP_IDENTIFIER}} + + \ No newline at end of file diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy new file mode 100644 index 0000000..73c00be --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy @@ -0,0 +1,26 @@ + + + + + NSPrivacy + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryScreenCapture + NSPrivacyAccessedAPITypeReason + User-initiated screen recording via ReplayKit + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryAudio + NSPrivacyAccessedAPITypeReason + User-initiated microphone capture in screen recording + + + NSPrivacyCollectedDataTypes + + + + diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements new file mode 100644 index 0000000..470ed66 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + {{GROUP_IDENTIFIER}} + + + \ No newline at end of file diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h new file mode 100644 index 0000000..9830154 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h @@ -0,0 +1,7 @@ +#import + +/// Finishes a broadcast without triggering the โ€œerrorโ€ alert. +/// (RPBroadcastSampleHandlerโ€™s parameter is formally non-null, so we suppress +/// the compiler warning.) +/// Refer to https://mehmetbaykar.com/posts/how-to-gracefully-stop-a-broadcast-upload-extension/ +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull handler); \ No newline at end of file diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m new file mode 100644 index 0000000..0a67791 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m @@ -0,0 +1,8 @@ +#import "BroadcastHelper.h" + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull handler) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [handler finishBroadcastWithError:nil]; // โ† the magic line โœจ +#pragma clang diagnostic pop +} \ No newline at end of file diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift new file mode 100644 index 0000000..44d82b1 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -0,0 +1,434 @@ +// MARK: Broadcast Writer + +// Copied from the repo: +// https://github.com/romiroma/BroadcastWriter + +import AVFoundation +import CoreGraphics +import Foundation +import ReplayKit + +extension AVAssetWriter.Status { + var description: String { + switch self { + case .cancelled: return "cancelled" + case .completed: return "completed" + case .failed: return "failed" + case .unknown: return "unknown" + case .writing: return "writing" + @unknown default: return "@unknown default" + } + } +} + +extension CGFloat { + var nsNumber: NSNumber { + return .init(value: native) + } +} + +extension Int { + var nsNumber: NSNumber { + return .init(value: self) + } +} + +enum Error: Swift.Error { + case wrongAssetWriterStatus(AVAssetWriter.Status) + case selfDeallocated +} + +public final class BroadcastWriter { + + private var assetWriterSessionStarted: Bool = false + private var audioAssetWriterSessionStarted: Bool = false + private let assetWriterQueue: DispatchQueue + private let assetWriter: AVAssetWriter + + // Separate audio writer + private var separateAudioWriter: AVAssetWriter? + private let separateAudioFile: Bool + private let audioOutputURL: URL? + + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in + let videoWidth = screenSize.width * screenScale + let videoHeight = screenSize.height * screenScale + + // Ensure encoder-friendly even dimensions + let w = (Int(videoWidth) / 2) * 2 + let h = (Int(videoHeight) / 2) * 2 + + // Decide codec: prefer HEVC when available + let hevcSupported: Bool = { + if #available(iOS 11.0, *) { + return self.assetWriter.canApply( + outputSettings: [AVVideoCodecKey: AVVideoCodecType.hevc], + forMediaType: .video + ) + } + return false + }() + + let codec: AVVideoCodecType = hevcSupported ? .hevc : .h264 + + var compressionProperties: [String: Any] = [ + AVVideoExpectedSourceFrameRateKey: 60.nsNumber + ] + if hevcSupported { + // Works broadly; adjust if you need different profiles + compressionProperties[AVVideoProfileLevelKey] = "HEVC_Main_AutoLevel" + } else { + compressionProperties[AVVideoProfileLevelKey] = AVVideoProfileLevelH264HighAutoLevel + } + + let videoSettings: [String: Any] = [ + AVVideoCodecKey: codec, + AVVideoWidthKey: w.nsNumber, + AVVideoHeightKey: h.nsNumber, + AVVideoCompressionPropertiesKey: compressionProperties, + ] + + let input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + input.expectsMediaDataInRealTime = true + return input + }() + + private var audioSampleRate: Double { + AVAudioSession.sharedInstance().sampleRate + } + private lazy var audioInput: AVAssetWriterInput = { + + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + private lazy var microphoneInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Separate audio file input (for microphone audio only) + private lazy var separateAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + private lazy var inputs: [AVAssetWriterInput] = [ + videoInput, + audioInput, + microphoneInput, + ] + + private let screenSize: CGSize + private let screenScale: CGFloat + + public init( + outputURL url: URL, + audioOutputURL: URL? = nil, + assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), + screenSize: CGSize, + screenScale: CGFloat, + separateAudioFile: Bool = false + ) throws { + assetWriterQueue = queue + assetWriter = try .init(url: url, fileType: .mp4) + assetWriter.shouldOptimizeForNetworkUse = true + + self.screenSize = screenSize + self.screenScale = screenScale + self.separateAudioFile = separateAudioFile + self.audioOutputURL = audioOutputURL + + // Initialize separate audio writer if needed + if separateAudioFile, let audioURL = audioOutputURL { + separateAudioWriter = try .init(url: audioURL, fileType: .m4a) + separateAudioWriter?.shouldOptimizeForNetworkUse = true + } + } + + public func start() throws { + try assetWriterQueue.sync { + let status = assetWriter.status + guard status == .unknown else { + throw Error.wrongAssetWriterStatus(status) + } + try assetWriter.error.map { + throw $0 + } + inputs + .lazy + .filter(assetWriter.canAdd(_:)) + .forEach(assetWriter.add(_:)) + try assetWriter.error.map { + throw $0 + } + assetWriter.startWriting() + try assetWriter.error.map { + throw $0 + } + + // Start separate audio writer if enabled + if separateAudioFile, let audioWriter = separateAudioWriter { + let audioStatus = audioWriter.status + guard audioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(audioStatus) + } + try audioWriter.error.map { throw $0 } + if audioWriter.canAdd(separateAudioInput) { + audioWriter.add(separateAudioInput) + } + try audioWriter.error.map { throw $0 } + audioWriter.startWriting() + try audioWriter.error.map { throw $0 } + } + } + } + + public func processSampleBuffer( + _ sampleBuffer: CMSampleBuffer, + with sampleBufferType: RPSampleBufferType + ) throws -> Bool { + + guard sampleBuffer.isValid, + CMSampleBufferDataIsReady(sampleBuffer) + else { + debugPrint( + "sampleBuffer.isValid", sampleBuffer.isValid, + "CMSampleBufferDataIsReady(sampleBuffer)", CMSampleBufferDataIsReady(sampleBuffer) + ) + return false + } + + let isWriting = assetWriterQueue.sync { + assetWriter.status == .writing + } + + guard isWriting else { + debugPrint( + "assetWriter.status", + assetWriter.status.description, + "assetWriter.error:", + assetWriter.error ?? "no error" + ) + return false + } + + assetWriterQueue.sync { + startSessionIfNeeded(sampleBuffer: sampleBuffer) + } + + let capture: (CMSampleBuffer) -> Bool + switch sampleBufferType { + case .video: + capture = captureVideoOutput + case .audioApp: + capture = captureAudioOutput + case .audioMic: + capture = captureMicrophoneOutput + // Also write to separate audio file if enabled + if separateAudioFile { + assetWriterQueue.sync { + _ = captureSeparateAudioOutput(sampleBuffer) + } + } + @unknown default: + debugPrint(#file, "Unknown type of sample buffer, \(sampleBufferType)") + capture = { _ in false } + } + + return assetWriterQueue.sync { + capture(sampleBuffer) + } + } + + public func pause() { + // TODO: Pause + } + + public func resume() { + // TODO: Resume + } + + /// Result containing both video and optional audio URLs + public struct FinishResult { + public let videoURL: URL + public let audioURL: URL? + } + + public func finish() throws -> URL { + let result = try finishWithAudio() + return result.videoURL + } + + public func finishWithAudio() throws -> FinishResult { + return try assetWriterQueue.sync { + let group: DispatchGroup = .init() + + inputs + .lazy + .filter { $0.isReadyForMoreMediaData } + .forEach { $0.markAsFinished() } + + let status = assetWriter.status + guard status == .writing else { + throw Error.wrongAssetWriterStatus(status) + } + group.enter() + + var error: Swift.Error? + assetWriter.finishWriting { [weak self] in + + defer { + group.leave() + } + + guard let self = self else { + error = Error.selfDeallocated + return + } + + if let e = self.assetWriter.error { + error = e + return + } + + let status = self.assetWriter.status + guard status == .completed else { + error = Error.wrongAssetWriterStatus(status) + return + } + } + group.wait() + try error.map { throw $0 } + + // Finish separate audio writer if enabled + var audioURL: URL? = nil + if separateAudioFile, let audioWriter = separateAudioWriter { + if separateAudioInput.isReadyForMoreMediaData { + separateAudioInput.markAsFinished() + } + + if audioWriter.status == .writing { + let audioGroup = DispatchGroup() + audioGroup.enter() + + var audioError: Swift.Error? + audioWriter.finishWriting { + defer { audioGroup.leave() } + if let e = audioWriter.error { + audioError = e + return + } + if audioWriter.status != .completed { + audioError = Error.wrongAssetWriterStatus(audioWriter.status) + } + } + audioGroup.wait() + + if audioError == nil { + audioURL = audioWriter.outputURL + } + } + } + + return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + } + } +} + +extension BroadcastWriter { + + fileprivate func startSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !assetWriterSessionStarted else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + assetWriter.startSession(atSourceTime: sourceTime) + assetWriterSessionStarted = true + } + + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + audioWriter.startSession(atSourceTime: sourceTime) + audioAssetWriterSessionStarted = true + } + + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard videoInput.isReadyForMoreMediaData else { + debugPrint("videoInput is not ready") + return false + } + return videoInput.append(sampleBuffer) + } + + fileprivate func captureAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard audioInput.isReadyForMoreMediaData else { + debugPrint("audioInput is not ready") + return false + } + return audioInput.append(sampleBuffer) + } + + fileprivate func captureMicrophoneOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + + guard microphoneInput.isReadyForMoreMediaData else { + debugPrint("microphoneInput is not ready") + return false + } + return microphoneInput.append(sampleBuffer) + } + + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let audioWriter = separateAudioWriter else { + return false + } + + // Check if audio writer is still writing + guard audioWriter.status == .writing else { + debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") + return false + } + + // Start session if needed + startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard separateAudioInput.isReadyForMoreMediaData else { + debugPrint("separateAudioInput is not ready") + return false + } + return separateAudioInput.append(sampleBuffer) + } +} diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift new file mode 100644 index 0000000..f8feae7 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -0,0 +1,244 @@ +import AVFoundation +import ReplayKit +import UserNotifications +import Darwin + +@_silgen_name("finishBroadcastGracefully") +func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) + +/* + Handles the main processing of the global broadcast. + The app-group identifier is fetched from the extension's Info.plist + ("BroadcastExtensionAppGroupIdentifier" key) so you don't have to hard-code it here. + */ +final class SampleHandler: RPBroadcastSampleHandler { + + // MARK: โ€“ Properties + + private func appGroupIDFromPlist() -> String? { + guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + !value.isEmpty + else { + return nil + } + return value + } + + // Store both the CFString and CFNotificationName versions + private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString + private static let stopNotificationName = CFNotificationName(stopNotificationString) + + private lazy var hostAppGroupIdentifier: String? = { + return appGroupIDFromPlist() + }() + + private var writer: BroadcastWriter? + private let fileManager: FileManager = .default + private let nodeURL: URL + private let audioNodeURL: URL + private var sawMicBuffers = false + private var separateAudioFile: Bool = false + + // MARK: โ€“ Init + override init() { + let uuid = UUID().uuidString + nodeURL = fileManager.temporaryDirectory + .appendingPathComponent(uuid) + .appendingPathExtension(for: .mpeg4Movie) + + audioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_audio") + .appendingPathExtension("m4a") + + fileManager.removeFileIfExists(url: nodeURL) + fileManager.removeFileIfExists(url: audioNodeURL) + super.init() + } + + deinit { + CFNotificationCenterRemoveObserver( + CFNotificationCenterGetDarwinNotifyCenter(), + UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + SampleHandler.stopNotificationName, + nil + ) + } + + private func startListeningForStopSignal() { + let center = CFNotificationCenterGetDarwinNotifyCenter() + + CFNotificationCenterAddObserver( + center, + UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + { _, observer, name, _, _ in + guard + let observer, + let name, + name == SampleHandler.stopNotificationName + else { return } + + let me = Unmanaged + .fromOpaque(observer) + .takeUnretainedValue() + me.stopBroadcastGracefully() + }, + SampleHandler.stopNotificationString, + nil, + .deliverImmediately + ) + } + + // MARK: โ€“ Broadcast lifecycle + override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { + startListeningForStopSignal() + + guard let groupID = hostAppGroupIdentifier else { + finishBroadcastWithError( + NSError( + domain: "SampleHandler", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] + ) + ) + return + } + + // Check if separate audio file is requested + if let userDefaults = UserDefaults(suiteName: groupID) { + separateAudioFile = userDefaults.bool(forKey: "SeparateAudioFileEnabled") + } + + // Clean up old recordings + cleanupOldRecordings(in: groupID) + + // Start recording + let screen: UIScreen = .main + do { + writer = try .init( + outputURL: nodeURL, + audioOutputURL: separateAudioFile ? audioNodeURL : nil, + screenSize: screen.bounds.size, + screenScale: screen.scale, + separateAudioFile: separateAudioFile + ) + try writer?.start() + } catch { + finishBroadcastWithError(error) + } + } + + private func cleanupOldRecordings(in groupID: String) { + guard let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) + else { return } + + do { + let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) + for url in items where url.pathExtension.lowercased() == "mp4" { + try? fileManager.removeItem(at: url) + } + } catch { + // Non-critical error, continue with broadcast + } + } + + override func processSampleBuffer( + _ sampleBuffer: CMSampleBuffer, + with sampleBufferType: RPSampleBufferType + ) { + guard let writer else { return } + + if sampleBufferType == .audioMic { + sawMicBuffers = true + } + + do { + _ = try writer.processSampleBuffer(sampleBuffer, with: sampleBufferType) + } catch { + finishBroadcastWithError(error) + } + } + + override func broadcastPaused() { + writer?.pause() + } + + override func broadcastResumed() { + writer?.resume() + } + + private func stopBroadcastGracefully() { + finishBroadcastGracefully(self) + } + + override func broadcastFinished() { + guard let writer else { return } + + // Finish writing - use finishWithAudio to get both video and audio URLs + let result: BroadcastWriter.FinishResult + do { + result = try writer.finishWithAudio() + } catch { + // Writer failed, but we can't call finishBroadcastWithError here + // as we're already in the finish process + return + } + + guard let groupID = hostAppGroupIdentifier else { return } + + // Get container directory + guard let containerURL = fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) + else { return } + + // Create directory if needed + do { + try fileManager.createDirectory(at: containerURL, withIntermediateDirectories: true) + } catch { + return + } + + // Move video file to shared container + let videoDestination = containerURL.appendingPathComponent(result.videoURL.lastPathComponent) + do { + try fileManager.moveItem(at: result.videoURL, to: videoDestination) + } catch { + // File move failed, but we can't error out at this point + return + } + + // Move audio file to shared container if it exists + if let audioURL = result.audioURL { + let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) + do { + try fileManager.moveItem(at: audioURL, to: audioDestination) + // Store audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") + } catch { + // Audio file move failed, but video is already saved + debugPrint("Failed to move audio file: \(error)") + } + } else { + // Clear audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAudioFileName") + } + + // Persist microphone state and audio file state + UserDefaults(suiteName: groupID)? + .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") + UserDefaults(suiteName: groupID)? + .set(separateAudioFile, forKey: "LastBroadcastHadSeparateAudio") + } +} + +// MARK: โ€“ Helpers +extension FileManager { + fileprivate func removeFileIfExists(url: URL) { + guard fileExists(atPath: url.path) else { return } + try? removeItem(at: url) + } +} \ No newline at end of file diff --git a/lib/commonjs/expo-plugin/support/iosConstants.js b/lib/commonjs/expo-plugin/support/iosConstants.js new file mode 100644 index 0000000..7893815 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/iosConstants.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAppGroup = exports.BROADCAST_EXT_ALL_FILES = exports.BROADCAST_EXT_CONFIG_FILES = exports.BROADCAST_EXT_SOURCE_FILES = exports.DEFAULT_BUNDLE_SHORT_VERSION = exports.DEFAULT_BUNDLE_VERSION = exports.SCHEME_TEMPLATE_REGEX = exports.BUNDLE_VERSION_TEMPLATE_REGEX = exports.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = exports.GROUP_IDENTIFIER_TEMPLATE_REGEX = exports.getBroadcastExtensionPodfileSnippet = exports.getBroadcastExtensionTargetName = exports.TARGETED_DEVICE_FAMILY = exports.IPHONEOS_DEPLOYMENT_TARGET = void 0; +exports.getBroadcastExtensionBundleIdentifier = getBroadcastExtensionBundleIdentifier; +exports.IPHONEOS_DEPLOYMENT_TARGET = '11.0'; +exports.TARGETED_DEVICE_FAMILY = `"1,2"`; +const getBroadcastExtensionTargetName = (props) => { + if (props.iosBroadcastExtensionTargetName) + return props.iosBroadcastExtensionTargetName; + return `BroadcastExtension`; +}; +exports.getBroadcastExtensionTargetName = getBroadcastExtensionTargetName; +// Podfile configuration for ReplayKit (if needed for dependencies) +const getBroadcastExtensionPodfileSnippet = (props) => { + const targetName = (0, exports.getBroadcastExtensionTargetName)(props); + return ` + target '${targetName}' do + # ReplayKit is a system framework, no pods needed typically + # Add any specific pods for broadcast extension here if needed + end`; +}; +exports.getBroadcastExtensionPodfileSnippet = getBroadcastExtensionPodfileSnippet; +// Template replacement patterns +exports.GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; +exports.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = /{{BUNDLE_SHORT_VERSION}}/gm; +exports.BUNDLE_VERSION_TEMPLATE_REGEX = /{{BUNDLE_VERSION}}/gm; +exports.SCHEME_TEMPLATE_REGEX = /{{SCHEME}}/gm; +exports.DEFAULT_BUNDLE_VERSION = '1'; +exports.DEFAULT_BUNDLE_SHORT_VERSION = '1.0'; +// Broadcast Extension specific constants +exports.BROADCAST_EXT_SOURCE_FILES = [ + 'SampleHandler.swift', + 'BroadcastWriter.swift', + 'BroadcastHelper.m', +]; +exports.BROADCAST_EXT_CONFIG_FILES = [ + `BroadcastExtension-Info.plist`, + `BroadcastExtension.entitlements`, + 'BroadcastExtension-PrivacyInfo.xcprivacy', + 'BroadcastHelper.h', + 'BroadcastExtension-Bridging-Header.h', +]; +// All extension files combined +exports.BROADCAST_EXT_ALL_FILES = [ + ...exports.BROADCAST_EXT_SOURCE_FILES, + ...exports.BROADCAST_EXT_CONFIG_FILES, +]; +const getAppGroup = (mainAppBundleId, props) => { + if (props.iosAppGroupIdentifier) + return props.iosAppGroupIdentifier; + return `group.${mainAppBundleId}.screen-recorder`; +}; +exports.getAppGroup = getAppGroup; +// Helper function to get broadcast extension bundle identifier +function getBroadcastExtensionBundleIdentifier(mainAppBundleId, props) { + if (props.iosExtensionBundleIdentifier) + return props.iosExtensionBundleIdentifier; + const targetName = (0, exports.getBroadcastExtensionTargetName)(props); + return `${mainAppBundleId}.${targetName}`; +} diff --git a/lib/commonjs/expo-plugin/support/iosConstants.ts b/lib/commonjs/expo-plugin/support/iosConstants.ts new file mode 100644 index 0000000..2f102d1 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/iosConstants.ts @@ -0,0 +1,66 @@ +import type { ConfigProps } from '../@types'; + +export const IPHONEOS_DEPLOYMENT_TARGET = '11.0'; +export const TARGETED_DEVICE_FAMILY = `"1,2"`; + +export const getBroadcastExtensionTargetName = (props: ConfigProps) => { + if (props.iosBroadcastExtensionTargetName) + return props.iosBroadcastExtensionTargetName; + return `BroadcastExtension`; +}; + +// Podfile configuration for ReplayKit (if needed for dependencies) +export const getBroadcastExtensionPodfileSnippet = (props: ConfigProps) => { + const targetName = getBroadcastExtensionTargetName(props); + return ` + target '${targetName}' do + # ReplayKit is a system framework, no pods needed typically + # Add any specific pods for broadcast extension here if needed + end`; +}; + +// Template replacement patterns +export const GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; +export const BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = /{{BUNDLE_SHORT_VERSION}}/gm; +export const BUNDLE_VERSION_TEMPLATE_REGEX = /{{BUNDLE_VERSION}}/gm; +export const SCHEME_TEMPLATE_REGEX = /{{SCHEME}}/gm; + +export const DEFAULT_BUNDLE_VERSION = '1'; +export const DEFAULT_BUNDLE_SHORT_VERSION = '1.0'; + +// Broadcast Extension specific constants +export const BROADCAST_EXT_SOURCE_FILES = [ + 'SampleHandler.swift', + 'BroadcastWriter.swift', + 'BroadcastHelper.m', +]; + +export const BROADCAST_EXT_CONFIG_FILES = [ + `BroadcastExtension-Info.plist`, + `BroadcastExtension.entitlements`, + 'BroadcastExtension-PrivacyInfo.xcprivacy', + 'BroadcastHelper.h', + 'BroadcastExtension-Bridging-Header.h', +]; + +// All extension files combined +export const BROADCAST_EXT_ALL_FILES = [ + ...BROADCAST_EXT_SOURCE_FILES, + ...BROADCAST_EXT_CONFIG_FILES, +]; + +export const getAppGroup = (mainAppBundleId: string, props: ConfigProps) => { + if (props.iosAppGroupIdentifier) return props.iosAppGroupIdentifier; + return `group.${mainAppBundleId}.screen-recorder`; +}; + +// Helper function to get broadcast extension bundle identifier +export function getBroadcastExtensionBundleIdentifier( + mainAppBundleId: string, + props: ConfigProps +): string { + if (props.iosExtensionBundleIdentifier) + return props.iosExtensionBundleIdentifier; + const targetName = getBroadcastExtensionTargetName(props); + return `${mainAppBundleId}.${targetName}`; +} diff --git a/lib/commonjs/expo-plugin/support/updatePodfile.js b/lib/commonjs/expo-plugin/support/updatePodfile.js new file mode 100644 index 0000000..dc94234 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/updatePodfile.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updatePodfile = updatePodfile; +// updatePodfile.ts +const fs_1 = __importDefault(require("fs")); +const iosConstants_1 = require("./iosConstants"); +const ScreenRecorderLog_1 = require("./ScreenRecorderLog"); +const FileManager_1 = require("./FileManager"); +async function updatePodfile(iosPath, props) { + const podfilePath = `${iosPath}/Podfile`; + let podfile = await FileManager_1.FileManager.readFile(podfilePath); + // Skip if already present + if (podfile.includes((0, iosConstants_1.getBroadcastExtensionTargetName)(props))) { + ScreenRecorderLog_1.ScreenRecorderLog.log('Extension target already in Podfile. Skippingโ€ฆ'); + return; + } + // Inject snippet into every `target 'Something' do โ€ฆ end` that looks like an iOS app + podfile = podfile.replace(/target ['"][^'"]+['"] do([\s\S]*?)end/g, (block) => block.replace(/\nend$/, `${(0, iosConstants_1.getBroadcastExtensionPodfileSnippet)(props)}\nend`)); + await fs_1.default.promises.writeFile(podfilePath, podfile, 'utf8'); + ScreenRecorderLog_1.ScreenRecorderLog.log('Inserted BroadcastExtension into Podfile.'); +} diff --git a/lib/commonjs/expo-plugin/support/updatePodfile.ts b/lib/commonjs/expo-plugin/support/updatePodfile.ts new file mode 100644 index 0000000..2053f49 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/updatePodfile.ts @@ -0,0 +1,31 @@ +// updatePodfile.ts +import fs from 'fs'; +import { + getBroadcastExtensionPodfileSnippet, + getBroadcastExtensionTargetName, +} from './iosConstants'; +import { ScreenRecorderLog } from './ScreenRecorderLog'; +import { FileManager } from './FileManager'; +import type { ConfigProps } from '../@types'; + +export async function updatePodfile(iosPath: string, props: ConfigProps) { + const podfilePath = `${iosPath}/Podfile`; + let podfile = await FileManager.readFile(podfilePath); + + // Skip if already present + if (podfile.includes(getBroadcastExtensionTargetName(props))) { + ScreenRecorderLog.log('Extension target already in Podfile. Skippingโ€ฆ'); + return; + } + + // Inject snippet into every `target 'Something' do โ€ฆ end` that looks like an iOS app + podfile = podfile.replace(/target ['"][^'"]+['"] do([\s\S]*?)end/g, (block) => + block.replace( + /\nend$/, + `${getBroadcastExtensionPodfileSnippet(props)}\nend` + ) + ); + + await fs.promises.writeFile(podfilePath, podfile, 'utf8'); + ScreenRecorderLog.log('Inserted BroadcastExtension into Podfile.'); +} diff --git a/lib/commonjs/expo-plugin/support/validatePluginProps.js b/lib/commonjs/expo-plugin/support/validatePluginProps.js new file mode 100644 index 0000000..f92447c --- /dev/null +++ b/lib/commonjs/expo-plugin/support/validatePluginProps.js @@ -0,0 +1,54 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validatePluginProps = validatePluginProps; +const PLUGIN_NAME = 'Nitro Screen Recorder Expo Plugin'; +const VALID_PLUGIN_PROP_NAMES = [ + 'enableCameraPermission', + 'cameraPermissionText', + 'enableMicrophonePermission', + 'microphonePermissionText', + 'showPluginLogs', + 'iosBroadcastExtensionTargetName', + 'iosAppGroupIdentifier', + 'iosExtensionBundleIdentifier', +]; +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +function validatePluginProps(props) { + if (props == null || typeof props !== 'object') { + throw new Error(`${PLUGIN_NAME}: expected props to be an object, got ${typeof props}`); + } + if (props.enableCameraPermission !== undefined && + typeof props.enableCameraPermission !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'enableCameraPermission' must be a boolean.`); + } + if (props.cameraPermissionText !== undefined && + typeof props.cameraPermissionText !== 'string') { + throw new Error(`${PLUGIN_NAME}: 'cameraPermissionText' must be a string.`); + } + if (props.enableMicrophonePermission !== undefined && + typeof props.enableMicrophonePermission !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'enableMicrophonePermission' must be a boolean.`); + } + if (props.microphonePermissionText !== undefined && + typeof props.microphonePermissionText !== 'string') { + throw new Error(`${PLUGIN_NAME}: 'microphonePermissionText' must be a string.`); + } + if (props.showPluginLogs !== undefined && + typeof props.showPluginLogs !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'showPluginLogs' must be a boolean.`); + } + if (props.iosBroadcastExtensionTargetName !== undefined && + props.iosBroadcastExtensionTargetName.includes(' ')) { + throw new Error(`${PLUGIN_NAME}: 'iosBroadcastExtensionTargetName' cannot have spaces.`); + } + if (props.iosAppGroupIdentifier !== undefined && + !props.iosAppGroupIdentifier.startsWith('group')) { + throw new Error(`${PLUGIN_NAME}: 'iosAppGroupIdentifier' must start with group! Try changing to "group.(insert main app bundle id) or removing this line and letting the plugin manage the app group name for you.`); + } + const invalidKeys = Object.keys(props).filter((k) => !VALID_PLUGIN_PROP_NAMES.includes(k)); + if (invalidKeys.length > 0) { + throw new Error(`${PLUGIN_NAME}: invalid propert${invalidKeys.length === 1 ? 'y' : 'ies'} ${invalidKeys.map((p) => `"${p}"`).join(', ')} provided.`); + } +} diff --git a/lib/commonjs/expo-plugin/support/validatePluginProps.ts b/lib/commonjs/expo-plugin/support/validatePluginProps.ts new file mode 100644 index 0000000..15e4962 --- /dev/null +++ b/lib/commonjs/expo-plugin/support/validatePluginProps.ts @@ -0,0 +1,95 @@ +import type { ConfigProps } from '../@types'; + +const PLUGIN_NAME = 'Nitro Screen Recorder Expo Plugin'; + +const VALID_PLUGIN_PROP_NAMES: string[] = [ + 'enableCameraPermission', + 'cameraPermissionText', + 'enableMicrophonePermission', + 'microphonePermissionText', + 'showPluginLogs', + 'iosBroadcastExtensionTargetName', + 'iosAppGroupIdentifier', + 'iosExtensionBundleIdentifier', +]; + +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +export function validatePluginProps(props: ConfigProps): void { + if (props == null || typeof props !== 'object') { + throw new Error( + `${PLUGIN_NAME}: expected props to be an object, got ${typeof props}` + ); + } + + if ( + props.enableCameraPermission !== undefined && + typeof props.enableCameraPermission !== 'boolean' + ) { + throw new Error( + `${PLUGIN_NAME}: 'enableCameraPermission' must be a boolean.` + ); + } + + if ( + props.cameraPermissionText !== undefined && + typeof props.cameraPermissionText !== 'string' + ) { + throw new Error(`${PLUGIN_NAME}: 'cameraPermissionText' must be a string.`); + } + + if ( + props.enableMicrophonePermission !== undefined && + typeof props.enableMicrophonePermission !== 'boolean' + ) { + throw new Error( + `${PLUGIN_NAME}: 'enableMicrophonePermission' must be a boolean.` + ); + } + + if ( + props.microphonePermissionText !== undefined && + typeof props.microphonePermissionText !== 'string' + ) { + throw new Error( + `${PLUGIN_NAME}: 'microphonePermissionText' must be a string.` + ); + } + + if ( + props.showPluginLogs !== undefined && + typeof props.showPluginLogs !== 'boolean' + ) { + throw new Error(`${PLUGIN_NAME}: 'showPluginLogs' must be a boolean.`); + } + + if ( + props.iosBroadcastExtensionTargetName !== undefined && + props.iosBroadcastExtensionTargetName.includes(' ') + ) { + throw new Error( + `${PLUGIN_NAME}: 'iosBroadcastExtensionTargetName' cannot have spaces.` + ); + } + + if ( + props.iosAppGroupIdentifier !== undefined && + !props.iosAppGroupIdentifier.startsWith('group') + ) { + throw new Error( + `${PLUGIN_NAME}: 'iosAppGroupIdentifier' must start with group! Try changing to "group.(insert main app bundle id) or removing this line and letting the plugin manage the app group name for you.` + ); + } + + const invalidKeys = Object.keys(props).filter( + (k) => !VALID_PLUGIN_PROP_NAMES.includes(k) + ); + if (invalidKeys.length > 0) { + throw new Error( + `${PLUGIN_NAME}: invalid propert${ + invalidKeys.length === 1 ? 'y' : 'ies' + } ${invalidKeys.map((p) => `"${p}"`).join(', ')} provided.` + ); + } +} diff --git a/lib/commonjs/expo-plugin/withScreenRecorder.js b/lib/commonjs/expo-plugin/withScreenRecorder.js new file mode 100644 index 0000000..ab7d3f6 --- /dev/null +++ b/lib/commonjs/expo-plugin/withScreenRecorder.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const config_plugins_1 = require("@expo/config-plugins"); +const withBroadcastExtension_1 = require("./ios/withBroadcastExtension"); +const withAndroidScreenRecording_1 = require("./android/withAndroidScreenRecording"); +const validatePluginProps_1 = require("./support/validatePluginProps"); +const pkg = require('../package.json'); +const CAMERA_USAGE = 'Allow $(PRODUCT_NAME) to access your camera'; +const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone'; +const withScreenRecorder = (config, props = {}) => { + var _a, _b, _c, _d; + (0, validatePluginProps_1.validatePluginProps)(props); + /*---------------IOS-------------------- */ + if (config.ios == null) + config.ios = {}; + if (config.ios.infoPlist == null) + config.ios.infoPlist = {}; + if (props.enableCameraPermission === true) { + config.ios.infoPlist.NSCameraUsageDescription = + (_b = (_a = props.cameraPermissionText) !== null && _a !== void 0 ? _a : config.ios.infoPlist.NSCameraUsageDescription) !== null && _b !== void 0 ? _b : CAMERA_USAGE; + } + if (props.enableMicrophonePermission === true) { + config.ios.infoPlist.NSMicrophoneUsageDescription = + (_d = (_c = props.microphonePermissionText) !== null && _c !== void 0 ? _c : config.ios.infoPlist.NSMicrophoneUsageDescription) !== null && _d !== void 0 ? _d : MICROPHONE_USAGE; + } + config = (0, withBroadcastExtension_1.withBroadcastExtension)(config, props); + /*---------------ANDROID-------------------- */ + const androidPermissions = [ + // already conditionally added + ...(props.enableMicrophonePermission !== false + ? ['android.permission.RECORD_AUDIO'] + : []), + 'android.permission.FOREGROUND_SERVICE', + 'android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION', + 'android.permission.POST_NOTIFICATIONS', + ]; + return (0, config_plugins_1.withPlugins)(config, [ + // Android plugins + [config_plugins_1.AndroidConfig.Permissions.withPermissions, androidPermissions], + [withAndroidScreenRecording_1.withAndroidScreenRecording, props], + ]); +}; +exports.default = (0, config_plugins_1.createRunOncePlugin)(withScreenRecorder, pkg.name, pkg.version); diff --git a/lib/commonjs/functions.js b/lib/commonjs/functions.js new file mode 100644 index 0000000..96b4dfe --- /dev/null +++ b/lib/commonjs/functions.js @@ -0,0 +1,336 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.addBroadcastPickerListener = addBroadcastPickerListener; +exports.addScreenRecordingListener = addScreenRecordingListener; +exports.cancelInAppRecording = cancelInAppRecording; +exports.clearCache = clearCache; +exports.getCameraPermissionStatus = getCameraPermissionStatus; +exports.getMicrophonePermissionStatus = getMicrophonePermissionStatus; +exports.requestCameraPermission = requestCameraPermission; +exports.requestMicrophonePermission = requestMicrophonePermission; +exports.retrieveLastGlobalRecording = retrieveLastGlobalRecording; +exports.startGlobalRecording = startGlobalRecording; +exports.startInAppRecording = startInAppRecording; +exports.stopGlobalRecording = stopGlobalRecording; +exports.stopInAppRecording = stopInAppRecording; +var _reactNativeNitroModules = require("react-native-nitro-modules"); +var _reactNative = require("react-native"); +const NitroScreenRecorderHybridObject = _reactNativeNitroModules.NitroModules.createHybridObject('NitroScreenRecorder'); +const isAndroid = _reactNative.Platform.OS === 'android'; + +// ============================================================================ +// PERMISSIONS +// ============================================================================ + +/** + * Gets the current camera permission status without requesting permission. + * + * @platform iOS, Android + * @returns The current permission status for camera access + * @example + * ```typescript + * const status = getCameraPermissionStatus(); + * if (status === 'granted') { + * // Camera is available + * } + * ``` + */ +function getCameraPermissionStatus() { + return NitroScreenRecorderHybridObject.getCameraPermissionStatus(); +} + +/** + * Gets the current microphone permission status without requesting permission. + * + * @platform iOS, Android + * @returns The current permission status for microphone access + * @example + * ```typescript + * const status = getMicrophonePermissionStatus(); + * if (status === 'granted') { + * // Microphone is available + * } + * ``` + */ +function getMicrophonePermissionStatus() { + return NitroScreenRecorderHybridObject.getMicrophonePermissionStatus(); +} + +/** + * Requests camera permission from the user if not already granted. + * Shows the system permission dialog if permission hasn't been determined. + * + * @platform iOS, Android + * @returns Promise that resolves with the permission response + * @example + * ```typescript + * const response = await requestCameraPermission(); + * if (response.status === 'granted') { + * // Permission granted, can use camera + * } + * ``` + */ +async function requestCameraPermission() { + return NitroScreenRecorderHybridObject.requestCameraPermission(); +} + +/** + * Requests microphone permission from the user if not already granted. + * Shows the system permission dialog if permission hasn't been determined. + * + * @platform iOS, Android + * @returns Promise that resolves with the permission response + * @example + * ```typescript + * const response = await requestMicrophonePermission(); + * if (response.status === 'granted') { + * // Permission granted, can record audio + * } + * ``` + */ +async function requestMicrophonePermission() { + return NitroScreenRecorderHybridObject.requestMicrophonePermission(); +} + +// ============================================================================ +// IN-APP RECORDING +// ============================================================================ + +/** + * Starts in-app screen recording with the specified configuration. + * Records only the current app's content, not system-wide screen content. + * + * @platform iOS + * @param input Configuration object containing recording options and callbacks + * @returns Promise that resolves when recording starts successfully + * @example + * ```typescript + * await startInAppRecording({ + * options: { + * enableMic: true, + * enableCamera: true, + * cameraDevice: 'front', + * cameraPreviewStyle: { width: 100, height: 150, top: 30, left: 10 } + * }, + * onRecordingFinished: (file) => { + * console.log('Recording saved:', file.path); + * } + * }); + * ``` + */ +async function startInAppRecording(input) { + if (isAndroid) { + console.warn('`startInAppRecording` is only supported on iOS.'); + return; + } + if (input.options.enableMic && getMicrophonePermissionStatus() !== 'granted') { + throw new Error('Microphone permission not granted.'); + } + if (input.options.enableCamera && getCameraPermissionStatus() !== 'granted') { + throw new Error('Camera permission not granted.'); + } + // Handle camera options based on enableCamera flag + if (input.options.enableCamera) { + return NitroScreenRecorderHybridObject.startInAppRecording(input.options.enableMic, input.options.enableCamera, input.options.cameraPreviewStyle ?? {}, input.options.cameraDevice, input.options.separateAudioFile ?? false, input.onRecordingFinished + // input.onRecordingError + ); + } else { + return NitroScreenRecorderHybridObject.startInAppRecording(input.options.enableMic, input.options.enableCamera, {}, 'front', input.options.separateAudioFile ?? false, input.onRecordingFinished + // input.onRecordingError + ); + } +} + +/** + * Stops the current in-app recording and saves the recorded video. + * The recording file will be provided through the onRecordingFinished callback. + * + * @platform iOS-only + * @example + * ```typescript + * stopInAppRecording(); // File will be available in onRecordingFinished callback + * ``` + */ +async function stopInAppRecording() { + if (isAndroid) { + console.warn('`stopInAppRecording` is only supported on iOS.'); + return; + } + return NitroScreenRecorderHybridObject.stopInAppRecording(); +} + +/** + * Cancels the current in-app recording without saving the video. + * No file will be generated and onRecordingFinished will not be called. + * + * @platform iOS-only + * @example + * ```typescript + * cancelInAppRecording(); // Recording discarded, no file saved + * ``` + */ +async function cancelInAppRecording() { + if (isAndroid) { + console.warn('`cancelInAppRecording` is only supported on iOS.'); + return; + } + return NitroScreenRecorderHybridObject.cancelInAppRecording(); +} + +// ============================================================================ +// GLOBAL RECORDING +// ============================================================================ + +/** + * Starts global screen recording that captures the entire device screen. + * Records system-wide content, including other apps and system UI. + * Requires screen recording permission on iOS. + * + * @platform iOS, Android + * @example + * ```typescript + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues + * ``` + */ +function startGlobalRecording(input) { + // On IOS, the user grants microphone permission via a picker toggle + // button, so we don't need this check first + if (input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { + throw new Error('Microphone permission not granted.'); + } + return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, input?.onRecordingError); +} + +/** + * Stops the current global screen recording and saves the video. + * The recorded file can be retrieved using retrieveLastGlobalRecording(). + * + * @platform Android/ios + * @param options.settledTimeMs A "delay" time to wait before the function + * tries to retrieve the file from the asset writer. It can take some time + * to finish completion and correclty return the file. Default = 500ms + * @example + * ```typescript + * const file = await stopGlobalRecording({ settledTimeMs: 1000 }); + * if (file) { + * console.log('Global recording saved:', file.path); + * } + * ``` + */ +async function stopGlobalRecording(options) { + let settledTimeMs = 500; + if (options?.settledTimeMs) { + if (typeof options.settledTimeMs !== 'number' || options.settledTimeMs <= 0) { + console.warn('Provided invalid value to `settledTimeMs` in `stopGlobalRecording` function, value will be ignored. Please use a value >0'); + } else { + settledTimeMs = options.settledTimeMs; + } + } + return NitroScreenRecorderHybridObject.stopGlobalRecording(settledTimeMs); +} + +/** + * Retrieves the most recently completed global recording file. + * Returns undefined if no global recording has been completed. + * + * @platform iOS, Android + * @returns The last global recording file or undefined if none exists + * @example + * ```typescript + * const lastRecording = retrieveLastGlobalRecording(); + * if (lastRecording) { + * console.log('Duration:', lastRecording.duration); + * console.log('File size:', lastRecording.size); + * } + * ``` + */ +function retrieveLastGlobalRecording() { + return NitroScreenRecorderHybridObject.retrieveLastGlobalRecording(); +} + +// ============================================================================ +// EVENT LISTENERS +// ============================================================================ + +/** + * Adds a listener for screen recording events (began, ended, etc.). + * Returns a cleanup function to remove the listener when no longer needed. + * + * @platform iOS, Android + * @param listener Callback function that receives screen recording events + * @returns Cleanup function to remove the listener + * @example + * ```typescript + * useEffect(() => { + * const removeListener = addScreenRecordingListener((event: ScreenRecordingEvent) => { + * console.log("Event type:", event.type, "Event reason:", event.reason) + * }); + * // Later, remove the listener + * return () => removeListener(); + * },[]) + * ``` + */ +function addScreenRecordingListener({ + listener, + ignoreRecordingsInitiatedElsewhere = false +}) { + let listenerId; + listenerId = NitroScreenRecorderHybridObject.addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere, listener); + return () => { + NitroScreenRecorderHybridObject.removeScreenRecordingListener(listenerId); + }; +} + +/** + * Adds a listener for ios only to track whether (start, stop, error, etc.). + * Returns a cleanup function to remove the listener when no longer needed. + * + * @platform iOS + * @param listener Callback function that receives the status of the BroadcastPickerView + * on ios + * @returns Cleanup function to remove the listener + * @example + * ```typescript + * useEffect(() => { + * const removeListener = addBroadcastPickerListener((event: BroadcastPickerPresentationEvent) => { + * console.log("Picker status", event) + * }); + * // Later, remove the listener + * return () => removeListener(); + * },[]) + * ``` + */ +function addBroadcastPickerListener(listener) { + if (_reactNative.Platform.OS === 'android') { + // return a no-op cleanup function + return () => {}; + } + let listenerId; + listenerId = NitroScreenRecorderHybridObject.addBroadcastPickerListener(listener); + return () => { + NitroScreenRecorderHybridObject.removeBroadcastPickerListener(listenerId); + }; +} + +// ============================================================================ +// UTILITIES +// ============================================================================ + +/** + * Clears all cached recording files to free up storage space. + * This will delete temporary files but not files that have been explicitly saved. + * + * @platform iOS, Android + * @example + * ```typescript + * clearCache(); // Frees up storage by removing temporary recording files + * ``` + */ +function clearCache() { + return NitroScreenRecorderHybridObject.clearRecordingCache(); +} +//# sourceMappingURL=functions.js.map \ No newline at end of file diff --git a/lib/commonjs/functions.js.map b/lib/commonjs/functions.js.map new file mode 100644 index 0000000..9fe31c0 --- /dev/null +++ b/lib/commonjs/functions.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_reactNativeNitroModules","require","_reactNative","NitroScreenRecorderHybridObject","NitroModules","createHybridObject","isAndroid","Platform","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","startGlobalRecording","onRecordingError","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} diff --git a/lib/commonjs/hooks/index.js b/lib/commonjs/hooks/index.js new file mode 100644 index 0000000..ce4f5fa --- /dev/null +++ b/lib/commonjs/hooks/index.js @@ -0,0 +1,17 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _useGlobalRecording = require("./useGlobalRecording"); +Object.keys(_useGlobalRecording).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _useGlobalRecording[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _useGlobalRecording[key]; + } + }); +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/commonjs/hooks/index.js.map b/lib/commonjs/hooks/index.js.map new file mode 100644 index 0000000..b3e9de0 --- /dev/null +++ b/lib/commonjs/hooks/index.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_useGlobalRecording","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get"],"sourceRoot":"../../../src","sources":["hooks/index.ts"],"mappings":";;;;;AAAA,IAAAA,mBAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,mBAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,mBAAA,CAAAK,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAT,mBAAA,CAAAK,GAAA;IAAA;EAAA;AAAA","ignoreList":[]} diff --git a/lib/commonjs/hooks/useCameraMicPermissions.js b/lib/commonjs/hooks/useCameraMicPermissions.js new file mode 100644 index 0000000..1bf6226 --- /dev/null +++ b/lib/commonjs/hooks/useCameraMicPermissions.js @@ -0,0 +1,69 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.useCameraPermission = useCameraPermission; +exports.useMicrophonePermission = useMicrophonePermission; +var _react = require("react"); +var _reactNative = require("react-native"); +var _functions = require("../functions"); +function usePermission(get, request) { + const [hasPermission, setHasPermission] = (0, _react.useState)(() => get() === 'granted'); + const requestPermission = (0, _react.useCallback)(async () => { + const result = await request(); + const hasPermissionNow = result.status === 'granted'; + setHasPermission(hasPermissionNow); + return hasPermissionNow; + }, [request]); + (0, _react.useEffect)(() => { + // Refresh permission when app state changes, as user might have allowed it in Settings + const listener = _reactNative.AppState.addEventListener('change', () => { + setHasPermission(get() === 'granted'); + }); + return () => listener.remove(); + }, [get]); + return (0, _react.useMemo)(() => ({ + hasPermission, + requestPermission + }), [hasPermission, requestPermission]); +} + +/** + * Returns whether the user has granted permission to use the Camera, or not. + * + * If the user doesn't grant Camera Permission, you cannot use the ``. + * + * @example + * ```tsx + * const { hasPermission, requestPermission } = useCameraPermission() + * + * if (!hasPermission) { + * return + * } else { + * return + * } + * ``` + */ +function useCameraPermission() { + return usePermission(_functions.getCameraPermissionStatus, _functions.requestCameraPermission); +} + +/** + * Returns whether the user has granted permission to use the Microphone, or not. + * + * If the user doesn't grant Audio Permission, you can use the `` but you cannot + * record videos with audio (the `audio={..}` prop). + * + * @example + * ```tsx + * const { hasPermission, requestPermission } = useMicrophonePermission() + * const canRecordAudio = hasPermission + * + * return + * ``` + */ +function useMicrophonePermission() { + return usePermission(_functions.getMicrophonePermissionStatus, _functions.requestMicrophonePermission); +} +//# sourceMappingURL=useCameraMicPermissions.js.map \ No newline at end of file diff --git a/lib/commonjs/hooks/useCameraMicPermissions.js.map b/lib/commonjs/hooks/useCameraMicPermissions.js.map new file mode 100644 index 0000000..8fbec51 --- /dev/null +++ b/lib/commonjs/hooks/useCameraMicPermissions.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_react","require","_reactNative","_functions","usePermission","get","request","hasPermission","setHasPermission","useState","requestPermission","useCallback","result","hasPermissionNow","status","useEffect","listener","AppState","addEventListener","remove","useMemo","useCameraPermission","getCameraPermissionStatus","requestCameraPermission","useMicrophonePermission","getMicrophonePermissionStatus","requestMicrophonePermission"],"sourceRoot":"../../../src","sources":["hooks/useCameraMicPermissions.ts"],"mappings":";;;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,UAAA,GAAAF,OAAA;AAoBA,SAASG,aAAaA,CACpBC,GAA2B,EAC3BC,OAA0C,EACzB;EACjB,MAAM,CAACC,aAAa,EAAEC,gBAAgB,CAAC,GAAG,IAAAC,eAAQ,EAAC,MAAMJ,GAAG,CAAC,CAAC,KAAK,SAAS,CAAC;EAE7E,MAAMK,iBAAiB,GAAG,IAAAC,kBAAW,EAAC,YAAY;IAChD,MAAMC,MAAM,GAAG,MAAMN,OAAO,CAAC,CAAC;IAC9B,MAAMO,gBAAgB,GAAGD,MAAM,CAACE,MAAM,KAAK,SAAS;IACpDN,gBAAgB,CAACK,gBAAgB,CAAC;IAClC,OAAOA,gBAAgB;EACzB,CAAC,EAAE,CAACP,OAAO,CAAC,CAAC;EAEb,IAAAS,gBAAS,EAAC,MAAM;IACd;IACA,MAAMC,QAAQ,GAAGC,qBAAQ,CAACC,gBAAgB,CAAC,QAAQ,EAAE,MAAM;MACzDV,gBAAgB,CAACH,GAAG,CAAC,CAAC,KAAK,SAAS,CAAC;IACvC,CAAC,CAAC;IACF,OAAO,MAAMW,QAAQ,CAACG,MAAM,CAAC,CAAC;EAChC,CAAC,EAAE,CAACd,GAAG,CAAC,CAAC;EAET,OAAO,IAAAe,cAAO,EACZ,OAAO;IACLb,aAAa;IACbG;EACF,CAAC,CAAC,EACF,CAACH,aAAa,EAAEG,iBAAiB,CACnC,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASW,mBAAmBA,CAAA,EAAoB;EACrD,OAAOjB,aAAa,CAACkB,oCAAyB,EAAEC,kCAAuB,CAAC;AAC1E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,uBAAuBA,CAAA,EAAoB;EACzD,OAAOpB,aAAa,CAClBqB,wCAA6B,EAC7BC,sCACF,CAAC;AACH","ignoreList":[]} diff --git a/lib/commonjs/hooks/useGlobalRecording.js b/lib/commonjs/hooks/useGlobalRecording.js new file mode 100644 index 0000000..d7aebde --- /dev/null +++ b/lib/commonjs/hooks/useGlobalRecording.js @@ -0,0 +1,105 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.useGlobalRecording = void 0; +var _react = require("react"); +var _functions = require("../functions"); +/** + * A "modern" sleep statement. + * + * @param ms The number of milliseconds to wait. + */ +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Configuration options for the global recording hook. + */ + +/** + * Return value from the global recording hook. + */ + +/** + * React hook for monitoring and responding to global screen recording events. + * + * This hook automatically tracks the state of global screen recordings (recordings + * that capture the entire device screen, not just your app) and provides callbacks + * for when recordings start and finish. It also manages the timing of file retrieval + * to ensure the recording file is fully written before attempting to access it. + * + * **Key Features:** + * - Automatically tracks global recording state + * - Provides lifecycle callbacks for recording start/finish events + * - Handles timing delays for safe file retrieval + * - Filters out within-app recordings (only responds to global recordings) + * + * **Use Cases:** + * - Show recording indicators in your UI + * - Automatically upload or process completed recordings + * - Trigger analytics events for recording usage + * - Update app state based on recording activity + * + * @param props Configuration options for the hook + * @returns Object containing the current recording state + * + * @example + * ```tsx + * const { isRecording } = useGlobalRecording({ + * onRecordingStarted: () => { + * analytics.track('recording_started'); + * }, + * onBroadcastModalShown: () => { + * console.log("User tried to initiate recording") + * }, + * onBroadcastModalDismissed: () => { + * redirectToAnotherApp() + * }, + * onRecordingFinished: async (file) => { + * if (file) { + * try { + * await uploadRecording(file); + * showSuccessToast('Recording uploaded successfully!'); + * } catch (error) { + * showErrorToast('Failed to upload recording'); + * } + * } + * }, + * }); + * ``` + */ +const useGlobalRecording = props => { + const [isRecording, setIsRecording] = (0, _react.useState)(false); + (0, _react.useEffect)(() => { + const unsubscribe = (0, _functions.addScreenRecordingListener)({ + ignoreRecordingsInitiatedElsewhere: props?.ignoreRecordingsInitiatedElsewhere ?? false, + listener: async event => { + if (event.type === 'withinApp') return; + if (event.reason === 'began') { + setIsRecording(true); + props?.onRecordingStarted?.(); + } else { + setIsRecording(false); + // We add a small delay after the recording ends to allow the file to finish writing + // to disk before trying to fetch it + await delay(props?.settledTimeMs ?? 500); + const file = (0, _functions.retrieveLastGlobalRecording)(); + props?.onRecordingFinished?.(file); + } + } + }); + return unsubscribe; + }, [props]); + (0, _react.useEffect)(() => { + const unsubscribe = (0, _functions.addBroadcastPickerListener)(event => { + event === 'dismissed' ? props?.onBroadcastModalDismissed?.() : props?.onBroadcastModalShown?.(); + }); + return unsubscribe; + }, [props]); + return { + isRecording + }; +}; +exports.useGlobalRecording = useGlobalRecording; +//# sourceMappingURL=useGlobalRecording.js.map \ No newline at end of file diff --git a/lib/commonjs/hooks/useGlobalRecording.js.map b/lib/commonjs/hooks/useGlobalRecording.js.map new file mode 100644 index 0000000..10607ae --- /dev/null +++ b/lib/commonjs/hooks/useGlobalRecording.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_react","require","_functions","delay","ms","Promise","resolve","setTimeout","useGlobalRecording","props","isRecording","setIsRecording","useState","useEffect","unsubscribe","addScreenRecordingListener","ignoreRecordingsInitiatedElsewhere","listener","event","type","reason","onRecordingStarted","settledTimeMs","file","retrieveLastGlobalRecording","onRecordingFinished","addBroadcastPickerListener","onBroadcastModalDismissed","onBroadcastModalShown","exports"],"sourceRoot":"../../../src","sources":["hooks/useGlobalRecording.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,UAAA,GAAAD,OAAA;AAOA;AACA;AACA;AACA;AACA;AACA,MAAME,KAAK,GAAIC,EAAU,IACvB,IAAIC,OAAO,CAAEC,OAAO,IAAKC,UAAU,CAACD,OAAO,EAAgBF,EAAE,CAAC,CAAC;;AAEjE;AACA;AACA;;AAqCA;AACA;AACA;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMI,kBAAkB,GAC7BC,KAAgC,IACF;EAC9B,MAAM,CAACC,WAAW,EAAEC,cAAc,CAAC,GAAG,IAAAC,eAAQ,EAAC,KAAK,CAAC;EAErD,IAAAC,gBAAS,EAAC,MAAM;IACd,MAAMC,WAAW,GAAG,IAAAC,qCAA0B,EAAC;MAC7CC,kCAAkC,EAChCP,KAAK,EAAEO,kCAAkC,IAAI,KAAK;MACpDC,QAAQ,EAAE,MAAOC,KAAK,IAAK;QACzB,IAAIA,KAAK,CAACC,IAAI,KAAK,WAAW,EAAE;QAEhC,IAAID,KAAK,CAACE,MAAM,KAAK,OAAO,EAAE;UAC5BT,cAAc,CAAC,IAAI,CAAC;UACpBF,KAAK,EAAEY,kBAAkB,GAAG,CAAC;QAC/B,CAAC,MAAM;UACLV,cAAc,CAAC,KAAK,CAAC;UACrB;UACA;UACA,MAAMR,KAAK,CAACM,KAAK,EAAEa,aAAa,IAAI,GAAG,CAAC;UACxC,MAAMC,IAAI,GAAG,IAAAC,sCAA2B,EAAC,CAAC;UAC1Cf,KAAK,EAAEgB,mBAAmB,GAAGF,IAAI,CAAC;QACpC;MACF;IACF,CAAC,CAAC;IAEF,OAAOT,WAAW;EACpB,CAAC,EAAE,CAACL,KAAK,CAAC,CAAC;EAEX,IAAAI,gBAAS,EAAC,MAAM;IACd,MAAMC,WAAW,GAAG,IAAAY,qCAA0B,EAAER,KAAK,IAAK;MACxDA,KAAK,KAAK,WAAW,GACjBT,KAAK,EAAEkB,yBAAyB,GAAG,CAAC,GACpClB,KAAK,EAAEmB,qBAAqB,GAAG,CAAC;IACtC,CAAC,CAAC;IAEF,OAAOd,WAAW;EACpB,CAAC,EAAE,CAACL,KAAK,CAAC,CAAC;EAEX,OAAO;IAAEC;EAAY,CAAC;AACxB,CAAC;AAACmB,OAAA,CAAArB,kBAAA,GAAAA,kBAAA","ignoreList":[]} diff --git a/lib/commonjs/index.js b/lib/commonjs/index.js new file mode 100644 index 0000000..92e49c7 --- /dev/null +++ b/lib/commonjs/index.js @@ -0,0 +1,39 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _types = require("./types"); +Object.keys(_types).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _types[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _types[key]; + } + }); +}); +var _functions = require("./functions"); +Object.keys(_functions).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _functions[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _functions[key]; + } + }); +}); +var _hooks = require("./hooks"); +Object.keys(_hooks).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _hooks[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _hooks[key]; + } + }); +}); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/commonjs/index.js.map b/lib/commonjs/index.js.map new file mode 100644 index 0000000..60ea399 --- /dev/null +++ b/lib/commonjs/index.js.map @@ -0,0 +1 @@ +{"version":3,"names":["_types","require","Object","keys","forEach","key","exports","defineProperty","enumerable","get","_functions","_hooks"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAH,MAAA,EAAAI,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAL,MAAA,CAAAK,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAT,MAAA,CAAAK,GAAA;IAAA;EAAA;AAAA;AACA,IAAAK,UAAA,GAAAT,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAO,UAAA,EAAAN,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAK,UAAA,CAAAL,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAC,UAAA,CAAAL,GAAA;IAAA;EAAA;AAAA;AACA,IAAAM,MAAA,GAAAV,OAAA;AAAAC,MAAA,CAAAC,IAAA,CAAAQ,MAAA,EAAAP,OAAA,WAAAC,GAAA;EAAA,IAAAA,GAAA,kBAAAA,GAAA;EAAA,IAAAA,GAAA,IAAAC,OAAA,IAAAA,OAAA,CAAAD,GAAA,MAAAM,MAAA,CAAAN,GAAA;EAAAH,MAAA,CAAAK,cAAA,CAAAD,OAAA,EAAAD,GAAA;IAAAG,UAAA;IAAAC,GAAA,WAAAA,CAAA;MAAA,OAAAE,MAAA,CAAAN,GAAA;IAAA;EAAA;AAAA","ignoreList":[]} diff --git a/lib/commonjs/package.json b/lib/commonjs/package.json new file mode 100644 index 0000000..729ac4d --- /dev/null +++ b/lib/commonjs/package.json @@ -0,0 +1 @@ +{"type":"commonjs"} diff --git a/lib/commonjs/types.js b/lib/commonjs/types.js new file mode 100644 index 0000000..2f0e414 --- /dev/null +++ b/lib/commonjs/types.js @@ -0,0 +1,2 @@ +"use strict"; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/lib/commonjs/types.js.map b/lib/commonjs/types.js.map new file mode 100644 index 0000000..d9cdba6 --- /dev/null +++ b/lib/commonjs/types.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sourceRoot":"../../src","sources":["types.ts"],"mappings":"","ignoreList":[]} diff --git a/lib/module/NitroScreenRecorder.nitro.js b/lib/module/NitroScreenRecorder.nitro.js new file mode 100644 index 0000000..b7dd59e --- /dev/null +++ b/lib/module/NitroScreenRecorder.nitro.js @@ -0,0 +1,4 @@ +"use strict"; + +export {}; +//# sourceMappingURL=NitroScreenRecorder.nitro.js.map \ No newline at end of file diff --git a/lib/module/NitroScreenRecorder.nitro.js.map b/lib/module/NitroScreenRecorder.nitro.js.map new file mode 100644 index 0000000..b57cda8 --- /dev/null +++ b/lib/module/NitroScreenRecorder.nitro.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroScreenRecorder.nitro.ts"],"mappings":"","ignoreList":[]} diff --git a/lib/module/expo-plugin/@types.d.ts b/lib/module/expo-plugin/@types.d.ts new file mode 100644 index 0000000..cab87a6 --- /dev/null +++ b/lib/module/expo-plugin/@types.d.ts @@ -0,0 +1,58 @@ +export interface ConfigProps { + /** + * Whether to enable camera permission for screen recording with camera overlay. + * + * @platform iOS + * @default true + * @example true + */ + enableCameraPermission?: boolean; + /** + * Camera permission description text displayed in iOS permission dialog. + * This text explains why the app needs camera access for screen recording features. + * + * @platform iOS + * @default "Allow $(PRODUCT_NAME) to access your camera for screen recording with camera overlay" + * @example "This app needs camera access to include your camera feed in screen recordings" + */ + cameraPermissionText?: string; + /** + * Whether to enable microphone permission for screen recording with audio capture. + * + * @platform iOS, Android + * @default true + * @example false + */ + enableMicrophonePermission?: boolean; + /** + * Microphone permission description text displayed in iOS permission dialog. + * This text explains why the app needs microphone access for audio recording. + * + * @platform iOS + * @default "Allow $(PRODUCT_NAME) to access your microphone for screen recording with audio" + * @example "This app needs microphone access to record audio during screen capture" + */ + microphonePermissionText?: string; + /** + * Provies a means for customizing the ios broadcast extension target name. + * @default: `BroadcastExtension` + */ + iosBroadcastExtensionTargetName?: string; + /** + * Provies a means for customizing your app group identifier. + */ + iosAppGroupIdentifier?: string; + /** + * Provies a means for customizing the ios broadcast extension bundle identifier. + */ + iosExtensionBundleIdentifier?: string; + /** + * Whether to display detailed plugin logs during the build process. + * Useful for debugging configuration issues during development. + * + * @platform iOS, Android + * @default false + * @example true + */ + showPluginLogs?: boolean; +} diff --git a/lib/module/expo-plugin/@types.js b/lib/module/expo-plugin/@types.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/module/expo-plugin/@types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/module/expo-plugin/android/withAndroidScreenRecording.d.ts b/lib/module/expo-plugin/android/withAndroidScreenRecording.d.ts new file mode 100644 index 0000000..3d12296 --- /dev/null +++ b/lib/module/expo-plugin/android/withAndroidScreenRecording.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +export declare const withAndroidScreenRecording: ConfigPlugin; diff --git a/lib/module/expo-plugin/android/withAndroidScreenRecording.js b/lib/module/expo-plugin/android/withAndroidScreenRecording.js new file mode 100644 index 0000000..ec865bc --- /dev/null +++ b/lib/module/expo-plugin/android/withAndroidScreenRecording.js @@ -0,0 +1,212 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withAndroidScreenRecording = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const withAndroidScreenRecording = (config) => { + // Add permissions and services to AndroidManifest.xml + config = (0, config_plugins_1.withAndroidManifest)(config, (mod) => { + var _a; + ScreenRecorderLog_1.ScreenRecorderLog.log('Adding screen recording permissions and services to AndroidManifest.xml'); + const androidManifest = mod.modResults; + if (!((_a = androidManifest.manifest.application) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Cannot find in AndroidManifest.xml'); + } + const application = androidManifest.manifest.application[0]; + if (!application.service) { + application.service = []; + } + // Add only the Global ScreenRecordingService + const serviceName = 'com.margelo.nitro.nitroscreenrecorder.ScreenRecordingService'; + const existingService = application.service.find((service) => { var _a; return ((_a = service.$) === null || _a === void 0 ? void 0 : _a['android:name']) === serviceName; }); + if (!existingService) { + application.service.push({ + $: { + 'android:name': serviceName, + 'android:enabled': 'true', + 'android:exported': 'false', + 'android:foregroundServiceType': 'mediaProjection', + }, + }); + ScreenRecorderLog_1.ScreenRecorderLog.log(`โœ… Added Global ScreenRecordingService to AndroidManifest.xml`); + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log(`โ„น๏ธ Global ScreenRecordingService already exists in AndroidManifest.xml`); + } + return mod; + }); + // Modify MainActivity to handle activity results (still needed for Global Recording) + config = (0, config_plugins_1.withMainActivity)(config, (mod) => { + ScreenRecorderLog_1.ScreenRecorderLog.log('Modifying MainActivity for screen recording activity results'); + const { modResults } = mod; + let mainActivityContent = modResults.contents; + const isKotlin = mainActivityContent.includes('class MainActivity') && + (mainActivityContent.includes('override fun') || + mainActivityContent.includes('kotlin')); + if (isKotlin) { + mainActivityContent = + addKotlinScreenRecordingSupport(mainActivityContent); + } + else { + mainActivityContent = addJavaScreenRecordingSupport(mainActivityContent); + } + modResults.contents = mainActivityContent; + return mod; + }); + return config; +}; +exports.withAndroidScreenRecording = withAndroidScreenRecording; +// This function remains unchanged as it's still needed for Global Recording +function addKotlinScreenRecordingSupport(content) { + // Required imports + const requiredImports = [ + 'import com.margelo.nitro.nitroscreenrecorder.NitroScreenRecorder', + 'import android.content.Intent', + 'import android.util.Log', + ]; + // Add imports if not present + requiredImports.forEach((importStatement) => { + if (!content.includes(importStatement)) { + const importRegex = /(import\s+.*\n)/g; + let lastImportMatch; + let match; + while ((match = importRegex.exec(content)) !== null) { + lastImportMatch = match; + } + if (lastImportMatch) { + const insertPosition = lastImportMatch.index + lastImportMatch[0].length; + content = + content.slice(0, insertPosition) + + importStatement + + '\n' + + content.slice(insertPosition); + } + } + }); + // Add onActivityResult method if not present + if (!content.includes('onActivityResult')) { + const classEndRegex = /(\s*)\}(\s*)$/; + const match = content.match(classEndRegex); + if (match && match.index !== undefined) { + const onActivityResultMethod = ` + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + Log.d("MainActivity", "onActivityResult: requestCode=$requestCode, resultCode=$resultCode") + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data) + } catch (e: Exception) { + Log.e("MainActivity", "Error handling activity result: \${e.message}") + e.printStackTrace() + } + } +`; + const insertPosition = match.index; + content = + content.slice(0, insertPosition) + + onActivityResultMethod + + content.slice(insertPosition); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added onActivityResult method to Kotlin MainActivity'); + } + } + else { + if (!content.includes('NitroScreenRecorder.handleActivityResult')) { + const onActivityResultRegex = /(override\s+fun\s+onActivityResult\s*\([^)]*\)\s*\{[^}]*)(super\.onActivityResult[^}]*)/; + const match = content.match(onActivityResultRegex); + if (match && match[1] && match[2]) { + const screenRecordingHandler = ` + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data) + } catch (e: Exception) { + Log.e("MainActivity", "Error handling activity result: \${e.message}") + e.printStackTrace() + }`; + content = content.replace(onActivityResultRegex, match[1] + match[2] + screenRecordingHandler); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added screen recording handler to existing onActivityResult method'); + } + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log('โ„น๏ธ Screen recording handler already exists in onActivityResult method'); + } + } + return content; +} +// This function remains unchanged as it's still needed for Global Recording +function addJavaScreenRecordingSupport(content) { + const requiredImports = [ + 'import android.content.Intent;', + 'import com.margelo.nitro.nitroscreenrecorder.NitroScreenRecorder;', + 'import android.util.Log;', + ]; + requiredImports.forEach((importStatement) => { + if (!content.includes(importStatement)) { + const importRegex = /(import\s+.*;\s*\n)/g; + let lastImportMatch; + let match; + while ((match = importRegex.exec(content)) !== null) { + lastImportMatch = match; + } + if (lastImportMatch) { + const insertPosition = lastImportMatch.index + lastImportMatch[0].length; + content = + content.slice(0, insertPosition) + + importStatement + + '\n' + + content.slice(insertPosition); + } + } + }); + if (!content.includes('onActivityResult')) { + const classEndRegex = /(\s*)\}(\s*)$/; + const match = content.match(classEndRegex); + if (match && match.index !== undefined) { + const onActivityResultMethod = ` + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.d("MainActivity", "onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode); + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data); + } catch (Exception e) { + Log.e("MainActivity", "Error handling activity result: " + e.getMessage()); + e.printStackTrace(); + } + } +`; + const insertPosition = match.index; + content = + content.slice(0, insertPosition) + + onActivityResultMethod + + content.slice(insertPosition); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added onActivityResult method to Java MainActivity'); + } + } + else { + if (!content.includes('NitroScreenRecorder.handleActivityResult')) { + const onActivityResultRegex = /(@Override\s+public\s+void\s+onActivityResult\s*\([^)]*\)\s*\{[^}]*)(super\.onActivityResult[^}]*)/; + const match = content.match(onActivityResultRegex); + if (match && match[1] && match[2]) { + const screenRecordingHandler = ` + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data); + } catch (Exception e) { + Log.e("MainActivity", "Error handling activity result: " + e.getMessage()); + e.printStackTrace(); + }`; + content = content.replace(onActivityResultRegex, match[1] + match[2] + screenRecordingHandler); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added screen recording handler to existing onActivityResult method'); + } + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log('โ„น๏ธ Screen recording handler already exists in onActivityResult method'); + } + } + return content; +} diff --git a/lib/module/expo-plugin/eas/getEasManagedCredentials.d.ts b/lib/module/expo-plugin/eas/getEasManagedCredentials.d.ts new file mode 100644 index 0000000..2d00d2d --- /dev/null +++ b/lib/module/expo-plugin/eas/getEasManagedCredentials.d.ts @@ -0,0 +1,5 @@ +import type { ConfigProps } from '../@types'; +import type { ExpoConfig } from '@expo/config-types'; +export default function getEasManagedCredentialsConfigExtra(config: ExpoConfig, props: ConfigProps): { + [k: string]: any; +}; diff --git a/lib/module/expo-plugin/eas/getEasManagedCredentials.js b/lib/module/expo-plugin/eas/getEasManagedCredentials.js new file mode 100644 index 0000000..f427f48 --- /dev/null +++ b/lib/module/expo-plugin/eas/getEasManagedCredentials.js @@ -0,0 +1,43 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = getEasManagedCredentialsConfigExtra; +const iosConstants_1 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +function getEasManagedCredentialsConfigExtra(config, props) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; + const providedExtensionBundleId = !!props.iosExtensionBundleIdentifier; + if (!providedExtensionBundleId && !((_a = config.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier)) { + (0, assert_1.default)((_b = config.ios) === null || _b === void 0 ? void 0 : _b.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + } + const extensionTargetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + return { + ...config.extra, + eas: { + ...(_c = config.extra) === null || _c === void 0 ? void 0 : _c.eas, + build: { + ...(_e = (_d = config.extra) === null || _d === void 0 ? void 0 : _d.eas) === null || _e === void 0 ? void 0 : _e.build, + experimental: { + ...(_h = (_g = (_f = config.extra) === null || _f === void 0 ? void 0 : _f.eas) === null || _g === void 0 ? void 0 : _g.build) === null || _h === void 0 ? void 0 : _h.experimental, + ios: { + ...(_m = (_l = (_k = (_j = config.extra) === null || _j === void 0 ? void 0 : _j.eas) === null || _k === void 0 ? void 0 : _k.build) === null || _l === void 0 ? void 0 : _l.experimental) === null || _m === void 0 ? void 0 : _m.ios, + appExtensions: [ + ...((_t = (_s = (_r = (_q = (_p = (_o = config.extra) === null || _o === void 0 ? void 0 : _o.eas) === null || _p === void 0 ? void 0 : _p.build) === null || _q === void 0 ? void 0 : _q.experimental) === null || _r === void 0 ? void 0 : _r.ios) === null || _s === void 0 ? void 0 : _s.appExtensions) !== null && _t !== void 0 ? _t : []), + { + targetName: extensionTargetName, + bundleIdentifier: (0, iosConstants_1.getBroadcastExtensionBundleIdentifier)((_u = config === null || config === void 0 ? void 0 : config.ios) === null || _u === void 0 ? void 0 : _u.bundleIdentifier, props), + entitlements: { + 'com.apple.security.application-groups': [ + (0, iosConstants_1.getAppGroup)((_v = config === null || config === void 0 ? void 0 : config.ios) === null || _v === void 0 ? void 0 : _v.bundleIdentifier, props), + ], + }, + }, + ], + }, + }, + }, + }, + }; +} diff --git a/lib/module/expo-plugin/ios/withBroadcastExtension.d.ts b/lib/module/expo-plugin/ios/withBroadcastExtension.d.ts new file mode 100644 index 0000000..454f5a7 --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtension.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +export declare const withBroadcastExtension: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtension.js b/lib/module/expo-plugin/ios/withBroadcastExtension.js new file mode 100644 index 0000000..ed15dec --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtension.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtension = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +// Local helpers / subโ€‘mods โ–ถ๏ธ +const withMainAppAppGroupInfoPlist_1 = require("./withMainAppAppGroupInfoPlist"); +const withMainAppAppGroupEntitlement_1 = require("./withMainAppAppGroupEntitlement"); +const withBroadcastExtensionFiles_1 = require("./withBroadcastExtensionFiles"); +const withBroadcastExtensionXcodeProject_1 = require("./withBroadcastExtensionXcodeProject"); +const withBroadcastExtensionPodfile_1 = require("./withBroadcastExtensionPodfile"); +const withEasManagedCredentials_1 = require("./withEasManagedCredentials"); +const withMainAppEntitlementsFile_1 = require("./withMainAppEntitlementsFile"); +const withBroadcastExtension = (config, props) => { + return (0, config_plugins_1.withPlugins)(config, [ + /** Mainโ€‘app tweaks */ + [withMainAppAppGroupInfoPlist_1.withMainAppAppGroupInfoPlist, props], + [withMainAppEntitlementsFile_1.withMainAppEntitlementsFile, props], + [withMainAppAppGroupEntitlement_1.withMainAppAppGroupEntitlement, props], + /** Broadcast extension target */ + [withBroadcastExtensionFiles_1.withBroadcastExtensionFiles, props], + [withBroadcastExtensionXcodeProject_1.withBroadcastExtensionXcodeProject, props], + [withBroadcastExtensionPodfile_1.withBroadcastExtensionPodfile, props], + /** Extras for EAS build */ + [withEasManagedCredentials_1.withEasManagedCredentials, props], + ]); +}; +exports.withBroadcastExtension = withBroadcastExtension; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtensionFiles.d.ts b/lib/module/expo-plugin/ios/withBroadcastExtensionFiles.d.ts new file mode 100644 index 0000000..cc178b4 --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtensionFiles.d.ts @@ -0,0 +1,8 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +/** + * Copies the ReplayKit Broadcast Upload Extension templates into the iOS + * project and patches them so their App Group + bundle versions match the + * host app. Mirrors OneSignal's NSE flow for consistency. + */ +export declare const withBroadcastExtensionFiles: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtensionFiles.js b/lib/module/expo-plugin/ios/withBroadcastExtensionFiles.js new file mode 100644 index 0000000..6a2745b --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtensionFiles.js @@ -0,0 +1,89 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionFiles = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const iosConstants_1 = require("../support/iosConstants"); +const FileManager_1 = require("../support/FileManager"); +const BEUpdateManager_1 = __importDefault(require("../support/BEUpdateManager")); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const SAMPLE_HANDLER_FILE = 'SampleHandler.swift'; +/** + * Copies the ReplayKit Broadcast Upload Extension templates into the iOS + * project and patches them so their App Group + bundle versions match the + * host app. Mirrors OneSignal's NSE flow for consistency. + */ +const withBroadcastExtensionFiles = (config, props) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + async (mod) => { + var _a, _b, _c, _d; + const iosPath = path.join(mod.modRequest.projectRoot, 'ios'); + const targetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + const sourceDir = path.join(__dirname, '..', 'support', 'broadcastExtensionFiles'); + fs.mkdirSync(`${iosPath}/${targetName}`, { + recursive: true, + }); + for (const extFile of iosConstants_1.BROADCAST_EXT_ALL_FILES) { + const targetFile = `${iosPath}/${targetName}/${extFile}`; + await FileManager_1.FileManager.copyFile(`${sourceDir}/${extFile}`, targetFile); + } + const sourceSamplePath = `${sourceDir}/${SAMPLE_HANDLER_FILE}`; + const targetSamplePath = `${iosPath}/${targetName}/${SAMPLE_HANDLER_FILE}`; + await FileManager_1.FileManager.copyFile(sourceSamplePath, targetSamplePath); + ScreenRecorderLog_1.ScreenRecorderLog.log(`Copied broadcast extension files to ${iosPath}/${targetName}`); + /* ------------------------------------------------------------ */ + /* 2๏ธโƒฃ Patch entitlements & Info.plist placeholders */ + /* ------------------------------------------------------------ */ + const updater = new BEUpdateManager_1.default(iosPath, props); + const mainAppBundleId = (_a = mod.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + if (!mainAppBundleId) { + throw new Error('Failed to find main app bundle id!'); + } + const groupIdentifier = (0, iosConstants_1.getAppGroup)(mainAppBundleId, props); + await updater.updateEntitlements(groupIdentifier); + await updater.updateInfoPlist((_c = (_b = mod.ios) === null || _b === void 0 ? void 0 : _b.buildNumber) !== null && _c !== void 0 ? _c : iosConstants_1.DEFAULT_BUNDLE_VERSION, groupIdentifier); + await updater.updateBundleShortVersion((_d = mod.version) !== null && _d !== void 0 ? _d : iosConstants_1.DEFAULT_BUNDLE_SHORT_VERSION); + ScreenRecorderLog_1.ScreenRecorderLog.log('Patched broadcast extension entitlements and Info.plist with app group and version values.'); + return mod; + }, + ]); +}; +exports.withBroadcastExtensionFiles = withBroadcastExtensionFiles; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts b/lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts new file mode 100644 index 0000000..94c0564 --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +export declare const withBroadcastExtensionPodfile: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.js b/lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.js new file mode 100644 index 0000000..6b6e659 --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtensionPodfile.js @@ -0,0 +1,21 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionPodfile = void 0; +const path_1 = __importDefault(require("path")); +const config_plugins_1 = require("@expo/config-plugins"); +const updatePodfile_1 = require("../support/updatePodfile"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const withBroadcastExtensionPodfile = (config, props) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + async (mod) => { + const iosRoot = path_1.default.join(mod.modRequest.projectRoot, 'ios'); + await (0, updatePodfile_1.updatePodfile)(iosRoot, props).catch(ScreenRecorderLog_1.ScreenRecorderLog.error); + return mod; + }, + ]); +}; +exports.withBroadcastExtensionPodfile = withBroadcastExtensionPodfile; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts b/lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts new file mode 100644 index 0000000..583a769 --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +export declare const withBroadcastExtensionXcodeProject: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.js b/lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.js new file mode 100644 index 0000000..d1561f2 --- /dev/null +++ b/lib/module/expo-plugin/ios/withBroadcastExtensionXcodeProject.js @@ -0,0 +1,139 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionXcodeProject = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const assert_1 = __importDefault(require("assert")); +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Helper: pull DEVELOPMENT_TEAM from the main-app targetโ€™s build settings +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getMainAppDevelopmentTeam(pbx, l) { + var _a, _b; + const configs = pbx.pbxXCBuildConfigurationSection(); + for (const key in configs) { + const config = configs[key]; + const bs = config.buildSettings; + if (!bs || !bs.PRODUCT_NAME) + continue; + const productName = (_a = bs.PRODUCT_NAME) === null || _a === void 0 ? void 0 : _a.replace(/"/g, ''); + // Ignore other extensions/widgets + if (productName && + (productName.includes('Extension') || productName.includes('Widget'))) { + continue; + } + const developmentTeam = (_b = bs.DEVELOPMENT_TEAM) === null || _b === void 0 ? void 0 : _b.replace(/"/g, ''); + if (developmentTeam) { + l.log(`Found DEVELOPMENT_TEAM='${developmentTeam}' from main app configuration.`); + return developmentTeam; + } + } + l.error('No DEVELOPMENT_TEAM found in main app build settings. Developer will need to manually add Dev Team.'); + return null; +} +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Main Expo config-plugin +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const withBroadcastExtensionXcodeProject = (config, props) => { + return (0, config_plugins_1.withXcodeProject)(config, (newConfig) => { + var _a, _b, _c, _d; + const xcodeProject = newConfig.modResults; + const extensionTargetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + const appIdentifier = (_a = newConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + (0, assert_1.default)(appIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const bundleIdentifier = (0, iosConstants_1.getBroadcastExtensionBundleIdentifier)(appIdentifier, props); + /* ------------------------------------------------------------------ */ + /* 0. Resolve DEVELOPMENT_TEAM (props override > auto-detect > none) */ + /* ------------------------------------------------------------------ */ + const detectedDevTeam = getMainAppDevelopmentTeam(xcodeProject, ScreenRecorderLog_1.ScreenRecorderLog); + const devTeam = detectedDevTeam !== null && detectedDevTeam !== void 0 ? detectedDevTeam : undefined; + /* ------------------------------------------------------------------ */ + /* 1. Bail out early if target/group already exist */ + /* ------------------------------------------------------------------ */ + const existingTarget = xcodeProject.pbxTargetByName(extensionTargetName); + if (existingTarget) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${extensionTargetName} already exists in project. Skippingโ€ฆ`); + return newConfig; + } + const existingGroups = xcodeProject.hash.project.objects.PBXGroup; + const groupExists = Object.values(existingGroups).some((group) => group && group.name === extensionTargetName); + if (groupExists) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${extensionTargetName} group already exists in project. Skippingโ€ฆ`); + return newConfig; + } + /* ------------------------------------------------------------------ */ + /* 2. Create target, group & build phases (COMBINED APPROACH) */ + /* ------------------------------------------------------------------ */ + const pbx = xcodeProject; + // 2.1 Create PBXGroup for the extension (OneSignal style - single group creation) + const extGroup = pbx.addPbxGroup(iosConstants_1.BROADCAST_EXT_ALL_FILES, extensionTargetName, extensionTargetName); + // 2.2 Add the new PBXGroup to the top level group + const groups = pbx.hash.project.objects.PBXGroup; + Object.keys(groups).forEach(function (key) { + if (typeof groups[key] === 'object' && + groups[key].name === undefined && + groups[key].path === undefined) { + pbx.addToPbxGroup(extGroup.uuid, key); + } + }); + // 2.3 WORK AROUND for addTarget BUG (from OneSignal) + // Xcode projects don't contain these if there is only one target + const projObjects = pbx.hash.project.objects; + projObjects.PBXTargetDependency = projObjects.PBXTargetDependency || {}; + projObjects.PBXContainerItemProxy = projObjects.PBXContainerItemProxy || {}; + // 2.4 Create native target + const target = pbx.addTarget(extensionTargetName, 'app_extension', extensionTargetName); + // 2.5 Add build phases to the new target (OneSignal approach) + pbx.addBuildPhase(iosConstants_1.BROADCAST_EXT_SOURCE_FILES, // Add source files directly to the build phase + 'PBXSourcesBuildPhase', 'Sources', target.uuid); + pbx.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid); + pbx.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid); + // 2.6 Link ReplayKit + pbx.addFramework('ReplayKit.framework', { + target: target.uuid, + sourceTree: 'SDKROOT', + link: true, + }); + /* ------------------------------------------------------------------ */ + /* 3. Build-settings tweaks */ + /* ------------------------------------------------------------------ */ + const configurations = xcodeProject.pbxXCBuildConfigurationSection(); + for (const key in configurations) { + const cfg = configurations[key]; + const b = cfg.buildSettings; + if (!b) + continue; + if (b.PRODUCT_NAME === `"${extensionTargetName}"`) { + b.CLANG_ENABLE_MODULES = 'YES'; + b.INFOPLIST_FILE = `"${extensionTargetName}/BroadcastExtension-Info.plist"`; + b.CODE_SIGN_ENTITLEMENTS = `"${extensionTargetName}/BroadcastExtension.entitlements"`; + b.CODE_SIGN_STYLE = 'Automatic'; + b.CURRENT_PROJECT_VERSION = + (_c = (_b = newConfig.ios) === null || _b === void 0 ? void 0 : _b.buildNumber) !== null && _c !== void 0 ? _c : iosConstants_1.DEFAULT_BUNDLE_VERSION; + b.MARKETING_VERSION = (_d = newConfig.version) !== null && _d !== void 0 ? _d : iosConstants_1.DEFAULT_BUNDLE_SHORT_VERSION; + b.PRODUCT_BUNDLE_IDENTIFIER = `"${bundleIdentifier}"`; + b.SWIFT_VERSION = '5.0'; + b.SWIFT_EMIT_LOC_STRINGS = 'YES'; + b.SWIFT_OBJC_BRIDGING_HEADER = `"${extensionTargetName}/BroadcastExtension-Bridging-Header.h"`; + b.HEADER_SEARCH_PATHS = `"$(SRCROOT)/${extensionTargetName}"`; + b.TARGETED_DEVICE_FAMILY = iosConstants_1.TARGETED_DEVICE_FAMILY; + if (devTeam) + b.DEVELOPMENT_TEAM = devTeam; + } + } + /* ------------------------------------------------------------------ */ + /* 4. Apply DevelopmentTeam to both targets */ + /* ------------------------------------------------------------------ */ + if (devTeam) { + xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam); + const broadcastTarget = xcodeProject.pbxTargetByName(extensionTargetName); + xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam, broadcastTarget); + } + ScreenRecorderLog_1.ScreenRecorderLog.log(`Successfully created ${extensionTargetName} target with files`); + return newConfig; + }); +}; +exports.withBroadcastExtensionXcodeProject = withBroadcastExtensionXcodeProject; diff --git a/lib/module/expo-plugin/ios/withEasManagedCredentials.d.ts b/lib/module/expo-plugin/ios/withEasManagedCredentials.d.ts new file mode 100644 index 0000000..bef053e --- /dev/null +++ b/lib/module/expo-plugin/ios/withEasManagedCredentials.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +export declare const withEasManagedCredentials: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withEasManagedCredentials.js b/lib/module/expo-plugin/ios/withEasManagedCredentials.js new file mode 100644 index 0000000..d89e837 --- /dev/null +++ b/lib/module/expo-plugin/ios/withEasManagedCredentials.js @@ -0,0 +1,12 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withEasManagedCredentials = void 0; +const getEasManagedCredentials_1 = __importDefault(require("../eas/getEasManagedCredentials")); +const withEasManagedCredentials = (config, props) => { + config.extra = (0, getEasManagedCredentials_1.default)(config, props); + return config; +}; +exports.withEasManagedCredentials = withEasManagedCredentials; diff --git a/lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts b/lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts new file mode 100644 index 0000000..e531f83 --- /dev/null +++ b/lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts @@ -0,0 +1,6 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +/** + * Add "App Group" permission + */ +export declare const withMainAppAppGroupEntitlement: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.js b/lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.js new file mode 100644 index 0000000..0824d0a --- /dev/null +++ b/lib/module/expo-plugin/ios/withMainAppAppGroupEntitlement.js @@ -0,0 +1,32 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppAppGroupEntitlement = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +/** + * Add "App Group" permission + */ +const withMainAppAppGroupEntitlement = (config, props) => { + const APP_GROUP_KEY = 'com.apple.security.application-groups'; + return (0, config_plugins_1.withEntitlementsPlist)(config, (newConfig) => { + var _a, _b; + // Ensure we have an array, preserving any existing entries + if (!Array.isArray(newConfig.modResults[APP_GROUP_KEY])) { + newConfig.modResults[APP_GROUP_KEY] = []; + } + const modResultsArray = newConfig.modResults[APP_GROUP_KEY]; + (0, assert_1.default)((_a = newConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const entitlement = (0, iosConstants_1.getAppGroup)((_b = newConfig === null || newConfig === void 0 ? void 0 : newConfig.ios) === null || _b === void 0 ? void 0 : _b.bundleIdentifier, props); + // Check if our entitlement already exists + if (modResultsArray.includes(entitlement)) { + return newConfig; + } + modResultsArray.push(entitlement); + return newConfig; + }); +}; +exports.withMainAppAppGroupEntitlement = withMainAppAppGroupEntitlement; diff --git a/lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts b/lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts new file mode 100644 index 0000000..9383b28 --- /dev/null +++ b/lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +export declare const withMainAppAppGroupInfoPlist: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.js b/lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.js new file mode 100644 index 0000000..af3cf18 --- /dev/null +++ b/lib/module/expo-plugin/ios/withMainAppAppGroupInfoPlist.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppAppGroupInfoPlist = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const iosConstants_2 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +const withMainAppAppGroupInfoPlist = (config, props) => { + return (0, config_plugins_1.withInfoPlist)(config, (modConfig) => { + var _a; + const appIdentifier = (_a = modConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + (0, assert_1.default)(appIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const appGroup = (0, iosConstants_1.getAppGroup)(appIdentifier, props); + const broadcastExtensionBundleId = (0, iosConstants_2.getBroadcastExtensionBundleIdentifier)(appIdentifier, props); + modConfig.modResults.BroadcastExtensionAppGroupIdentifier = appGroup; + modConfig.modResults.BroadcastExtensionBundleIdentifier = + broadcastExtensionBundleId; + return modConfig; + }); +}; +exports.withMainAppAppGroupInfoPlist = withMainAppAppGroupInfoPlist; diff --git a/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.d.ts b/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.d.ts new file mode 100644 index 0000000..e46005b --- /dev/null +++ b/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.d.ts @@ -0,0 +1,7 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +/** + * Add the main app's entitlements file to the Xcode project navigator + * This ensures the .entitlements file is visible in Xcode's file tree + */ +export declare const withMainAppEntitlementsFile: ConfigPlugin; diff --git a/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js b/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js new file mode 100644 index 0000000..e6da01b --- /dev/null +++ b/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js @@ -0,0 +1,98 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppEntitlementsFile = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +/** + * Add the main app's entitlements file to the Xcode project navigator + * This ensures the .entitlements file is visible in Xcode's file tree + */ +const withMainAppEntitlementsFile = (config) => { + return (0, config_plugins_1.withXcodeProject)(config, (newConfig) => { + const xcodeProject = newConfig.modResults; + const projectName = newConfig.name; + const entitlementsFileName = `${projectName}.entitlements`; + const entitlementsPath = `${projectName}/${entitlementsFileName}`; + // Check if the entitlements file is already added to the project + const files = xcodeProject.hash.project.objects.PBXFileReference; + const entitlementsFileExists = Object.values(files).some((file) => file && file.path === `"${entitlementsFileName}"`); + if (entitlementsFileExists) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${entitlementsFileName} already exists in project. Skipping...`); + return newConfig; + } + // Find the main app group (try multiple approaches) + const groups = xcodeProject.hash.project.objects.PBXGroup; + let mainAppGroupKey = null; + // Debug: log all group names to understand the structure + ScreenRecorderLog_1.ScreenRecorderLog.log('Available groups:'); + for (const key in groups) { + const group = groups[key]; + if (group && group.name) { + ScreenRecorderLog_1.ScreenRecorderLog.log(` - ${group.name} (key: ${key})`); + } + } + // Try different variations of the project name + const searchNames = [ + `"${projectName}"`, // Quoted version + projectName, // Unquoted version + `"${projectName}/"`, // With trailing slash + `${projectName}/`, // Unquoted with trailing slash + ]; + for (const searchName of searchNames) { + for (const key in groups) { + const group = groups[key]; + if (group && group.name === searchName) { + mainAppGroupKey = key; + ScreenRecorderLog_1.ScreenRecorderLog.log(`Found main app group with name: ${searchName}`); + break; + } + } + if (mainAppGroupKey) + break; + } + // If still not found, try to find the group that contains AppDelegate or main source files + if (!mainAppGroupKey) { + ScreenRecorderLog_1.ScreenRecorderLog.log('Trying to find main app group by looking for AppDelegate...'); + for (const key in groups) { + const group = groups[key]; + if (group && group.children) { + // Check if this group contains typical main app files + const hasMainAppFiles = group.children.some((childKey) => { + var _a, _b, _c; + const file = files[childKey]; + return (file && + (((_a = file.path) === null || _a === void 0 ? void 0 : _a.includes('AppDelegate')) || + ((_b = file.path) === null || _b === void 0 ? void 0 : _b.includes('Info.plist')) || + ((_c = file.name) === null || _c === void 0 ? void 0 : _c.includes('AppDelegate')))); + }); + if (hasMainAppFiles) { + mainAppGroupKey = key; + ScreenRecorderLog_1.ScreenRecorderLog.log(`Found main app group by AppDelegate: ${group.name || 'unnamed'}`); + break; + } + } + } + } + if (!mainAppGroupKey) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Could not find main app group for ${projectName}. Available groups logged above.`); + return newConfig; + } + // Add the entitlements file to the project + try { + // Create the file reference + const fileRef = xcodeProject.addFile(entitlementsPath, mainAppGroupKey, { + lastKnownFileType: 'text.plist.entitlements', + defaultEncoding: 4, + target: undefined, + }); + if (fileRef) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Successfully added ${entitlementsFileName} to Xcode project navigator`); + } + } + catch (error) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Error adding entitlements file to project: ${error}`); + } + return newConfig; + }); +}; +exports.withMainAppEntitlementsFile = withMainAppEntitlementsFile; diff --git a/lib/module/expo-plugin/support/BEUpdateManager.d.ts b/lib/module/expo-plugin/support/BEUpdateManager.d.ts new file mode 100644 index 0000000..6b4f0a6 --- /dev/null +++ b/lib/module/expo-plugin/support/BEUpdateManager.d.ts @@ -0,0 +1,20 @@ +import { type ConfigProps } from '../@types'; +export default class BEUpdaterManager { + private extensionPath; + constructor(iosPath: string, props: ConfigProps); + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + updateEntitlements(groupIdentifier: string): Promise; + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + updateInfoPlist(version: string, groupIdentifier: string): Promise; + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + updateBundleShortVersion(version: string): Promise; +} diff --git a/lib/module/expo-plugin/support/BEUpdateManager.js b/lib/module/expo-plugin/support/BEUpdateManager.js new file mode 100644 index 0000000..1401c50 --- /dev/null +++ b/lib/module/expo-plugin/support/BEUpdateManager.js @@ -0,0 +1,47 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const FileManager_1 = require("./FileManager"); +const iosConstants_1 = require("./iosConstants"); +// project `ios/${BROADCAST_EXT_TARGET_NAME}` directory +const entitlementsFileName = `BroadcastExtension.entitlements`; +const plistFileName = `BroadcastExtension-Info.plist`; +class BEUpdaterManager { + constructor(iosPath, props) { + this.extensionPath = ''; + const targetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + this.extensionPath = `${iosPath}/${targetName}`; + } + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + async updateEntitlements(groupIdentifier) { + const entitlementsFilePath = `${this.extensionPath}/${entitlementsFileName}`; + let entitlementsFile = await FileManager_1.FileManager.readFile(entitlementsFilePath); + entitlementsFile = entitlementsFile.replace(iosConstants_1.GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + await FileManager_1.FileManager.writeFile(entitlementsFilePath, entitlementsFile); + } + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + async updateInfoPlist(version, groupIdentifier) { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager_1.FileManager.readFile(plistFilePath); + plistFile = plistFile + .replace(iosConstants_1.BUNDLE_VERSION_TEMPLATE_REGEX, version) + .replace(iosConstants_1.GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + await FileManager_1.FileManager.writeFile(plistFilePath, plistFile); + } + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + async updateBundleShortVersion(version) { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager_1.FileManager.readFile(plistFilePath); + plistFile = plistFile.replace(iosConstants_1.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, version); + await FileManager_1.FileManager.writeFile(plistFilePath, plistFile); + } +} +exports.default = BEUpdaterManager; diff --git a/lib/module/expo-plugin/support/BEUpdateManager.ts b/lib/module/expo-plugin/support/BEUpdateManager.ts new file mode 100644 index 0000000..6ee0411 --- /dev/null +++ b/lib/module/expo-plugin/support/BEUpdateManager.ts @@ -0,0 +1,68 @@ +import { type ConfigProps } from '../@types'; +import { FileManager } from './FileManager'; +import { + BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, + BUNDLE_VERSION_TEMPLATE_REGEX, + getBroadcastExtensionTargetName, + GROUP_IDENTIFIER_TEMPLATE_REGEX, +} from './iosConstants'; + +// project `ios/${BROADCAST_EXT_TARGET_NAME}` directory +const entitlementsFileName = `BroadcastExtension.entitlements`; +const plistFileName = `BroadcastExtension-Info.plist`; + +export default class BEUpdaterManager { + private extensionPath = ''; + + constructor(iosPath: string, props: ConfigProps) { + const targetName = getBroadcastExtensionTargetName(props); + this.extensionPath = `${iosPath}/${targetName}`; + } + + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + async updateEntitlements(groupIdentifier: string): Promise { + const entitlementsFilePath = `${this.extensionPath}/${entitlementsFileName}`; + let entitlementsFile = await FileManager.readFile(entitlementsFilePath); + + entitlementsFile = entitlementsFile.replace( + GROUP_IDENTIFIER_TEMPLATE_REGEX, + groupIdentifier + ); + + await FileManager.writeFile(entitlementsFilePath, entitlementsFile); + } + + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + async updateInfoPlist( + version: string, + groupIdentifier: string + ): Promise { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager.readFile(plistFilePath); + + plistFile = plistFile + .replace(BUNDLE_VERSION_TEMPLATE_REGEX, version) + .replace(GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + + await FileManager.writeFile(plistFilePath, plistFile); + } + + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + async updateBundleShortVersion(version: string): Promise { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager.readFile(plistFilePath); + + plistFile = plistFile.replace(BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, version); + + await FileManager.writeFile(plistFilePath, plistFile); + } +} diff --git a/lib/module/expo-plugin/support/FileManager.d.ts b/lib/module/expo-plugin/support/FileManager.d.ts new file mode 100644 index 0000000..3c4e3fb --- /dev/null +++ b/lib/module/expo-plugin/support/FileManager.d.ts @@ -0,0 +1,9 @@ +/** + * FileManager contains static *awaitable* file-system functions + */ +export declare class FileManager { + static readFile(path: string): Promise; + static writeFile(path: string, contents: string): Promise; + static copyFile(path1: string, path2: string): Promise; + static dirExists(path: string): boolean; +} diff --git a/lib/module/expo-plugin/support/FileManager.js b/lib/module/expo-plugin/support/FileManager.js new file mode 100644 index 0000000..9581566 --- /dev/null +++ b/lib/module/expo-plugin/support/FileManager.js @@ -0,0 +1,75 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileManager = void 0; +const fs = __importStar(require("fs")); +const ScreenRecorderLog_1 = require("./ScreenRecorderLog"); +/** + * FileManager contains static *awaitable* file-system functions + */ +class FileManager { + static async readFile(path) { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err || !data) { + ScreenRecorderLog_1.ScreenRecorderLog.error("Couldn't read file:" + path); + reject(err); + return; + } + resolve(data); + }); + }); + } + static async writeFile(path, contents) { + return new Promise((resolve, reject) => { + fs.writeFile(path, contents, 'utf8', (err) => { + if (err) { + ScreenRecorderLog_1.ScreenRecorderLog.error("Couldn't write file:" + path); + reject(err); + return; + } + resolve(); + }); + }); + } + static async copyFile(path1, path2) { + const fileContents = await FileManager.readFile(path1); + await FileManager.writeFile(path2, fileContents); + } + static dirExists(path) { + return fs.existsSync(path); + } +} +exports.FileManager = FileManager; diff --git a/lib/module/expo-plugin/support/FileManager.ts b/lib/module/expo-plugin/support/FileManager.ts new file mode 100644 index 0000000..1d61d78 --- /dev/null +++ b/lib/module/expo-plugin/support/FileManager.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import { ScreenRecorderLog } from './ScreenRecorderLog'; + +/** + * FileManager contains static *awaitable* file-system functions + */ +export class FileManager { + static async readFile(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err || !data) { + ScreenRecorderLog.error("Couldn't read file:" + path); + reject(err); + return; + } + resolve(data); + }); + }); + } + + static async writeFile(path: string, contents: string): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(path, contents, 'utf8', (err) => { + if (err) { + ScreenRecorderLog.error("Couldn't write file:" + path); + reject(err); + return; + } + resolve(); + }); + }); + } + + static async copyFile(path1: string, path2: string): Promise { + const fileContents = await FileManager.readFile(path1); + await FileManager.writeFile(path2, fileContents); + } + + static dirExists(path: string): boolean { + return fs.existsSync(path); + } +} diff --git a/lib/module/expo-plugin/support/ScreenRecorderLog.d.ts b/lib/module/expo-plugin/support/ScreenRecorderLog.d.ts new file mode 100644 index 0000000..051192d --- /dev/null +++ b/lib/module/expo-plugin/support/ScreenRecorderLog.d.ts @@ -0,0 +1,5 @@ +export declare class ScreenRecorderLog { + private static readonly PLUGIN; + static log(message: string, ...optional: any[]): void; + static error(message: string, ...optional: any[]): void; +} diff --git a/lib/module/expo-plugin/support/ScreenRecorderLog.js b/lib/module/expo-plugin/support/ScreenRecorderLog.js new file mode 100644 index 0000000..ae6080f --- /dev/null +++ b/lib/module/expo-plugin/support/ScreenRecorderLog.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ScreenRecorderLog = void 0; +class ScreenRecorderLog { + static log(message, ...optional) { + const green = '\x1b[32m'; + const reset = '\x1b[0m'; + console.log(`${green}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } + static error(message, ...optional) { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + console.error(`${red}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } +} +exports.ScreenRecorderLog = ScreenRecorderLog; +ScreenRecorderLog.PLUGIN = 'react-native-nitro-screen-recorder'; diff --git a/lib/module/expo-plugin/support/ScreenRecorderLog.ts b/lib/module/expo-plugin/support/ScreenRecorderLog.ts new file mode 100644 index 0000000..edc5325 --- /dev/null +++ b/lib/module/expo-plugin/support/ScreenRecorderLog.ts @@ -0,0 +1,15 @@ +export class ScreenRecorderLog { + private static readonly PLUGIN = 'react-native-nitro-screen-recorder'; + + static log(message: string, ...optional: any[]) { + const green = '\x1b[32m'; + const reset = '\x1b[0m'; + console.log(`${green}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } + + static error(message: string, ...optional: any[]) { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + console.error(`${red}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } +} diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h new file mode 100644 index 0000000..187805c --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h @@ -0,0 +1 @@ +#import "BroadcastHelper.h" \ No newline at end of file diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist new file mode 100644 index 0000000..3bff812 --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleName + $(PRODUCT_NAME) + + CFBundleDisplayName + $(PRODUCT_NAME) + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + + CFBundleExecutable + $(EXECUTABLE_NAME) + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + CFBundleShortVersionString + $(MARKETING_VERSION) + + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SampleHandler + + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + + BroadcastExtensionAppGroupIdentifier + {{GROUP_IDENTIFIER}} + + \ No newline at end of file diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy new file mode 100644 index 0000000..73c00be --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy @@ -0,0 +1,26 @@ + + + + + NSPrivacy + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryScreenCapture + NSPrivacyAccessedAPITypeReason + User-initiated screen recording via ReplayKit + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryAudio + NSPrivacyAccessedAPITypeReason + User-initiated microphone capture in screen recording + + + NSPrivacyCollectedDataTypes + + + + diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements new file mode 100644 index 0000000..470ed66 --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + {{GROUP_IDENTIFIER}} + + + \ No newline at end of file diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h new file mode 100644 index 0000000..9830154 --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h @@ -0,0 +1,7 @@ +#import + +/// Finishes a broadcast without triggering the โ€œerrorโ€ alert. +/// (RPBroadcastSampleHandlerโ€™s parameter is formally non-null, so we suppress +/// the compiler warning.) +/// Refer to https://mehmetbaykar.com/posts/how-to-gracefully-stop-a-broadcast-upload-extension/ +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull handler); \ No newline at end of file diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m new file mode 100644 index 0000000..0a67791 --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m @@ -0,0 +1,8 @@ +#import "BroadcastHelper.h" + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull handler) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [handler finishBroadcastWithError:nil]; // โ† the magic line โœจ +#pragma clang diagnostic pop +} \ No newline at end of file diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift new file mode 100644 index 0000000..44d82b1 --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -0,0 +1,434 @@ +// MARK: Broadcast Writer + +// Copied from the repo: +// https://github.com/romiroma/BroadcastWriter + +import AVFoundation +import CoreGraphics +import Foundation +import ReplayKit + +extension AVAssetWriter.Status { + var description: String { + switch self { + case .cancelled: return "cancelled" + case .completed: return "completed" + case .failed: return "failed" + case .unknown: return "unknown" + case .writing: return "writing" + @unknown default: return "@unknown default" + } + } +} + +extension CGFloat { + var nsNumber: NSNumber { + return .init(value: native) + } +} + +extension Int { + var nsNumber: NSNumber { + return .init(value: self) + } +} + +enum Error: Swift.Error { + case wrongAssetWriterStatus(AVAssetWriter.Status) + case selfDeallocated +} + +public final class BroadcastWriter { + + private var assetWriterSessionStarted: Bool = false + private var audioAssetWriterSessionStarted: Bool = false + private let assetWriterQueue: DispatchQueue + private let assetWriter: AVAssetWriter + + // Separate audio writer + private var separateAudioWriter: AVAssetWriter? + private let separateAudioFile: Bool + private let audioOutputURL: URL? + + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in + let videoWidth = screenSize.width * screenScale + let videoHeight = screenSize.height * screenScale + + // Ensure encoder-friendly even dimensions + let w = (Int(videoWidth) / 2) * 2 + let h = (Int(videoHeight) / 2) * 2 + + // Decide codec: prefer HEVC when available + let hevcSupported: Bool = { + if #available(iOS 11.0, *) { + return self.assetWriter.canApply( + outputSettings: [AVVideoCodecKey: AVVideoCodecType.hevc], + forMediaType: .video + ) + } + return false + }() + + let codec: AVVideoCodecType = hevcSupported ? .hevc : .h264 + + var compressionProperties: [String: Any] = [ + AVVideoExpectedSourceFrameRateKey: 60.nsNumber + ] + if hevcSupported { + // Works broadly; adjust if you need different profiles + compressionProperties[AVVideoProfileLevelKey] = "HEVC_Main_AutoLevel" + } else { + compressionProperties[AVVideoProfileLevelKey] = AVVideoProfileLevelH264HighAutoLevel + } + + let videoSettings: [String: Any] = [ + AVVideoCodecKey: codec, + AVVideoWidthKey: w.nsNumber, + AVVideoHeightKey: h.nsNumber, + AVVideoCompressionPropertiesKey: compressionProperties, + ] + + let input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + input.expectsMediaDataInRealTime = true + return input + }() + + private var audioSampleRate: Double { + AVAudioSession.sharedInstance().sampleRate + } + private lazy var audioInput: AVAssetWriterInput = { + + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + private lazy var microphoneInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Separate audio file input (for microphone audio only) + private lazy var separateAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + private lazy var inputs: [AVAssetWriterInput] = [ + videoInput, + audioInput, + microphoneInput, + ] + + private let screenSize: CGSize + private let screenScale: CGFloat + + public init( + outputURL url: URL, + audioOutputURL: URL? = nil, + assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), + screenSize: CGSize, + screenScale: CGFloat, + separateAudioFile: Bool = false + ) throws { + assetWriterQueue = queue + assetWriter = try .init(url: url, fileType: .mp4) + assetWriter.shouldOptimizeForNetworkUse = true + + self.screenSize = screenSize + self.screenScale = screenScale + self.separateAudioFile = separateAudioFile + self.audioOutputURL = audioOutputURL + + // Initialize separate audio writer if needed + if separateAudioFile, let audioURL = audioOutputURL { + separateAudioWriter = try .init(url: audioURL, fileType: .m4a) + separateAudioWriter?.shouldOptimizeForNetworkUse = true + } + } + + public func start() throws { + try assetWriterQueue.sync { + let status = assetWriter.status + guard status == .unknown else { + throw Error.wrongAssetWriterStatus(status) + } + try assetWriter.error.map { + throw $0 + } + inputs + .lazy + .filter(assetWriter.canAdd(_:)) + .forEach(assetWriter.add(_:)) + try assetWriter.error.map { + throw $0 + } + assetWriter.startWriting() + try assetWriter.error.map { + throw $0 + } + + // Start separate audio writer if enabled + if separateAudioFile, let audioWriter = separateAudioWriter { + let audioStatus = audioWriter.status + guard audioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(audioStatus) + } + try audioWriter.error.map { throw $0 } + if audioWriter.canAdd(separateAudioInput) { + audioWriter.add(separateAudioInput) + } + try audioWriter.error.map { throw $0 } + audioWriter.startWriting() + try audioWriter.error.map { throw $0 } + } + } + } + + public func processSampleBuffer( + _ sampleBuffer: CMSampleBuffer, + with sampleBufferType: RPSampleBufferType + ) throws -> Bool { + + guard sampleBuffer.isValid, + CMSampleBufferDataIsReady(sampleBuffer) + else { + debugPrint( + "sampleBuffer.isValid", sampleBuffer.isValid, + "CMSampleBufferDataIsReady(sampleBuffer)", CMSampleBufferDataIsReady(sampleBuffer) + ) + return false + } + + let isWriting = assetWriterQueue.sync { + assetWriter.status == .writing + } + + guard isWriting else { + debugPrint( + "assetWriter.status", + assetWriter.status.description, + "assetWriter.error:", + assetWriter.error ?? "no error" + ) + return false + } + + assetWriterQueue.sync { + startSessionIfNeeded(sampleBuffer: sampleBuffer) + } + + let capture: (CMSampleBuffer) -> Bool + switch sampleBufferType { + case .video: + capture = captureVideoOutput + case .audioApp: + capture = captureAudioOutput + case .audioMic: + capture = captureMicrophoneOutput + // Also write to separate audio file if enabled + if separateAudioFile { + assetWriterQueue.sync { + _ = captureSeparateAudioOutput(sampleBuffer) + } + } + @unknown default: + debugPrint(#file, "Unknown type of sample buffer, \(sampleBufferType)") + capture = { _ in false } + } + + return assetWriterQueue.sync { + capture(sampleBuffer) + } + } + + public func pause() { + // TODO: Pause + } + + public func resume() { + // TODO: Resume + } + + /// Result containing both video and optional audio URLs + public struct FinishResult { + public let videoURL: URL + public let audioURL: URL? + } + + public func finish() throws -> URL { + let result = try finishWithAudio() + return result.videoURL + } + + public func finishWithAudio() throws -> FinishResult { + return try assetWriterQueue.sync { + let group: DispatchGroup = .init() + + inputs + .lazy + .filter { $0.isReadyForMoreMediaData } + .forEach { $0.markAsFinished() } + + let status = assetWriter.status + guard status == .writing else { + throw Error.wrongAssetWriterStatus(status) + } + group.enter() + + var error: Swift.Error? + assetWriter.finishWriting { [weak self] in + + defer { + group.leave() + } + + guard let self = self else { + error = Error.selfDeallocated + return + } + + if let e = self.assetWriter.error { + error = e + return + } + + let status = self.assetWriter.status + guard status == .completed else { + error = Error.wrongAssetWriterStatus(status) + return + } + } + group.wait() + try error.map { throw $0 } + + // Finish separate audio writer if enabled + var audioURL: URL? = nil + if separateAudioFile, let audioWriter = separateAudioWriter { + if separateAudioInput.isReadyForMoreMediaData { + separateAudioInput.markAsFinished() + } + + if audioWriter.status == .writing { + let audioGroup = DispatchGroup() + audioGroup.enter() + + var audioError: Swift.Error? + audioWriter.finishWriting { + defer { audioGroup.leave() } + if let e = audioWriter.error { + audioError = e + return + } + if audioWriter.status != .completed { + audioError = Error.wrongAssetWriterStatus(audioWriter.status) + } + } + audioGroup.wait() + + if audioError == nil { + audioURL = audioWriter.outputURL + } + } + } + + return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + } + } +} + +extension BroadcastWriter { + + fileprivate func startSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !assetWriterSessionStarted else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + assetWriter.startSession(atSourceTime: sourceTime) + assetWriterSessionStarted = true + } + + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + audioWriter.startSession(atSourceTime: sourceTime) + audioAssetWriterSessionStarted = true + } + + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard videoInput.isReadyForMoreMediaData else { + debugPrint("videoInput is not ready") + return false + } + return videoInput.append(sampleBuffer) + } + + fileprivate func captureAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard audioInput.isReadyForMoreMediaData else { + debugPrint("audioInput is not ready") + return false + } + return audioInput.append(sampleBuffer) + } + + fileprivate func captureMicrophoneOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + + guard microphoneInput.isReadyForMoreMediaData else { + debugPrint("microphoneInput is not ready") + return false + } + return microphoneInput.append(sampleBuffer) + } + + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let audioWriter = separateAudioWriter else { + return false + } + + // Check if audio writer is still writing + guard audioWriter.status == .writing else { + debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") + return false + } + + // Start session if needed + startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard separateAudioInput.isReadyForMoreMediaData else { + debugPrint("separateAudioInput is not ready") + return false + } + return separateAudioInput.append(sampleBuffer) + } +} diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift new file mode 100644 index 0000000..f8feae7 --- /dev/null +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -0,0 +1,244 @@ +import AVFoundation +import ReplayKit +import UserNotifications +import Darwin + +@_silgen_name("finishBroadcastGracefully") +func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) + +/* + Handles the main processing of the global broadcast. + The app-group identifier is fetched from the extension's Info.plist + ("BroadcastExtensionAppGroupIdentifier" key) so you don't have to hard-code it here. + */ +final class SampleHandler: RPBroadcastSampleHandler { + + // MARK: โ€“ Properties + + private func appGroupIDFromPlist() -> String? { + guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + !value.isEmpty + else { + return nil + } + return value + } + + // Store both the CFString and CFNotificationName versions + private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString + private static let stopNotificationName = CFNotificationName(stopNotificationString) + + private lazy var hostAppGroupIdentifier: String? = { + return appGroupIDFromPlist() + }() + + private var writer: BroadcastWriter? + private let fileManager: FileManager = .default + private let nodeURL: URL + private let audioNodeURL: URL + private var sawMicBuffers = false + private var separateAudioFile: Bool = false + + // MARK: โ€“ Init + override init() { + let uuid = UUID().uuidString + nodeURL = fileManager.temporaryDirectory + .appendingPathComponent(uuid) + .appendingPathExtension(for: .mpeg4Movie) + + audioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_audio") + .appendingPathExtension("m4a") + + fileManager.removeFileIfExists(url: nodeURL) + fileManager.removeFileIfExists(url: audioNodeURL) + super.init() + } + + deinit { + CFNotificationCenterRemoveObserver( + CFNotificationCenterGetDarwinNotifyCenter(), + UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + SampleHandler.stopNotificationName, + nil + ) + } + + private func startListeningForStopSignal() { + let center = CFNotificationCenterGetDarwinNotifyCenter() + + CFNotificationCenterAddObserver( + center, + UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + { _, observer, name, _, _ in + guard + let observer, + let name, + name == SampleHandler.stopNotificationName + else { return } + + let me = Unmanaged + .fromOpaque(observer) + .takeUnretainedValue() + me.stopBroadcastGracefully() + }, + SampleHandler.stopNotificationString, + nil, + .deliverImmediately + ) + } + + // MARK: โ€“ Broadcast lifecycle + override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { + startListeningForStopSignal() + + guard let groupID = hostAppGroupIdentifier else { + finishBroadcastWithError( + NSError( + domain: "SampleHandler", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] + ) + ) + return + } + + // Check if separate audio file is requested + if let userDefaults = UserDefaults(suiteName: groupID) { + separateAudioFile = userDefaults.bool(forKey: "SeparateAudioFileEnabled") + } + + // Clean up old recordings + cleanupOldRecordings(in: groupID) + + // Start recording + let screen: UIScreen = .main + do { + writer = try .init( + outputURL: nodeURL, + audioOutputURL: separateAudioFile ? audioNodeURL : nil, + screenSize: screen.bounds.size, + screenScale: screen.scale, + separateAudioFile: separateAudioFile + ) + try writer?.start() + } catch { + finishBroadcastWithError(error) + } + } + + private func cleanupOldRecordings(in groupID: String) { + guard let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) + else { return } + + do { + let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) + for url in items where url.pathExtension.lowercased() == "mp4" { + try? fileManager.removeItem(at: url) + } + } catch { + // Non-critical error, continue with broadcast + } + } + + override func processSampleBuffer( + _ sampleBuffer: CMSampleBuffer, + with sampleBufferType: RPSampleBufferType + ) { + guard let writer else { return } + + if sampleBufferType == .audioMic { + sawMicBuffers = true + } + + do { + _ = try writer.processSampleBuffer(sampleBuffer, with: sampleBufferType) + } catch { + finishBroadcastWithError(error) + } + } + + override func broadcastPaused() { + writer?.pause() + } + + override func broadcastResumed() { + writer?.resume() + } + + private func stopBroadcastGracefully() { + finishBroadcastGracefully(self) + } + + override func broadcastFinished() { + guard let writer else { return } + + // Finish writing - use finishWithAudio to get both video and audio URLs + let result: BroadcastWriter.FinishResult + do { + result = try writer.finishWithAudio() + } catch { + // Writer failed, but we can't call finishBroadcastWithError here + // as we're already in the finish process + return + } + + guard let groupID = hostAppGroupIdentifier else { return } + + // Get container directory + guard let containerURL = fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) + else { return } + + // Create directory if needed + do { + try fileManager.createDirectory(at: containerURL, withIntermediateDirectories: true) + } catch { + return + } + + // Move video file to shared container + let videoDestination = containerURL.appendingPathComponent(result.videoURL.lastPathComponent) + do { + try fileManager.moveItem(at: result.videoURL, to: videoDestination) + } catch { + // File move failed, but we can't error out at this point + return + } + + // Move audio file to shared container if it exists + if let audioURL = result.audioURL { + let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) + do { + try fileManager.moveItem(at: audioURL, to: audioDestination) + // Store audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") + } catch { + // Audio file move failed, but video is already saved + debugPrint("Failed to move audio file: \(error)") + } + } else { + // Clear audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAudioFileName") + } + + // Persist microphone state and audio file state + UserDefaults(suiteName: groupID)? + .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") + UserDefaults(suiteName: groupID)? + .set(separateAudioFile, forKey: "LastBroadcastHadSeparateAudio") + } +} + +// MARK: โ€“ Helpers +extension FileManager { + fileprivate func removeFileIfExists(url: URL) { + guard fileExists(atPath: url.path) else { return } + try? removeItem(at: url) + } +} \ No newline at end of file diff --git a/lib/module/expo-plugin/support/iosConstants.d.ts b/lib/module/expo-plugin/support/iosConstants.d.ts new file mode 100644 index 0000000..71413cc --- /dev/null +++ b/lib/module/expo-plugin/support/iosConstants.d.ts @@ -0,0 +1,16 @@ +import type { ConfigProps } from '../@types'; +export declare const IPHONEOS_DEPLOYMENT_TARGET = "11.0"; +export declare const TARGETED_DEVICE_FAMILY = "\"1,2\""; +export declare const getBroadcastExtensionTargetName: (props: ConfigProps) => string; +export declare const getBroadcastExtensionPodfileSnippet: (props: ConfigProps) => string; +export declare const GROUP_IDENTIFIER_TEMPLATE_REGEX: RegExp; +export declare const BUNDLE_SHORT_VERSION_TEMPLATE_REGEX: RegExp; +export declare const BUNDLE_VERSION_TEMPLATE_REGEX: RegExp; +export declare const SCHEME_TEMPLATE_REGEX: RegExp; +export declare const DEFAULT_BUNDLE_VERSION = "1"; +export declare const DEFAULT_BUNDLE_SHORT_VERSION = "1.0"; +export declare const BROADCAST_EXT_SOURCE_FILES: string[]; +export declare const BROADCAST_EXT_CONFIG_FILES: string[]; +export declare const BROADCAST_EXT_ALL_FILES: string[]; +export declare const getAppGroup: (mainAppBundleId: string, props: ConfigProps) => string; +export declare function getBroadcastExtensionBundleIdentifier(mainAppBundleId: string, props: ConfigProps): string; diff --git a/lib/module/expo-plugin/support/iosConstants.js b/lib/module/expo-plugin/support/iosConstants.js new file mode 100644 index 0000000..7893815 --- /dev/null +++ b/lib/module/expo-plugin/support/iosConstants.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAppGroup = exports.BROADCAST_EXT_ALL_FILES = exports.BROADCAST_EXT_CONFIG_FILES = exports.BROADCAST_EXT_SOURCE_FILES = exports.DEFAULT_BUNDLE_SHORT_VERSION = exports.DEFAULT_BUNDLE_VERSION = exports.SCHEME_TEMPLATE_REGEX = exports.BUNDLE_VERSION_TEMPLATE_REGEX = exports.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = exports.GROUP_IDENTIFIER_TEMPLATE_REGEX = exports.getBroadcastExtensionPodfileSnippet = exports.getBroadcastExtensionTargetName = exports.TARGETED_DEVICE_FAMILY = exports.IPHONEOS_DEPLOYMENT_TARGET = void 0; +exports.getBroadcastExtensionBundleIdentifier = getBroadcastExtensionBundleIdentifier; +exports.IPHONEOS_DEPLOYMENT_TARGET = '11.0'; +exports.TARGETED_DEVICE_FAMILY = `"1,2"`; +const getBroadcastExtensionTargetName = (props) => { + if (props.iosBroadcastExtensionTargetName) + return props.iosBroadcastExtensionTargetName; + return `BroadcastExtension`; +}; +exports.getBroadcastExtensionTargetName = getBroadcastExtensionTargetName; +// Podfile configuration for ReplayKit (if needed for dependencies) +const getBroadcastExtensionPodfileSnippet = (props) => { + const targetName = (0, exports.getBroadcastExtensionTargetName)(props); + return ` + target '${targetName}' do + # ReplayKit is a system framework, no pods needed typically + # Add any specific pods for broadcast extension here if needed + end`; +}; +exports.getBroadcastExtensionPodfileSnippet = getBroadcastExtensionPodfileSnippet; +// Template replacement patterns +exports.GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; +exports.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = /{{BUNDLE_SHORT_VERSION}}/gm; +exports.BUNDLE_VERSION_TEMPLATE_REGEX = /{{BUNDLE_VERSION}}/gm; +exports.SCHEME_TEMPLATE_REGEX = /{{SCHEME}}/gm; +exports.DEFAULT_BUNDLE_VERSION = '1'; +exports.DEFAULT_BUNDLE_SHORT_VERSION = '1.0'; +// Broadcast Extension specific constants +exports.BROADCAST_EXT_SOURCE_FILES = [ + 'SampleHandler.swift', + 'BroadcastWriter.swift', + 'BroadcastHelper.m', +]; +exports.BROADCAST_EXT_CONFIG_FILES = [ + `BroadcastExtension-Info.plist`, + `BroadcastExtension.entitlements`, + 'BroadcastExtension-PrivacyInfo.xcprivacy', + 'BroadcastHelper.h', + 'BroadcastExtension-Bridging-Header.h', +]; +// All extension files combined +exports.BROADCAST_EXT_ALL_FILES = [ + ...exports.BROADCAST_EXT_SOURCE_FILES, + ...exports.BROADCAST_EXT_CONFIG_FILES, +]; +const getAppGroup = (mainAppBundleId, props) => { + if (props.iosAppGroupIdentifier) + return props.iosAppGroupIdentifier; + return `group.${mainAppBundleId}.screen-recorder`; +}; +exports.getAppGroup = getAppGroup; +// Helper function to get broadcast extension bundle identifier +function getBroadcastExtensionBundleIdentifier(mainAppBundleId, props) { + if (props.iosExtensionBundleIdentifier) + return props.iosExtensionBundleIdentifier; + const targetName = (0, exports.getBroadcastExtensionTargetName)(props); + return `${mainAppBundleId}.${targetName}`; +} diff --git a/lib/module/expo-plugin/support/iosConstants.ts b/lib/module/expo-plugin/support/iosConstants.ts new file mode 100644 index 0000000..2f102d1 --- /dev/null +++ b/lib/module/expo-plugin/support/iosConstants.ts @@ -0,0 +1,66 @@ +import type { ConfigProps } from '../@types'; + +export const IPHONEOS_DEPLOYMENT_TARGET = '11.0'; +export const TARGETED_DEVICE_FAMILY = `"1,2"`; + +export const getBroadcastExtensionTargetName = (props: ConfigProps) => { + if (props.iosBroadcastExtensionTargetName) + return props.iosBroadcastExtensionTargetName; + return `BroadcastExtension`; +}; + +// Podfile configuration for ReplayKit (if needed for dependencies) +export const getBroadcastExtensionPodfileSnippet = (props: ConfigProps) => { + const targetName = getBroadcastExtensionTargetName(props); + return ` + target '${targetName}' do + # ReplayKit is a system framework, no pods needed typically + # Add any specific pods for broadcast extension here if needed + end`; +}; + +// Template replacement patterns +export const GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; +export const BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = /{{BUNDLE_SHORT_VERSION}}/gm; +export const BUNDLE_VERSION_TEMPLATE_REGEX = /{{BUNDLE_VERSION}}/gm; +export const SCHEME_TEMPLATE_REGEX = /{{SCHEME}}/gm; + +export const DEFAULT_BUNDLE_VERSION = '1'; +export const DEFAULT_BUNDLE_SHORT_VERSION = '1.0'; + +// Broadcast Extension specific constants +export const BROADCAST_EXT_SOURCE_FILES = [ + 'SampleHandler.swift', + 'BroadcastWriter.swift', + 'BroadcastHelper.m', +]; + +export const BROADCAST_EXT_CONFIG_FILES = [ + `BroadcastExtension-Info.plist`, + `BroadcastExtension.entitlements`, + 'BroadcastExtension-PrivacyInfo.xcprivacy', + 'BroadcastHelper.h', + 'BroadcastExtension-Bridging-Header.h', +]; + +// All extension files combined +export const BROADCAST_EXT_ALL_FILES = [ + ...BROADCAST_EXT_SOURCE_FILES, + ...BROADCAST_EXT_CONFIG_FILES, +]; + +export const getAppGroup = (mainAppBundleId: string, props: ConfigProps) => { + if (props.iosAppGroupIdentifier) return props.iosAppGroupIdentifier; + return `group.${mainAppBundleId}.screen-recorder`; +}; + +// Helper function to get broadcast extension bundle identifier +export function getBroadcastExtensionBundleIdentifier( + mainAppBundleId: string, + props: ConfigProps +): string { + if (props.iosExtensionBundleIdentifier) + return props.iosExtensionBundleIdentifier; + const targetName = getBroadcastExtensionTargetName(props); + return `${mainAppBundleId}.${targetName}`; +} diff --git a/lib/module/expo-plugin/support/updatePodfile.d.ts b/lib/module/expo-plugin/support/updatePodfile.d.ts new file mode 100644 index 0000000..3e078ad --- /dev/null +++ b/lib/module/expo-plugin/support/updatePodfile.d.ts @@ -0,0 +1,2 @@ +import type { ConfigProps } from '../@types'; +export declare function updatePodfile(iosPath: string, props: ConfigProps): Promise; diff --git a/lib/module/expo-plugin/support/updatePodfile.js b/lib/module/expo-plugin/support/updatePodfile.js new file mode 100644 index 0000000..dc94234 --- /dev/null +++ b/lib/module/expo-plugin/support/updatePodfile.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updatePodfile = updatePodfile; +// updatePodfile.ts +const fs_1 = __importDefault(require("fs")); +const iosConstants_1 = require("./iosConstants"); +const ScreenRecorderLog_1 = require("./ScreenRecorderLog"); +const FileManager_1 = require("./FileManager"); +async function updatePodfile(iosPath, props) { + const podfilePath = `${iosPath}/Podfile`; + let podfile = await FileManager_1.FileManager.readFile(podfilePath); + // Skip if already present + if (podfile.includes((0, iosConstants_1.getBroadcastExtensionTargetName)(props))) { + ScreenRecorderLog_1.ScreenRecorderLog.log('Extension target already in Podfile. Skippingโ€ฆ'); + return; + } + // Inject snippet into every `target 'Something' do โ€ฆ end` that looks like an iOS app + podfile = podfile.replace(/target ['"][^'"]+['"] do([\s\S]*?)end/g, (block) => block.replace(/\nend$/, `${(0, iosConstants_1.getBroadcastExtensionPodfileSnippet)(props)}\nend`)); + await fs_1.default.promises.writeFile(podfilePath, podfile, 'utf8'); + ScreenRecorderLog_1.ScreenRecorderLog.log('Inserted BroadcastExtension into Podfile.'); +} diff --git a/lib/module/expo-plugin/support/updatePodfile.ts b/lib/module/expo-plugin/support/updatePodfile.ts new file mode 100644 index 0000000..2053f49 --- /dev/null +++ b/lib/module/expo-plugin/support/updatePodfile.ts @@ -0,0 +1,31 @@ +// updatePodfile.ts +import fs from 'fs'; +import { + getBroadcastExtensionPodfileSnippet, + getBroadcastExtensionTargetName, +} from './iosConstants'; +import { ScreenRecorderLog } from './ScreenRecorderLog'; +import { FileManager } from './FileManager'; +import type { ConfigProps } from '../@types'; + +export async function updatePodfile(iosPath: string, props: ConfigProps) { + const podfilePath = `${iosPath}/Podfile`; + let podfile = await FileManager.readFile(podfilePath); + + // Skip if already present + if (podfile.includes(getBroadcastExtensionTargetName(props))) { + ScreenRecorderLog.log('Extension target already in Podfile. Skippingโ€ฆ'); + return; + } + + // Inject snippet into every `target 'Something' do โ€ฆ end` that looks like an iOS app + podfile = podfile.replace(/target ['"][^'"]+['"] do([\s\S]*?)end/g, (block) => + block.replace( + /\nend$/, + `${getBroadcastExtensionPodfileSnippet(props)}\nend` + ) + ); + + await fs.promises.writeFile(podfilePath, podfile, 'utf8'); + ScreenRecorderLog.log('Inserted BroadcastExtension into Podfile.'); +} diff --git a/lib/module/expo-plugin/support/validatePluginProps.d.ts b/lib/module/expo-plugin/support/validatePluginProps.d.ts new file mode 100644 index 0000000..90e812f --- /dev/null +++ b/lib/module/expo-plugin/support/validatePluginProps.d.ts @@ -0,0 +1,5 @@ +import type { ConfigProps } from '../@types'; +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +export declare function validatePluginProps(props: ConfigProps): void; diff --git a/lib/module/expo-plugin/support/validatePluginProps.js b/lib/module/expo-plugin/support/validatePluginProps.js new file mode 100644 index 0000000..f92447c --- /dev/null +++ b/lib/module/expo-plugin/support/validatePluginProps.js @@ -0,0 +1,54 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validatePluginProps = validatePluginProps; +const PLUGIN_NAME = 'Nitro Screen Recorder Expo Plugin'; +const VALID_PLUGIN_PROP_NAMES = [ + 'enableCameraPermission', + 'cameraPermissionText', + 'enableMicrophonePermission', + 'microphonePermissionText', + 'showPluginLogs', + 'iosBroadcastExtensionTargetName', + 'iosAppGroupIdentifier', + 'iosExtensionBundleIdentifier', +]; +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +function validatePluginProps(props) { + if (props == null || typeof props !== 'object') { + throw new Error(`${PLUGIN_NAME}: expected props to be an object, got ${typeof props}`); + } + if (props.enableCameraPermission !== undefined && + typeof props.enableCameraPermission !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'enableCameraPermission' must be a boolean.`); + } + if (props.cameraPermissionText !== undefined && + typeof props.cameraPermissionText !== 'string') { + throw new Error(`${PLUGIN_NAME}: 'cameraPermissionText' must be a string.`); + } + if (props.enableMicrophonePermission !== undefined && + typeof props.enableMicrophonePermission !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'enableMicrophonePermission' must be a boolean.`); + } + if (props.microphonePermissionText !== undefined && + typeof props.microphonePermissionText !== 'string') { + throw new Error(`${PLUGIN_NAME}: 'microphonePermissionText' must be a string.`); + } + if (props.showPluginLogs !== undefined && + typeof props.showPluginLogs !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'showPluginLogs' must be a boolean.`); + } + if (props.iosBroadcastExtensionTargetName !== undefined && + props.iosBroadcastExtensionTargetName.includes(' ')) { + throw new Error(`${PLUGIN_NAME}: 'iosBroadcastExtensionTargetName' cannot have spaces.`); + } + if (props.iosAppGroupIdentifier !== undefined && + !props.iosAppGroupIdentifier.startsWith('group')) { + throw new Error(`${PLUGIN_NAME}: 'iosAppGroupIdentifier' must start with group! Try changing to "group.(insert main app bundle id) or removing this line and letting the plugin manage the app group name for you.`); + } + const invalidKeys = Object.keys(props).filter((k) => !VALID_PLUGIN_PROP_NAMES.includes(k)); + if (invalidKeys.length > 0) { + throw new Error(`${PLUGIN_NAME}: invalid propert${invalidKeys.length === 1 ? 'y' : 'ies'} ${invalidKeys.map((p) => `"${p}"`).join(', ')} provided.`); + } +} diff --git a/lib/module/expo-plugin/support/validatePluginProps.ts b/lib/module/expo-plugin/support/validatePluginProps.ts new file mode 100644 index 0000000..15e4962 --- /dev/null +++ b/lib/module/expo-plugin/support/validatePluginProps.ts @@ -0,0 +1,95 @@ +import type { ConfigProps } from '../@types'; + +const PLUGIN_NAME = 'Nitro Screen Recorder Expo Plugin'; + +const VALID_PLUGIN_PROP_NAMES: string[] = [ + 'enableCameraPermission', + 'cameraPermissionText', + 'enableMicrophonePermission', + 'microphonePermissionText', + 'showPluginLogs', + 'iosBroadcastExtensionTargetName', + 'iosAppGroupIdentifier', + 'iosExtensionBundleIdentifier', +]; + +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +export function validatePluginProps(props: ConfigProps): void { + if (props == null || typeof props !== 'object') { + throw new Error( + `${PLUGIN_NAME}: expected props to be an object, got ${typeof props}` + ); + } + + if ( + props.enableCameraPermission !== undefined && + typeof props.enableCameraPermission !== 'boolean' + ) { + throw new Error( + `${PLUGIN_NAME}: 'enableCameraPermission' must be a boolean.` + ); + } + + if ( + props.cameraPermissionText !== undefined && + typeof props.cameraPermissionText !== 'string' + ) { + throw new Error(`${PLUGIN_NAME}: 'cameraPermissionText' must be a string.`); + } + + if ( + props.enableMicrophonePermission !== undefined && + typeof props.enableMicrophonePermission !== 'boolean' + ) { + throw new Error( + `${PLUGIN_NAME}: 'enableMicrophonePermission' must be a boolean.` + ); + } + + if ( + props.microphonePermissionText !== undefined && + typeof props.microphonePermissionText !== 'string' + ) { + throw new Error( + `${PLUGIN_NAME}: 'microphonePermissionText' must be a string.` + ); + } + + if ( + props.showPluginLogs !== undefined && + typeof props.showPluginLogs !== 'boolean' + ) { + throw new Error(`${PLUGIN_NAME}: 'showPluginLogs' must be a boolean.`); + } + + if ( + props.iosBroadcastExtensionTargetName !== undefined && + props.iosBroadcastExtensionTargetName.includes(' ') + ) { + throw new Error( + `${PLUGIN_NAME}: 'iosBroadcastExtensionTargetName' cannot have spaces.` + ); + } + + if ( + props.iosAppGroupIdentifier !== undefined && + !props.iosAppGroupIdentifier.startsWith('group') + ) { + throw new Error( + `${PLUGIN_NAME}: 'iosAppGroupIdentifier' must start with group! Try changing to "group.(insert main app bundle id) or removing this line and letting the plugin manage the app group name for you.` + ); + } + + const invalidKeys = Object.keys(props).filter( + (k) => !VALID_PLUGIN_PROP_NAMES.includes(k) + ); + if (invalidKeys.length > 0) { + throw new Error( + `${PLUGIN_NAME}: invalid propert${ + invalidKeys.length === 1 ? 'y' : 'ies' + } ${invalidKeys.map((p) => `"${p}"`).join(', ')} provided.` + ); + } +} diff --git a/lib/module/expo-plugin/withScreenRecorder.d.ts b/lib/module/expo-plugin/withScreenRecorder.d.ts new file mode 100644 index 0000000..8dc55e7 --- /dev/null +++ b/lib/module/expo-plugin/withScreenRecorder.d.ts @@ -0,0 +1,4 @@ +import type { ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from './@types'; +declare const _default: ConfigPlugin; +export default _default; diff --git a/lib/module/expo-plugin/withScreenRecorder.js b/lib/module/expo-plugin/withScreenRecorder.js new file mode 100644 index 0000000..ab7d3f6 --- /dev/null +++ b/lib/module/expo-plugin/withScreenRecorder.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const config_plugins_1 = require("@expo/config-plugins"); +const withBroadcastExtension_1 = require("./ios/withBroadcastExtension"); +const withAndroidScreenRecording_1 = require("./android/withAndroidScreenRecording"); +const validatePluginProps_1 = require("./support/validatePluginProps"); +const pkg = require('../package.json'); +const CAMERA_USAGE = 'Allow $(PRODUCT_NAME) to access your camera'; +const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone'; +const withScreenRecorder = (config, props = {}) => { + var _a, _b, _c, _d; + (0, validatePluginProps_1.validatePluginProps)(props); + /*---------------IOS-------------------- */ + if (config.ios == null) + config.ios = {}; + if (config.ios.infoPlist == null) + config.ios.infoPlist = {}; + if (props.enableCameraPermission === true) { + config.ios.infoPlist.NSCameraUsageDescription = + (_b = (_a = props.cameraPermissionText) !== null && _a !== void 0 ? _a : config.ios.infoPlist.NSCameraUsageDescription) !== null && _b !== void 0 ? _b : CAMERA_USAGE; + } + if (props.enableMicrophonePermission === true) { + config.ios.infoPlist.NSMicrophoneUsageDescription = + (_d = (_c = props.microphonePermissionText) !== null && _c !== void 0 ? _c : config.ios.infoPlist.NSMicrophoneUsageDescription) !== null && _d !== void 0 ? _d : MICROPHONE_USAGE; + } + config = (0, withBroadcastExtension_1.withBroadcastExtension)(config, props); + /*---------------ANDROID-------------------- */ + const androidPermissions = [ + // already conditionally added + ...(props.enableMicrophonePermission !== false + ? ['android.permission.RECORD_AUDIO'] + : []), + 'android.permission.FOREGROUND_SERVICE', + 'android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION', + 'android.permission.POST_NOTIFICATIONS', + ]; + return (0, config_plugins_1.withPlugins)(config, [ + // Android plugins + [config_plugins_1.AndroidConfig.Permissions.withPermissions, androidPermissions], + [withAndroidScreenRecording_1.withAndroidScreenRecording, props], + ]); +}; +exports.default = (0, config_plugins_1.createRunOncePlugin)(withScreenRecorder, pkg.name, pkg.version); diff --git a/lib/module/functions.js b/lib/module/functions.js new file mode 100644 index 0000000..04f3768 --- /dev/null +++ b/lib/module/functions.js @@ -0,0 +1,320 @@ +"use strict"; + +import { NitroModules } from 'react-native-nitro-modules'; +import { Platform } from 'react-native'; +const NitroScreenRecorderHybridObject = NitroModules.createHybridObject('NitroScreenRecorder'); +const isAndroid = Platform.OS === 'android'; + +// ============================================================================ +// PERMISSIONS +// ============================================================================ + +/** + * Gets the current camera permission status without requesting permission. + * + * @platform iOS, Android + * @returns The current permission status for camera access + * @example + * ```typescript + * const status = getCameraPermissionStatus(); + * if (status === 'granted') { + * // Camera is available + * } + * ``` + */ +export function getCameraPermissionStatus() { + return NitroScreenRecorderHybridObject.getCameraPermissionStatus(); +} + +/** + * Gets the current microphone permission status without requesting permission. + * + * @platform iOS, Android + * @returns The current permission status for microphone access + * @example + * ```typescript + * const status = getMicrophonePermissionStatus(); + * if (status === 'granted') { + * // Microphone is available + * } + * ``` + */ +export function getMicrophonePermissionStatus() { + return NitroScreenRecorderHybridObject.getMicrophonePermissionStatus(); +} + +/** + * Requests camera permission from the user if not already granted. + * Shows the system permission dialog if permission hasn't been determined. + * + * @platform iOS, Android + * @returns Promise that resolves with the permission response + * @example + * ```typescript + * const response = await requestCameraPermission(); + * if (response.status === 'granted') { + * // Permission granted, can use camera + * } + * ``` + */ +export async function requestCameraPermission() { + return NitroScreenRecorderHybridObject.requestCameraPermission(); +} + +/** + * Requests microphone permission from the user if not already granted. + * Shows the system permission dialog if permission hasn't been determined. + * + * @platform iOS, Android + * @returns Promise that resolves with the permission response + * @example + * ```typescript + * const response = await requestMicrophonePermission(); + * if (response.status === 'granted') { + * // Permission granted, can record audio + * } + * ``` + */ +export async function requestMicrophonePermission() { + return NitroScreenRecorderHybridObject.requestMicrophonePermission(); +} + +// ============================================================================ +// IN-APP RECORDING +// ============================================================================ + +/** + * Starts in-app screen recording with the specified configuration. + * Records only the current app's content, not system-wide screen content. + * + * @platform iOS + * @param input Configuration object containing recording options and callbacks + * @returns Promise that resolves when recording starts successfully + * @example + * ```typescript + * await startInAppRecording({ + * options: { + * enableMic: true, + * enableCamera: true, + * cameraDevice: 'front', + * cameraPreviewStyle: { width: 100, height: 150, top: 30, left: 10 } + * }, + * onRecordingFinished: (file) => { + * console.log('Recording saved:', file.path); + * } + * }); + * ``` + */ +export async function startInAppRecording(input) { + if (isAndroid) { + console.warn('`startInAppRecording` is only supported on iOS.'); + return; + } + if (input.options.enableMic && getMicrophonePermissionStatus() !== 'granted') { + throw new Error('Microphone permission not granted.'); + } + if (input.options.enableCamera && getCameraPermissionStatus() !== 'granted') { + throw new Error('Camera permission not granted.'); + } + // Handle camera options based on enableCamera flag + if (input.options.enableCamera) { + return NitroScreenRecorderHybridObject.startInAppRecording(input.options.enableMic, input.options.enableCamera, input.options.cameraPreviewStyle ?? {}, input.options.cameraDevice, input.options.separateAudioFile ?? false, input.onRecordingFinished + // input.onRecordingError + ); + } else { + return NitroScreenRecorderHybridObject.startInAppRecording(input.options.enableMic, input.options.enableCamera, {}, 'front', input.options.separateAudioFile ?? false, input.onRecordingFinished + // input.onRecordingError + ); + } +} + +/** + * Stops the current in-app recording and saves the recorded video. + * The recording file will be provided through the onRecordingFinished callback. + * + * @platform iOS-only + * @example + * ```typescript + * stopInAppRecording(); // File will be available in onRecordingFinished callback + * ``` + */ +export async function stopInAppRecording() { + if (isAndroid) { + console.warn('`stopInAppRecording` is only supported on iOS.'); + return; + } + return NitroScreenRecorderHybridObject.stopInAppRecording(); +} + +/** + * Cancels the current in-app recording without saving the video. + * No file will be generated and onRecordingFinished will not be called. + * + * @platform iOS-only + * @example + * ```typescript + * cancelInAppRecording(); // Recording discarded, no file saved + * ``` + */ +export async function cancelInAppRecording() { + if (isAndroid) { + console.warn('`cancelInAppRecording` is only supported on iOS.'); + return; + } + return NitroScreenRecorderHybridObject.cancelInAppRecording(); +} + +// ============================================================================ +// GLOBAL RECORDING +// ============================================================================ + +/** + * Starts global screen recording that captures the entire device screen. + * Records system-wide content, including other apps and system UI. + * Requires screen recording permission on iOS. + * + * @platform iOS, Android + * @example + * ```typescript + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues + * ``` + */ +export function startGlobalRecording(input) { + // On IOS, the user grants microphone permission via a picker toggle + // button, so we don't need this check first + if (input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { + throw new Error('Microphone permission not granted.'); + } + return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, input?.onRecordingError); +} + +/** + * Stops the current global screen recording and saves the video. + * The recorded file can be retrieved using retrieveLastGlobalRecording(). + * + * @platform Android/ios + * @param options.settledTimeMs A "delay" time to wait before the function + * tries to retrieve the file from the asset writer. It can take some time + * to finish completion and correclty return the file. Default = 500ms + * @example + * ```typescript + * const file = await stopGlobalRecording({ settledTimeMs: 1000 }); + * if (file) { + * console.log('Global recording saved:', file.path); + * } + * ``` + */ +export async function stopGlobalRecording(options) { + let settledTimeMs = 500; + if (options?.settledTimeMs) { + if (typeof options.settledTimeMs !== 'number' || options.settledTimeMs <= 0) { + console.warn('Provided invalid value to `settledTimeMs` in `stopGlobalRecording` function, value will be ignored. Please use a value >0'); + } else { + settledTimeMs = options.settledTimeMs; + } + } + return NitroScreenRecorderHybridObject.stopGlobalRecording(settledTimeMs); +} + +/** + * Retrieves the most recently completed global recording file. + * Returns undefined if no global recording has been completed. + * + * @platform iOS, Android + * @returns The last global recording file or undefined if none exists + * @example + * ```typescript + * const lastRecording = retrieveLastGlobalRecording(); + * if (lastRecording) { + * console.log('Duration:', lastRecording.duration); + * console.log('File size:', lastRecording.size); + * } + * ``` + */ +export function retrieveLastGlobalRecording() { + return NitroScreenRecorderHybridObject.retrieveLastGlobalRecording(); +} + +// ============================================================================ +// EVENT LISTENERS +// ============================================================================ + +/** + * Adds a listener for screen recording events (began, ended, etc.). + * Returns a cleanup function to remove the listener when no longer needed. + * + * @platform iOS, Android + * @param listener Callback function that receives screen recording events + * @returns Cleanup function to remove the listener + * @example + * ```typescript + * useEffect(() => { + * const removeListener = addScreenRecordingListener((event: ScreenRecordingEvent) => { + * console.log("Event type:", event.type, "Event reason:", event.reason) + * }); + * // Later, remove the listener + * return () => removeListener(); + * },[]) + * ``` + */ +export function addScreenRecordingListener({ + listener, + ignoreRecordingsInitiatedElsewhere = false +}) { + let listenerId; + listenerId = NitroScreenRecorderHybridObject.addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere, listener); + return () => { + NitroScreenRecorderHybridObject.removeScreenRecordingListener(listenerId); + }; +} + +/** + * Adds a listener for ios only to track whether (start, stop, error, etc.). + * Returns a cleanup function to remove the listener when no longer needed. + * + * @platform iOS + * @param listener Callback function that receives the status of the BroadcastPickerView + * on ios + * @returns Cleanup function to remove the listener + * @example + * ```typescript + * useEffect(() => { + * const removeListener = addBroadcastPickerListener((event: BroadcastPickerPresentationEvent) => { + * console.log("Picker status", event) + * }); + * // Later, remove the listener + * return () => removeListener(); + * },[]) + * ``` + */ +export function addBroadcastPickerListener(listener) { + if (Platform.OS === 'android') { + // return a no-op cleanup function + return () => {}; + } + let listenerId; + listenerId = NitroScreenRecorderHybridObject.addBroadcastPickerListener(listener); + return () => { + NitroScreenRecorderHybridObject.removeBroadcastPickerListener(listenerId); + }; +} + +// ============================================================================ +// UTILITIES +// ============================================================================ + +/** + * Clears all cached recording files to free up storage space. + * This will delete temporary files but not files that have been explicitly saved. + * + * @platform iOS, Android + * @example + * ```typescript + * clearCache(); // Frees up storage by removing temporary recording files + * ``` + */ +export function clearCache() { + return NitroScreenRecorderHybridObject.clearRecordingCache(); +} +//# sourceMappingURL=functions.js.map \ No newline at end of file diff --git a/lib/module/functions.js.map b/lib/module/functions.js.map new file mode 100644 index 0000000..cb13f29 --- /dev/null +++ b/lib/module/functions.js.map @@ -0,0 +1 @@ +{"version":3,"names":["NitroModules","Platform","NitroScreenRecorderHybridObject","createHybridObject","isAndroid","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","startGlobalRecording","onRecordingError","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAWzD,SAASC,QAAQ,QAAQ,cAAc;AAEvC,MAAMC,+BAA+B,GACnCF,YAAY,CAACG,kBAAkB,CAAsB,qBAAqB,CAAC;AAE7E,MAAMC,SAAS,GAAGH,QAAQ,CAACI,EAAE,KAAK,SAAS;;AAE3C;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,yBAAyBA,CAAA,EAAqB;EAC5D,OAAOJ,+BAA+B,CAACI,yBAAyB,CAAC,CAAC;AACpE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,6BAA6BA,CAAA,EAAqB;EAChE,OAAOL,+BAA+B,CAACK,6BAA6B,CAAC,CAAC;AACxE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,uBAAuBA,CAAA,EAAgC;EAC3E,OAAON,+BAA+B,CAACM,uBAAuB,CAAC,CAAC;AAClE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,2BAA2BA,CAAA,EAAgC;EAC/E,OAAOP,+BAA+B,CAACO,2BAA2B,CAAC,CAAC;AACtE;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,mBAAmBA,CACvCC,KAA0B,EACX;EACf,IAAIP,SAAS,EAAE;IACbQ,OAAO,CAACC,IAAI,CAAC,iDAAiD,CAAC;IAC/D;EACF;EAEA,IACEF,KAAK,CAACG,OAAO,CAACC,SAAS,IACvBR,6BAA6B,CAAC,CAAC,KAAK,SAAS,EAC7C;IACA,MAAM,IAAIS,KAAK,CAAC,oCAAoC,CAAC;EACvD;EAEA,IAAIL,KAAK,CAACG,OAAO,CAACG,YAAY,IAAIX,yBAAyB,CAAC,CAAC,KAAK,SAAS,EAAE;IAC3E,MAAM,IAAIU,KAAK,CAAC,gCAAgC,CAAC;EACnD;EACA;EACA,IAAIL,KAAK,CAACG,OAAO,CAACG,YAAY,EAAE;IAC9B,OAAOf,+BAA+B,CAACQ,mBAAmB,CACxDC,KAAK,CAACG,OAAO,CAACC,SAAS,EACvBJ,KAAK,CAACG,OAAO,CAACG,YAAY,EAC1BN,KAAK,CAACG,OAAO,CAACI,kBAAkB,IAAI,CAAC,CAAC,EACtCP,KAAK,CAACG,OAAO,CAACK,YAAY,EAC1BR,KAAK,CAACG,OAAO,CAACM,iBAAiB,IAAI,KAAK,EACxCT,KAAK,CAACU;IACN;IACF,CAAC;EACH,CAAC,MAAM;IACL,OAAOnB,+BAA+B,CAACQ,mBAAmB,CACxDC,KAAK,CAACG,OAAO,CAACC,SAAS,EACvBJ,KAAK,CAACG,OAAO,CAACG,YAAY,EAC1B,CAAC,CAAC,EACF,OAAO,EACPN,KAAK,CAACG,OAAO,CAACM,iBAAiB,IAAI,KAAK,EACxCT,KAAK,CAACU;IACN;IACF,CAAC;EACH;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,kBAAkBA,CAAA,EAEtC;EACA,IAAIlB,SAAS,EAAE;IACbQ,OAAO,CAACC,IAAI,CAAC,gDAAgD,CAAC;IAC9D;EACF;EACA,OAAOX,+BAA+B,CAACoB,kBAAkB,CAAC,CAAC;AAC7D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,oBAAoBA,CAAA,EAAkB;EAC1D,IAAInB,SAAS,EAAE;IACbQ,OAAO,CAACC,IAAI,CAAC,kDAAkD,CAAC;IAChE;EACF;EACA,OAAOX,+BAA+B,CAACqB,oBAAoB,CAAC,CAAC;AAC/D;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAACb,KAA2B,EAAQ;EACtE;EACA;EACA,IACEA,KAAK,CAACG,OAAO,EAAEC,SAAS,IACxBX,SAAS,IACTG,6BAA6B,CAAC,CAAC,KAAK,SAAS,EAC7C;IACA,MAAM,IAAIS,KAAK,CAAC,oCAAoC,CAAC;EACvD;EACA,OAAOd,+BAA+B,CAACsB,oBAAoB,CACzDb,KAAK,EAAEG,OAAO,EAAEC,SAAS,IAAI,KAAK,EAClCJ,KAAK,EAAEG,OAAO,EAAEM,iBAAiB,IAAI,KAAK,EAC1CT,KAAK,EAAEc,gBACT,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,mBAAmBA,CAACZ,OAEzC,EAA4C;EAC3C,IAAIa,aAAa,GAAG,GAAG;EACvB,IAAIb,OAAO,EAAEa,aAAa,EAAE;IAC1B,IACE,OAAOb,OAAO,CAACa,aAAa,KAAK,QAAQ,IACzCb,OAAO,CAACa,aAAa,IAAI,CAAC,EAC1B;MACAf,OAAO,CAACC,IAAI,CACV,2HACF,CAAC;IACH,CAAC,MAAM;MACLc,aAAa,GAAGb,OAAO,CAACa,aAAa;IACvC;EACF;EACA,OAAOzB,+BAA+B,CAACwB,mBAAmB,CAACC,aAAa,CAAC;AAC3E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,2BAA2BA,CAAA,EAAoC;EAC7E,OAAO1B,+BAA+B,CAAC0B,2BAA2B,CAAC,CAAC;AACtE;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,0BAA0BA,CAAC;EACzCC,QAAQ;EACRC,kCAAkC,GAAG;AAIvC,CAAC,EAAc;EACb,IAAIC,UAAkB;EACtBA,UAAU,GAAG9B,+BAA+B,CAAC2B,0BAA0B,CACrEE,kCAAkC,EAClCD,QACF,CAAC;EACD,OAAO,MAAM;IACX5B,+BAA+B,CAAC+B,6BAA6B,CAACD,UAAU,CAAC;EAC3E,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,0BAA0BA,CACxCJ,QAA2D,EAC/C;EACZ,IAAI7B,QAAQ,CAACI,EAAE,KAAK,SAAS,EAAE;IAC7B;IACA,OAAO,MAAM,CAAC,CAAC;EACjB;EACA,IAAI2B,UAAkB;EACtBA,UAAU,GACR9B,+BAA+B,CAACgC,0BAA0B,CAACJ,QAAQ,CAAC;EACtE,OAAO,MAAM;IACX5B,+BAA+B,CAACiC,6BAA6B,CAACH,UAAU,CAAC;EAC3E,CAAC;AACH;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASI,UAAUA,CAAA,EAAS;EACjC,OAAOlC,+BAA+B,CAACmC,mBAAmB,CAAC,CAAC;AAC9D","ignoreList":[]} diff --git a/lib/module/hooks/index.js b/lib/module/hooks/index.js new file mode 100644 index 0000000..d98abaa --- /dev/null +++ b/lib/module/hooks/index.js @@ -0,0 +1,4 @@ +"use strict"; + +export * from './useGlobalRecording'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/module/hooks/index.js.map b/lib/module/hooks/index.js.map new file mode 100644 index 0000000..e9e1756 --- /dev/null +++ b/lib/module/hooks/index.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sourceRoot":"../../../src","sources":["hooks/index.ts"],"mappings":";;AAAA,cAAc,sBAAsB","ignoreList":[]} diff --git a/lib/module/hooks/useCameraMicPermissions.js b/lib/module/hooks/useCameraMicPermissions.js new file mode 100644 index 0000000..4e45361 --- /dev/null +++ b/lib/module/hooks/useCameraMicPermissions.js @@ -0,0 +1,64 @@ +"use strict"; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { AppState } from 'react-native'; +import { getCameraPermissionStatus, getMicrophonePermissionStatus, requestCameraPermission, requestMicrophonePermission } from '../functions'; +function usePermission(get, request) { + const [hasPermission, setHasPermission] = useState(() => get() === 'granted'); + const requestPermission = useCallback(async () => { + const result = await request(); + const hasPermissionNow = result.status === 'granted'; + setHasPermission(hasPermissionNow); + return hasPermissionNow; + }, [request]); + useEffect(() => { + // Refresh permission when app state changes, as user might have allowed it in Settings + const listener = AppState.addEventListener('change', () => { + setHasPermission(get() === 'granted'); + }); + return () => listener.remove(); + }, [get]); + return useMemo(() => ({ + hasPermission, + requestPermission + }), [hasPermission, requestPermission]); +} + +/** + * Returns whether the user has granted permission to use the Camera, or not. + * + * If the user doesn't grant Camera Permission, you cannot use the ``. + * + * @example + * ```tsx + * const { hasPermission, requestPermission } = useCameraPermission() + * + * if (!hasPermission) { + * return + * } else { + * return + * } + * ``` + */ +export function useCameraPermission() { + return usePermission(getCameraPermissionStatus, requestCameraPermission); +} + +/** + * Returns whether the user has granted permission to use the Microphone, or not. + * + * If the user doesn't grant Audio Permission, you can use the `` but you cannot + * record videos with audio (the `audio={..}` prop). + * + * @example + * ```tsx + * const { hasPermission, requestPermission } = useMicrophonePermission() + * const canRecordAudio = hasPermission + * + * return + * ``` + */ +export function useMicrophonePermission() { + return usePermission(getMicrophonePermissionStatus, requestMicrophonePermission); +} +//# sourceMappingURL=useCameraMicPermissions.js.map \ No newline at end of file diff --git a/lib/module/hooks/useCameraMicPermissions.js.map b/lib/module/hooks/useCameraMicPermissions.js.map new file mode 100644 index 0000000..acda55c --- /dev/null +++ b/lib/module/hooks/useCameraMicPermissions.js.map @@ -0,0 +1 @@ +{"version":3,"names":["useCallback","useEffect","useMemo","useState","AppState","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","usePermission","get","request","hasPermission","setHasPermission","requestPermission","result","hasPermissionNow","status","listener","addEventListener","remove","useCameraPermission","useMicrophonePermission"],"sourceRoot":"../../../src","sources":["hooks/useCameraMicPermissions.ts"],"mappings":";;AAAA,SAASA,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,OAAO;AAEjE,SAASC,QAAQ,QAAQ,cAAc;AACvC,SACEC,yBAAyB,EACzBC,6BAA6B,EAC7BC,uBAAuB,EACvBC,2BAA2B,QACtB,cAAc;AAerB,SAASC,aAAaA,CACpBC,GAA2B,EAC3BC,OAA0C,EACzB;EACjB,MAAM,CAACC,aAAa,EAAEC,gBAAgB,CAAC,GAAGV,QAAQ,CAAC,MAAMO,GAAG,CAAC,CAAC,KAAK,SAAS,CAAC;EAE7E,MAAMI,iBAAiB,GAAGd,WAAW,CAAC,YAAY;IAChD,MAAMe,MAAM,GAAG,MAAMJ,OAAO,CAAC,CAAC;IAC9B,MAAMK,gBAAgB,GAAGD,MAAM,CAACE,MAAM,KAAK,SAAS;IACpDJ,gBAAgB,CAACG,gBAAgB,CAAC;IAClC,OAAOA,gBAAgB;EACzB,CAAC,EAAE,CAACL,OAAO,CAAC,CAAC;EAEbV,SAAS,CAAC,MAAM;IACd;IACA,MAAMiB,QAAQ,GAAGd,QAAQ,CAACe,gBAAgB,CAAC,QAAQ,EAAE,MAAM;MACzDN,gBAAgB,CAACH,GAAG,CAAC,CAAC,KAAK,SAAS,CAAC;IACvC,CAAC,CAAC;IACF,OAAO,MAAMQ,QAAQ,CAACE,MAAM,CAAC,CAAC;EAChC,CAAC,EAAE,CAACV,GAAG,CAAC,CAAC;EAET,OAAOR,OAAO,CACZ,OAAO;IACLU,aAAa;IACbE;EACF,CAAC,CAAC,EACF,CAACF,aAAa,EAAEE,iBAAiB,CACnC,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASO,mBAAmBA,CAAA,EAAoB;EACrD,OAAOZ,aAAa,CAACJ,yBAAyB,EAAEE,uBAAuB,CAAC;AAC1E;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASe,uBAAuBA,CAAA,EAAoB;EACzD,OAAOb,aAAa,CAClBH,6BAA6B,EAC7BE,2BACF,CAAC;AACH","ignoreList":[]} diff --git a/lib/module/hooks/useGlobalRecording.js b/lib/module/hooks/useGlobalRecording.js new file mode 100644 index 0000000..4a201c0 --- /dev/null +++ b/lib/module/hooks/useGlobalRecording.js @@ -0,0 +1,100 @@ +"use strict"; + +import { useState, useEffect } from 'react'; +import { addBroadcastPickerListener, addScreenRecordingListener, retrieveLastGlobalRecording } from '../functions'; +/** + * A "modern" sleep statement. + * + * @param ms The number of milliseconds to wait. + */ +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Configuration options for the global recording hook. + */ + +/** + * Return value from the global recording hook. + */ + +/** + * React hook for monitoring and responding to global screen recording events. + * + * This hook automatically tracks the state of global screen recordings (recordings + * that capture the entire device screen, not just your app) and provides callbacks + * for when recordings start and finish. It also manages the timing of file retrieval + * to ensure the recording file is fully written before attempting to access it. + * + * **Key Features:** + * - Automatically tracks global recording state + * - Provides lifecycle callbacks for recording start/finish events + * - Handles timing delays for safe file retrieval + * - Filters out within-app recordings (only responds to global recordings) + * + * **Use Cases:** + * - Show recording indicators in your UI + * - Automatically upload or process completed recordings + * - Trigger analytics events for recording usage + * - Update app state based on recording activity + * + * @param props Configuration options for the hook + * @returns Object containing the current recording state + * + * @example + * ```tsx + * const { isRecording } = useGlobalRecording({ + * onRecordingStarted: () => { + * analytics.track('recording_started'); + * }, + * onBroadcastModalShown: () => { + * console.log("User tried to initiate recording") + * }, + * onBroadcastModalDismissed: () => { + * redirectToAnotherApp() + * }, + * onRecordingFinished: async (file) => { + * if (file) { + * try { + * await uploadRecording(file); + * showSuccessToast('Recording uploaded successfully!'); + * } catch (error) { + * showErrorToast('Failed to upload recording'); + * } + * } + * }, + * }); + * ``` + */ +export const useGlobalRecording = props => { + const [isRecording, setIsRecording] = useState(false); + useEffect(() => { + const unsubscribe = addScreenRecordingListener({ + ignoreRecordingsInitiatedElsewhere: props?.ignoreRecordingsInitiatedElsewhere ?? false, + listener: async event => { + if (event.type === 'withinApp') return; + if (event.reason === 'began') { + setIsRecording(true); + props?.onRecordingStarted?.(); + } else { + setIsRecording(false); + // We add a small delay after the recording ends to allow the file to finish writing + // to disk before trying to fetch it + await delay(props?.settledTimeMs ?? 500); + const file = retrieveLastGlobalRecording(); + props?.onRecordingFinished?.(file); + } + } + }); + return unsubscribe; + }, [props]); + useEffect(() => { + const unsubscribe = addBroadcastPickerListener(event => { + event === 'dismissed' ? props?.onBroadcastModalDismissed?.() : props?.onBroadcastModalShown?.(); + }); + return unsubscribe; + }, [props]); + return { + isRecording + }; +}; +//# sourceMappingURL=useGlobalRecording.js.map \ No newline at end of file diff --git a/lib/module/hooks/useGlobalRecording.js.map b/lib/module/hooks/useGlobalRecording.js.map new file mode 100644 index 0000000..0f55c66 --- /dev/null +++ b/lib/module/hooks/useGlobalRecording.js.map @@ -0,0 +1 @@ +{"version":3,"names":["useState","useEffect","addBroadcastPickerListener","addScreenRecordingListener","retrieveLastGlobalRecording","delay","ms","Promise","resolve","setTimeout","useGlobalRecording","props","isRecording","setIsRecording","unsubscribe","ignoreRecordingsInitiatedElsewhere","listener","event","type","reason","onRecordingStarted","settledTimeMs","file","onRecordingFinished","onBroadcastModalDismissed","onBroadcastModalShown"],"sourceRoot":"../../../src","sources":["hooks/useGlobalRecording.ts"],"mappings":";;AAAA,SAASA,QAAQ,EAAEC,SAAS,QAAQ,OAAO;AAC3C,SACEC,0BAA0B,EAC1BC,0BAA0B,EAC1BC,2BAA2B,QACtB,cAAc;AAGrB;AACA;AACA;AACA;AACA;AACA,MAAMC,KAAK,GAAIC,EAAU,IACvB,IAAIC,OAAO,CAAEC,OAAO,IAAKC,UAAU,CAACD,OAAO,EAAgBF,EAAE,CAAC,CAAC;;AAEjE;AACA;AACA;;AAqCA;AACA;AACA;;AASA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMI,kBAAkB,GAC7BC,KAAgC,IACF;EAC9B,MAAM,CAACC,WAAW,EAAEC,cAAc,CAAC,GAAGb,QAAQ,CAAC,KAAK,CAAC;EAErDC,SAAS,CAAC,MAAM;IACd,MAAMa,WAAW,GAAGX,0BAA0B,CAAC;MAC7CY,kCAAkC,EAChCJ,KAAK,EAAEI,kCAAkC,IAAI,KAAK;MACpDC,QAAQ,EAAE,MAAOC,KAAK,IAAK;QACzB,IAAIA,KAAK,CAACC,IAAI,KAAK,WAAW,EAAE;QAEhC,IAAID,KAAK,CAACE,MAAM,KAAK,OAAO,EAAE;UAC5BN,cAAc,CAAC,IAAI,CAAC;UACpBF,KAAK,EAAES,kBAAkB,GAAG,CAAC;QAC/B,CAAC,MAAM;UACLP,cAAc,CAAC,KAAK,CAAC;UACrB;UACA;UACA,MAAMR,KAAK,CAACM,KAAK,EAAEU,aAAa,IAAI,GAAG,CAAC;UACxC,MAAMC,IAAI,GAAGlB,2BAA2B,CAAC,CAAC;UAC1CO,KAAK,EAAEY,mBAAmB,GAAGD,IAAI,CAAC;QACpC;MACF;IACF,CAAC,CAAC;IAEF,OAAOR,WAAW;EACpB,CAAC,EAAE,CAACH,KAAK,CAAC,CAAC;EAEXV,SAAS,CAAC,MAAM;IACd,MAAMa,WAAW,GAAGZ,0BAA0B,CAAEe,KAAK,IAAK;MACxDA,KAAK,KAAK,WAAW,GACjBN,KAAK,EAAEa,yBAAyB,GAAG,CAAC,GACpCb,KAAK,EAAEc,qBAAqB,GAAG,CAAC;IACtC,CAAC,CAAC;IAEF,OAAOX,WAAW;EACpB,CAAC,EAAE,CAACH,KAAK,CAAC,CAAC;EAEX,OAAO;IAAEC;EAAY,CAAC;AACxB,CAAC","ignoreList":[]} diff --git a/lib/module/index.js b/lib/module/index.js new file mode 100644 index 0000000..4fa079e --- /dev/null +++ b/lib/module/index.js @@ -0,0 +1,6 @@ +"use strict"; + +export * from './types'; +export * from './functions'; +export * from './hooks'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/module/index.js.map b/lib/module/index.js.map new file mode 100644 index 0000000..d5ebc9a --- /dev/null +++ b/lib/module/index.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,cAAc,SAAS;AACvB,cAAc,aAAa;AAC3B,cAAc,SAAS","ignoreList":[]} diff --git a/lib/module/types.js b/lib/module/types.js new file mode 100644 index 0000000..2f0e414 --- /dev/null +++ b/lib/module/types.js @@ -0,0 +1,2 @@ +"use strict"; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/lib/module/types.js.map b/lib/module/types.js.map new file mode 100644 index 0000000..d9cdba6 --- /dev/null +++ b/lib/module/types.js.map @@ -0,0 +1 @@ +{"version":3,"names":[],"sourceRoot":"../../src","sources":["types.ts"],"mappings":"","ignoreList":[]} diff --git a/lib/typescript/NitroScreenRecorder.nitro.d.ts b/lib/typescript/NitroScreenRecorder.nitro.d.ts new file mode 100644 index 0000000..bc37797 --- /dev/null +++ b/lib/typescript/NitroScreenRecorder.nitro.d.ts @@ -0,0 +1,32 @@ +import type { HybridObject } from 'react-native-nitro-modules'; +import type { CameraDevice, RecorderCameraStyle, PermissionResponse, ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, RecordingError, BroadcastPickerPresentationEvent } from './types'; +/** + * ============================================================================ + * NOTES WITH NITRO-MODULES + * ============================================================================ + * After any change to this file, you have to run + * `yarn prepare` in the root project folder. This + * uses `npx expo prebuild --clean` under the hood + * + */ +export interface NitroScreenRecorder extends HybridObject<{ + ios: 'swift'; + android: 'kotlin'; +}> { + getCameraPermissionStatus(): PermissionStatus; + getMicrophonePermissionStatus(): PermissionStatus; + requestCameraPermission(): Promise; + requestMicrophonePermission(): Promise; + addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere: boolean, callback: (event: ScreenRecordingEvent) => void): number; + removeScreenRecordingListener(id: number): void; + addBroadcastPickerListener(callback: (event: BroadcastPickerPresentationEvent) => void): number; + removeBroadcastPickerListener(id: number): void; + startInAppRecording(enableMic: boolean, enableCamera: boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: boolean, onRecordingFinished: (file: ScreenRecordingFile) => void): void; + stopInAppRecording(): Promise; + cancelInAppRecording(): Promise; + startGlobalRecording(enableMic: boolean, separateAudioFile: boolean, onRecordingError: (error: RecordingError) => void): void; + stopGlobalRecording(settledTimeMs: number): Promise; + retrieveLastGlobalRecording(): ScreenRecordingFile | undefined; + clearRecordingCache(): void; +} +//# sourceMappingURL=NitroScreenRecorder.nitro.d.ts.map \ No newline at end of file diff --git a/lib/typescript/NitroScreenRecorder.nitro.d.ts.map b/lib/typescript/NitroScreenRecorder.nitro.d.ts.map new file mode 100644 index 0000000..0c9489d --- /dev/null +++ b/lib/typescript/NitroScreenRecorder.nitro.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"NitroScreenRecorder.nitro.d.ts","sourceRoot":"","sources":["../../src/NitroScreenRecorder.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAEjB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAKzD,yBAAyB,IAAI,gBAAgB,CAAC;IAC9C,6BAA6B,IAAI,gBAAgB,CAAC;IAClD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAM3D,0BAA0B,CACxB,kCAAkC,EAAE,OAAO,EAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAC9C,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,0BAA0B,CACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAMhD,mBAAmB,CACjB,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,OAAO,EACrB,kBAAkB,EAAE,mBAAmB,EACvC,YAAY,EAAE,YAAY,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,GAEvD,IAAI,CAAC;IACR,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC/D,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAMtC,oBAAoB,CAClB,SAAS,EAAE,OAAO,EAClB,iBAAiB,EAAE,OAAO,EAC1B,gBAAgB,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAChD,IAAI,CAAC;IACR,mBAAmB,CACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC5C,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAM/D,mBAAmB,IAAI,IAAI,CAAC;CAC7B"} \ No newline at end of file diff --git a/lib/typescript/expo-plugin/@types.d.ts b/lib/typescript/expo-plugin/@types.d.ts new file mode 100644 index 0000000..cab87a6 --- /dev/null +++ b/lib/typescript/expo-plugin/@types.d.ts @@ -0,0 +1,58 @@ +export interface ConfigProps { + /** + * Whether to enable camera permission for screen recording with camera overlay. + * + * @platform iOS + * @default true + * @example true + */ + enableCameraPermission?: boolean; + /** + * Camera permission description text displayed in iOS permission dialog. + * This text explains why the app needs camera access for screen recording features. + * + * @platform iOS + * @default "Allow $(PRODUCT_NAME) to access your camera for screen recording with camera overlay" + * @example "This app needs camera access to include your camera feed in screen recordings" + */ + cameraPermissionText?: string; + /** + * Whether to enable microphone permission for screen recording with audio capture. + * + * @platform iOS, Android + * @default true + * @example false + */ + enableMicrophonePermission?: boolean; + /** + * Microphone permission description text displayed in iOS permission dialog. + * This text explains why the app needs microphone access for audio recording. + * + * @platform iOS + * @default "Allow $(PRODUCT_NAME) to access your microphone for screen recording with audio" + * @example "This app needs microphone access to record audio during screen capture" + */ + microphonePermissionText?: string; + /** + * Provies a means for customizing the ios broadcast extension target name. + * @default: `BroadcastExtension` + */ + iosBroadcastExtensionTargetName?: string; + /** + * Provies a means for customizing your app group identifier. + */ + iosAppGroupIdentifier?: string; + /** + * Provies a means for customizing the ios broadcast extension bundle identifier. + */ + iosExtensionBundleIdentifier?: string; + /** + * Whether to display detailed plugin logs during the build process. + * Useful for debugging configuration issues during development. + * + * @platform iOS, Android + * @default false + * @example true + */ + showPluginLogs?: boolean; +} diff --git a/lib/typescript/expo-plugin/@types.js b/lib/typescript/expo-plugin/@types.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/lib/typescript/expo-plugin/@types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/typescript/expo-plugin/android/withAndroidScreenRecording.d.ts b/lib/typescript/expo-plugin/android/withAndroidScreenRecording.d.ts new file mode 100644 index 0000000..3d12296 --- /dev/null +++ b/lib/typescript/expo-plugin/android/withAndroidScreenRecording.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +export declare const withAndroidScreenRecording: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/android/withAndroidScreenRecording.js b/lib/typescript/expo-plugin/android/withAndroidScreenRecording.js new file mode 100644 index 0000000..ec865bc --- /dev/null +++ b/lib/typescript/expo-plugin/android/withAndroidScreenRecording.js @@ -0,0 +1,212 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withAndroidScreenRecording = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const withAndroidScreenRecording = (config) => { + // Add permissions and services to AndroidManifest.xml + config = (0, config_plugins_1.withAndroidManifest)(config, (mod) => { + var _a; + ScreenRecorderLog_1.ScreenRecorderLog.log('Adding screen recording permissions and services to AndroidManifest.xml'); + const androidManifest = mod.modResults; + if (!((_a = androidManifest.manifest.application) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Cannot find in AndroidManifest.xml'); + } + const application = androidManifest.manifest.application[0]; + if (!application.service) { + application.service = []; + } + // Add only the Global ScreenRecordingService + const serviceName = 'com.margelo.nitro.nitroscreenrecorder.ScreenRecordingService'; + const existingService = application.service.find((service) => { var _a; return ((_a = service.$) === null || _a === void 0 ? void 0 : _a['android:name']) === serviceName; }); + if (!existingService) { + application.service.push({ + $: { + 'android:name': serviceName, + 'android:enabled': 'true', + 'android:exported': 'false', + 'android:foregroundServiceType': 'mediaProjection', + }, + }); + ScreenRecorderLog_1.ScreenRecorderLog.log(`โœ… Added Global ScreenRecordingService to AndroidManifest.xml`); + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log(`โ„น๏ธ Global ScreenRecordingService already exists in AndroidManifest.xml`); + } + return mod; + }); + // Modify MainActivity to handle activity results (still needed for Global Recording) + config = (0, config_plugins_1.withMainActivity)(config, (mod) => { + ScreenRecorderLog_1.ScreenRecorderLog.log('Modifying MainActivity for screen recording activity results'); + const { modResults } = mod; + let mainActivityContent = modResults.contents; + const isKotlin = mainActivityContent.includes('class MainActivity') && + (mainActivityContent.includes('override fun') || + mainActivityContent.includes('kotlin')); + if (isKotlin) { + mainActivityContent = + addKotlinScreenRecordingSupport(mainActivityContent); + } + else { + mainActivityContent = addJavaScreenRecordingSupport(mainActivityContent); + } + modResults.contents = mainActivityContent; + return mod; + }); + return config; +}; +exports.withAndroidScreenRecording = withAndroidScreenRecording; +// This function remains unchanged as it's still needed for Global Recording +function addKotlinScreenRecordingSupport(content) { + // Required imports + const requiredImports = [ + 'import com.margelo.nitro.nitroscreenrecorder.NitroScreenRecorder', + 'import android.content.Intent', + 'import android.util.Log', + ]; + // Add imports if not present + requiredImports.forEach((importStatement) => { + if (!content.includes(importStatement)) { + const importRegex = /(import\s+.*\n)/g; + let lastImportMatch; + let match; + while ((match = importRegex.exec(content)) !== null) { + lastImportMatch = match; + } + if (lastImportMatch) { + const insertPosition = lastImportMatch.index + lastImportMatch[0].length; + content = + content.slice(0, insertPosition) + + importStatement + + '\n' + + content.slice(insertPosition); + } + } + }); + // Add onActivityResult method if not present + if (!content.includes('onActivityResult')) { + const classEndRegex = /(\s*)\}(\s*)$/; + const match = content.match(classEndRegex); + if (match && match.index !== undefined) { + const onActivityResultMethod = ` + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + Log.d("MainActivity", "onActivityResult: requestCode=$requestCode, resultCode=$resultCode") + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data) + } catch (e: Exception) { + Log.e("MainActivity", "Error handling activity result: \${e.message}") + e.printStackTrace() + } + } +`; + const insertPosition = match.index; + content = + content.slice(0, insertPosition) + + onActivityResultMethod + + content.slice(insertPosition); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added onActivityResult method to Kotlin MainActivity'); + } + } + else { + if (!content.includes('NitroScreenRecorder.handleActivityResult')) { + const onActivityResultRegex = /(override\s+fun\s+onActivityResult\s*\([^)]*\)\s*\{[^}]*)(super\.onActivityResult[^}]*)/; + const match = content.match(onActivityResultRegex); + if (match && match[1] && match[2]) { + const screenRecordingHandler = ` + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data) + } catch (e: Exception) { + Log.e("MainActivity", "Error handling activity result: \${e.message}") + e.printStackTrace() + }`; + content = content.replace(onActivityResultRegex, match[1] + match[2] + screenRecordingHandler); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added screen recording handler to existing onActivityResult method'); + } + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log('โ„น๏ธ Screen recording handler already exists in onActivityResult method'); + } + } + return content; +} +// This function remains unchanged as it's still needed for Global Recording +function addJavaScreenRecordingSupport(content) { + const requiredImports = [ + 'import android.content.Intent;', + 'import com.margelo.nitro.nitroscreenrecorder.NitroScreenRecorder;', + 'import android.util.Log;', + ]; + requiredImports.forEach((importStatement) => { + if (!content.includes(importStatement)) { + const importRegex = /(import\s+.*;\s*\n)/g; + let lastImportMatch; + let match; + while ((match = importRegex.exec(content)) !== null) { + lastImportMatch = match; + } + if (lastImportMatch) { + const insertPosition = lastImportMatch.index + lastImportMatch[0].length; + content = + content.slice(0, insertPosition) + + importStatement + + '\n' + + content.slice(insertPosition); + } + } + }); + if (!content.includes('onActivityResult')) { + const classEndRegex = /(\s*)\}(\s*)$/; + const match = content.match(classEndRegex); + if (match && match.index !== undefined) { + const onActivityResultMethod = ` + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.d("MainActivity", "onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode); + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data); + } catch (Exception e) { + Log.e("MainActivity", "Error handling activity result: " + e.getMessage()); + e.printStackTrace(); + } + } +`; + const insertPosition = match.index; + content = + content.slice(0, insertPosition) + + onActivityResultMethod + + content.slice(insertPosition); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added onActivityResult method to Java MainActivity'); + } + } + else { + if (!content.includes('NitroScreenRecorder.handleActivityResult')) { + const onActivityResultRegex = /(@Override\s+public\s+void\s+onActivityResult\s*\([^)]*\)\s*\{[^}]*)(super\.onActivityResult[^}]*)/; + const match = content.match(onActivityResultRegex); + if (match && match[1] && match[2]) { + const screenRecordingHandler = ` + + try { + // Handle screen recording activity results + NitroScreenRecorder.handleActivityResult(requestCode, resultCode, data); + } catch (Exception e) { + Log.e("MainActivity", "Error handling activity result: " + e.getMessage()); + e.printStackTrace(); + }`; + content = content.replace(onActivityResultRegex, match[1] + match[2] + screenRecordingHandler); + ScreenRecorderLog_1.ScreenRecorderLog.log('โœ… Added screen recording handler to existing onActivityResult method'); + } + } + else { + ScreenRecorderLog_1.ScreenRecorderLog.log('โ„น๏ธ Screen recording handler already exists in onActivityResult method'); + } + } + return content; +} diff --git a/lib/typescript/expo-plugin/eas/getEasManagedCredentials.d.ts b/lib/typescript/expo-plugin/eas/getEasManagedCredentials.d.ts new file mode 100644 index 0000000..2d00d2d --- /dev/null +++ b/lib/typescript/expo-plugin/eas/getEasManagedCredentials.d.ts @@ -0,0 +1,5 @@ +import type { ConfigProps } from '../@types'; +import type { ExpoConfig } from '@expo/config-types'; +export default function getEasManagedCredentialsConfigExtra(config: ExpoConfig, props: ConfigProps): { + [k: string]: any; +}; diff --git a/lib/typescript/expo-plugin/eas/getEasManagedCredentials.js b/lib/typescript/expo-plugin/eas/getEasManagedCredentials.js new file mode 100644 index 0000000..f427f48 --- /dev/null +++ b/lib/typescript/expo-plugin/eas/getEasManagedCredentials.js @@ -0,0 +1,43 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.default = getEasManagedCredentialsConfigExtra; +const iosConstants_1 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +function getEasManagedCredentialsConfigExtra(config, props) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; + const providedExtensionBundleId = !!props.iosExtensionBundleIdentifier; + if (!providedExtensionBundleId && !((_a = config.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier)) { + (0, assert_1.default)((_b = config.ios) === null || _b === void 0 ? void 0 : _b.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + } + const extensionTargetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + return { + ...config.extra, + eas: { + ...(_c = config.extra) === null || _c === void 0 ? void 0 : _c.eas, + build: { + ...(_e = (_d = config.extra) === null || _d === void 0 ? void 0 : _d.eas) === null || _e === void 0 ? void 0 : _e.build, + experimental: { + ...(_h = (_g = (_f = config.extra) === null || _f === void 0 ? void 0 : _f.eas) === null || _g === void 0 ? void 0 : _g.build) === null || _h === void 0 ? void 0 : _h.experimental, + ios: { + ...(_m = (_l = (_k = (_j = config.extra) === null || _j === void 0 ? void 0 : _j.eas) === null || _k === void 0 ? void 0 : _k.build) === null || _l === void 0 ? void 0 : _l.experimental) === null || _m === void 0 ? void 0 : _m.ios, + appExtensions: [ + ...((_t = (_s = (_r = (_q = (_p = (_o = config.extra) === null || _o === void 0 ? void 0 : _o.eas) === null || _p === void 0 ? void 0 : _p.build) === null || _q === void 0 ? void 0 : _q.experimental) === null || _r === void 0 ? void 0 : _r.ios) === null || _s === void 0 ? void 0 : _s.appExtensions) !== null && _t !== void 0 ? _t : []), + { + targetName: extensionTargetName, + bundleIdentifier: (0, iosConstants_1.getBroadcastExtensionBundleIdentifier)((_u = config === null || config === void 0 ? void 0 : config.ios) === null || _u === void 0 ? void 0 : _u.bundleIdentifier, props), + entitlements: { + 'com.apple.security.application-groups': [ + (0, iosConstants_1.getAppGroup)((_v = config === null || config === void 0 ? void 0 : config.ios) === null || _v === void 0 ? void 0 : _v.bundleIdentifier, props), + ], + }, + }, + ], + }, + }, + }, + }, + }; +} diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtension.d.ts b/lib/typescript/expo-plugin/ios/withBroadcastExtension.d.ts new file mode 100644 index 0000000..454f5a7 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtension.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +export declare const withBroadcastExtension: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtension.js b/lib/typescript/expo-plugin/ios/withBroadcastExtension.js new file mode 100644 index 0000000..ed15dec --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtension.js @@ -0,0 +1,27 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtension = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +// Local helpers / subโ€‘mods โ–ถ๏ธ +const withMainAppAppGroupInfoPlist_1 = require("./withMainAppAppGroupInfoPlist"); +const withMainAppAppGroupEntitlement_1 = require("./withMainAppAppGroupEntitlement"); +const withBroadcastExtensionFiles_1 = require("./withBroadcastExtensionFiles"); +const withBroadcastExtensionXcodeProject_1 = require("./withBroadcastExtensionXcodeProject"); +const withBroadcastExtensionPodfile_1 = require("./withBroadcastExtensionPodfile"); +const withEasManagedCredentials_1 = require("./withEasManagedCredentials"); +const withMainAppEntitlementsFile_1 = require("./withMainAppEntitlementsFile"); +const withBroadcastExtension = (config, props) => { + return (0, config_plugins_1.withPlugins)(config, [ + /** Mainโ€‘app tweaks */ + [withMainAppAppGroupInfoPlist_1.withMainAppAppGroupInfoPlist, props], + [withMainAppEntitlementsFile_1.withMainAppEntitlementsFile, props], + [withMainAppAppGroupEntitlement_1.withMainAppAppGroupEntitlement, props], + /** Broadcast extension target */ + [withBroadcastExtensionFiles_1.withBroadcastExtensionFiles, props], + [withBroadcastExtensionXcodeProject_1.withBroadcastExtensionXcodeProject, props], + [withBroadcastExtensionPodfile_1.withBroadcastExtensionPodfile, props], + /** Extras for EAS build */ + [withEasManagedCredentials_1.withEasManagedCredentials, props], + ]); +}; +exports.withBroadcastExtension = withBroadcastExtension; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.d.ts b/lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.d.ts new file mode 100644 index 0000000..cc178b4 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.d.ts @@ -0,0 +1,8 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +/** + * Copies the ReplayKit Broadcast Upload Extension templates into the iOS + * project and patches them so their App Group + bundle versions match the + * host app. Mirrors OneSignal's NSE flow for consistency. + */ +export declare const withBroadcastExtensionFiles: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.js b/lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.js new file mode 100644 index 0000000..6a2745b --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtensionFiles.js @@ -0,0 +1,89 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionFiles = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +const iosConstants_1 = require("../support/iosConstants"); +const FileManager_1 = require("../support/FileManager"); +const BEUpdateManager_1 = __importDefault(require("../support/BEUpdateManager")); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const SAMPLE_HANDLER_FILE = 'SampleHandler.swift'; +/** + * Copies the ReplayKit Broadcast Upload Extension templates into the iOS + * project and patches them so their App Group + bundle versions match the + * host app. Mirrors OneSignal's NSE flow for consistency. + */ +const withBroadcastExtensionFiles = (config, props) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + async (mod) => { + var _a, _b, _c, _d; + const iosPath = path.join(mod.modRequest.projectRoot, 'ios'); + const targetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + const sourceDir = path.join(__dirname, '..', 'support', 'broadcastExtensionFiles'); + fs.mkdirSync(`${iosPath}/${targetName}`, { + recursive: true, + }); + for (const extFile of iosConstants_1.BROADCAST_EXT_ALL_FILES) { + const targetFile = `${iosPath}/${targetName}/${extFile}`; + await FileManager_1.FileManager.copyFile(`${sourceDir}/${extFile}`, targetFile); + } + const sourceSamplePath = `${sourceDir}/${SAMPLE_HANDLER_FILE}`; + const targetSamplePath = `${iosPath}/${targetName}/${SAMPLE_HANDLER_FILE}`; + await FileManager_1.FileManager.copyFile(sourceSamplePath, targetSamplePath); + ScreenRecorderLog_1.ScreenRecorderLog.log(`Copied broadcast extension files to ${iosPath}/${targetName}`); + /* ------------------------------------------------------------ */ + /* 2๏ธโƒฃ Patch entitlements & Info.plist placeholders */ + /* ------------------------------------------------------------ */ + const updater = new BEUpdateManager_1.default(iosPath, props); + const mainAppBundleId = (_a = mod.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + if (!mainAppBundleId) { + throw new Error('Failed to find main app bundle id!'); + } + const groupIdentifier = (0, iosConstants_1.getAppGroup)(mainAppBundleId, props); + await updater.updateEntitlements(groupIdentifier); + await updater.updateInfoPlist((_c = (_b = mod.ios) === null || _b === void 0 ? void 0 : _b.buildNumber) !== null && _c !== void 0 ? _c : iosConstants_1.DEFAULT_BUNDLE_VERSION, groupIdentifier); + await updater.updateBundleShortVersion((_d = mod.version) !== null && _d !== void 0 ? _d : iosConstants_1.DEFAULT_BUNDLE_SHORT_VERSION); + ScreenRecorderLog_1.ScreenRecorderLog.log('Patched broadcast extension entitlements and Info.plist with app group and version values.'); + return mod; + }, + ]); +}; +exports.withBroadcastExtensionFiles = withBroadcastExtensionFiles; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts b/lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts new file mode 100644 index 0000000..94c0564 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +export declare const withBroadcastExtensionPodfile: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.js b/lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.js new file mode 100644 index 0000000..6b6e659 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtensionPodfile.js @@ -0,0 +1,21 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionPodfile = void 0; +const path_1 = __importDefault(require("path")); +const config_plugins_1 = require("@expo/config-plugins"); +const updatePodfile_1 = require("../support/updatePodfile"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const withBroadcastExtensionPodfile = (config, props) => { + return (0, config_plugins_1.withDangerousMod)(config, [ + 'ios', + async (mod) => { + const iosRoot = path_1.default.join(mod.modRequest.projectRoot, 'ios'); + await (0, updatePodfile_1.updatePodfile)(iosRoot, props).catch(ScreenRecorderLog_1.ScreenRecorderLog.error); + return mod; + }, + ]); +}; +exports.withBroadcastExtensionPodfile = withBroadcastExtensionPodfile; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts b/lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts new file mode 100644 index 0000000..583a769 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +export declare const withBroadcastExtensionXcodeProject: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.js b/lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.js new file mode 100644 index 0000000..d1561f2 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withBroadcastExtensionXcodeProject.js @@ -0,0 +1,139 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withBroadcastExtensionXcodeProject = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +const assert_1 = __importDefault(require("assert")); +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Helper: pull DEVELOPMENT_TEAM from the main-app targetโ€™s build settings +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function getMainAppDevelopmentTeam(pbx, l) { + var _a, _b; + const configs = pbx.pbxXCBuildConfigurationSection(); + for (const key in configs) { + const config = configs[key]; + const bs = config.buildSettings; + if (!bs || !bs.PRODUCT_NAME) + continue; + const productName = (_a = bs.PRODUCT_NAME) === null || _a === void 0 ? void 0 : _a.replace(/"/g, ''); + // Ignore other extensions/widgets + if (productName && + (productName.includes('Extension') || productName.includes('Widget'))) { + continue; + } + const developmentTeam = (_b = bs.DEVELOPMENT_TEAM) === null || _b === void 0 ? void 0 : _b.replace(/"/g, ''); + if (developmentTeam) { + l.log(`Found DEVELOPMENT_TEAM='${developmentTeam}' from main app configuration.`); + return developmentTeam; + } + } + l.error('No DEVELOPMENT_TEAM found in main app build settings. Developer will need to manually add Dev Team.'); + return null; +} +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Main Expo config-plugin +//โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +const withBroadcastExtensionXcodeProject = (config, props) => { + return (0, config_plugins_1.withXcodeProject)(config, (newConfig) => { + var _a, _b, _c, _d; + const xcodeProject = newConfig.modResults; + const extensionTargetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + const appIdentifier = (_a = newConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + (0, assert_1.default)(appIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const bundleIdentifier = (0, iosConstants_1.getBroadcastExtensionBundleIdentifier)(appIdentifier, props); + /* ------------------------------------------------------------------ */ + /* 0. Resolve DEVELOPMENT_TEAM (props override > auto-detect > none) */ + /* ------------------------------------------------------------------ */ + const detectedDevTeam = getMainAppDevelopmentTeam(xcodeProject, ScreenRecorderLog_1.ScreenRecorderLog); + const devTeam = detectedDevTeam !== null && detectedDevTeam !== void 0 ? detectedDevTeam : undefined; + /* ------------------------------------------------------------------ */ + /* 1. Bail out early if target/group already exist */ + /* ------------------------------------------------------------------ */ + const existingTarget = xcodeProject.pbxTargetByName(extensionTargetName); + if (existingTarget) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${extensionTargetName} already exists in project. Skippingโ€ฆ`); + return newConfig; + } + const existingGroups = xcodeProject.hash.project.objects.PBXGroup; + const groupExists = Object.values(existingGroups).some((group) => group && group.name === extensionTargetName); + if (groupExists) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${extensionTargetName} group already exists in project. Skippingโ€ฆ`); + return newConfig; + } + /* ------------------------------------------------------------------ */ + /* 2. Create target, group & build phases (COMBINED APPROACH) */ + /* ------------------------------------------------------------------ */ + const pbx = xcodeProject; + // 2.1 Create PBXGroup for the extension (OneSignal style - single group creation) + const extGroup = pbx.addPbxGroup(iosConstants_1.BROADCAST_EXT_ALL_FILES, extensionTargetName, extensionTargetName); + // 2.2 Add the new PBXGroup to the top level group + const groups = pbx.hash.project.objects.PBXGroup; + Object.keys(groups).forEach(function (key) { + if (typeof groups[key] === 'object' && + groups[key].name === undefined && + groups[key].path === undefined) { + pbx.addToPbxGroup(extGroup.uuid, key); + } + }); + // 2.3 WORK AROUND for addTarget BUG (from OneSignal) + // Xcode projects don't contain these if there is only one target + const projObjects = pbx.hash.project.objects; + projObjects.PBXTargetDependency = projObjects.PBXTargetDependency || {}; + projObjects.PBXContainerItemProxy = projObjects.PBXContainerItemProxy || {}; + // 2.4 Create native target + const target = pbx.addTarget(extensionTargetName, 'app_extension', extensionTargetName); + // 2.5 Add build phases to the new target (OneSignal approach) + pbx.addBuildPhase(iosConstants_1.BROADCAST_EXT_SOURCE_FILES, // Add source files directly to the build phase + 'PBXSourcesBuildPhase', 'Sources', target.uuid); + pbx.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid); + pbx.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid); + // 2.6 Link ReplayKit + pbx.addFramework('ReplayKit.framework', { + target: target.uuid, + sourceTree: 'SDKROOT', + link: true, + }); + /* ------------------------------------------------------------------ */ + /* 3. Build-settings tweaks */ + /* ------------------------------------------------------------------ */ + const configurations = xcodeProject.pbxXCBuildConfigurationSection(); + for (const key in configurations) { + const cfg = configurations[key]; + const b = cfg.buildSettings; + if (!b) + continue; + if (b.PRODUCT_NAME === `"${extensionTargetName}"`) { + b.CLANG_ENABLE_MODULES = 'YES'; + b.INFOPLIST_FILE = `"${extensionTargetName}/BroadcastExtension-Info.plist"`; + b.CODE_SIGN_ENTITLEMENTS = `"${extensionTargetName}/BroadcastExtension.entitlements"`; + b.CODE_SIGN_STYLE = 'Automatic'; + b.CURRENT_PROJECT_VERSION = + (_c = (_b = newConfig.ios) === null || _b === void 0 ? void 0 : _b.buildNumber) !== null && _c !== void 0 ? _c : iosConstants_1.DEFAULT_BUNDLE_VERSION; + b.MARKETING_VERSION = (_d = newConfig.version) !== null && _d !== void 0 ? _d : iosConstants_1.DEFAULT_BUNDLE_SHORT_VERSION; + b.PRODUCT_BUNDLE_IDENTIFIER = `"${bundleIdentifier}"`; + b.SWIFT_VERSION = '5.0'; + b.SWIFT_EMIT_LOC_STRINGS = 'YES'; + b.SWIFT_OBJC_BRIDGING_HEADER = `"${extensionTargetName}/BroadcastExtension-Bridging-Header.h"`; + b.HEADER_SEARCH_PATHS = `"$(SRCROOT)/${extensionTargetName}"`; + b.TARGETED_DEVICE_FAMILY = iosConstants_1.TARGETED_DEVICE_FAMILY; + if (devTeam) + b.DEVELOPMENT_TEAM = devTeam; + } + } + /* ------------------------------------------------------------------ */ + /* 4. Apply DevelopmentTeam to both targets */ + /* ------------------------------------------------------------------ */ + if (devTeam) { + xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam); + const broadcastTarget = xcodeProject.pbxTargetByName(extensionTargetName); + xcodeProject.addTargetAttribute('DevelopmentTeam', devTeam, broadcastTarget); + } + ScreenRecorderLog_1.ScreenRecorderLog.log(`Successfully created ${extensionTargetName} target with files`); + return newConfig; + }); +}; +exports.withBroadcastExtensionXcodeProject = withBroadcastExtensionXcodeProject; diff --git a/lib/typescript/expo-plugin/ios/withEasManagedCredentials.d.ts b/lib/typescript/expo-plugin/ios/withEasManagedCredentials.d.ts new file mode 100644 index 0000000..bef053e --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withEasManagedCredentials.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +export declare const withEasManagedCredentials: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withEasManagedCredentials.js b/lib/typescript/expo-plugin/ios/withEasManagedCredentials.js new file mode 100644 index 0000000..d89e837 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withEasManagedCredentials.js @@ -0,0 +1,12 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withEasManagedCredentials = void 0; +const getEasManagedCredentials_1 = __importDefault(require("../eas/getEasManagedCredentials")); +const withEasManagedCredentials = (config, props) => { + config.extra = (0, getEasManagedCredentials_1.default)(config, props); + return config; +}; +exports.withEasManagedCredentials = withEasManagedCredentials; diff --git a/lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts b/lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts new file mode 100644 index 0000000..e531f83 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.d.ts @@ -0,0 +1,6 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +/** + * Add "App Group" permission + */ +export declare const withMainAppAppGroupEntitlement: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.js b/lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.js new file mode 100644 index 0000000..0824d0a --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withMainAppAppGroupEntitlement.js @@ -0,0 +1,32 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppAppGroupEntitlement = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +/** + * Add "App Group" permission + */ +const withMainAppAppGroupEntitlement = (config, props) => { + const APP_GROUP_KEY = 'com.apple.security.application-groups'; + return (0, config_plugins_1.withEntitlementsPlist)(config, (newConfig) => { + var _a, _b; + // Ensure we have an array, preserving any existing entries + if (!Array.isArray(newConfig.modResults[APP_GROUP_KEY])) { + newConfig.modResults[APP_GROUP_KEY] = []; + } + const modResultsArray = newConfig.modResults[APP_GROUP_KEY]; + (0, assert_1.default)((_a = newConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const entitlement = (0, iosConstants_1.getAppGroup)((_b = newConfig === null || newConfig === void 0 ? void 0 : newConfig.ios) === null || _b === void 0 ? void 0 : _b.bundleIdentifier, props); + // Check if our entitlement already exists + if (modResultsArray.includes(entitlement)) { + return newConfig; + } + modResultsArray.push(entitlement); + return newConfig; + }); +}; +exports.withMainAppAppGroupEntitlement = withMainAppAppGroupEntitlement; diff --git a/lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts b/lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts new file mode 100644 index 0000000..9383b28 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.d.ts @@ -0,0 +1,3 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from '../@types'; +export declare const withMainAppAppGroupInfoPlist: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.js b/lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.js new file mode 100644 index 0000000..af3cf18 --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withMainAppAppGroupInfoPlist.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppAppGroupInfoPlist = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const iosConstants_1 = require("../support/iosConstants"); +const iosConstants_2 = require("../support/iosConstants"); +const assert_1 = __importDefault(require("assert")); +const withMainAppAppGroupInfoPlist = (config, props) => { + return (0, config_plugins_1.withInfoPlist)(config, (modConfig) => { + var _a; + const appIdentifier = (_a = modConfig.ios) === null || _a === void 0 ? void 0 : _a.bundleIdentifier; + (0, assert_1.default)(appIdentifier, "Missing 'ios.bundleIdentifier' in app config"); + const appGroup = (0, iosConstants_1.getAppGroup)(appIdentifier, props); + const broadcastExtensionBundleId = (0, iosConstants_2.getBroadcastExtensionBundleIdentifier)(appIdentifier, props); + modConfig.modResults.BroadcastExtensionAppGroupIdentifier = appGroup; + modConfig.modResults.BroadcastExtensionBundleIdentifier = + broadcastExtensionBundleId; + return modConfig; + }); +}; +exports.withMainAppAppGroupInfoPlist = withMainAppAppGroupInfoPlist; diff --git a/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.d.ts b/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.d.ts new file mode 100644 index 0000000..e46005b --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.d.ts @@ -0,0 +1,7 @@ +import { type ConfigPlugin } from '@expo/config-plugins'; +import { type ConfigProps } from '../@types'; +/** + * Add the main app's entitlements file to the Xcode project navigator + * This ensures the .entitlements file is visible in Xcode's file tree + */ +export declare const withMainAppEntitlementsFile: ConfigPlugin; diff --git a/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js b/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js new file mode 100644 index 0000000..e6da01b --- /dev/null +++ b/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js @@ -0,0 +1,98 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withMainAppEntitlementsFile = void 0; +const config_plugins_1 = require("@expo/config-plugins"); +const ScreenRecorderLog_1 = require("../support/ScreenRecorderLog"); +/** + * Add the main app's entitlements file to the Xcode project navigator + * This ensures the .entitlements file is visible in Xcode's file tree + */ +const withMainAppEntitlementsFile = (config) => { + return (0, config_plugins_1.withXcodeProject)(config, (newConfig) => { + const xcodeProject = newConfig.modResults; + const projectName = newConfig.name; + const entitlementsFileName = `${projectName}.entitlements`; + const entitlementsPath = `${projectName}/${entitlementsFileName}`; + // Check if the entitlements file is already added to the project + const files = xcodeProject.hash.project.objects.PBXFileReference; + const entitlementsFileExists = Object.values(files).some((file) => file && file.path === `"${entitlementsFileName}"`); + if (entitlementsFileExists) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`${entitlementsFileName} already exists in project. Skipping...`); + return newConfig; + } + // Find the main app group (try multiple approaches) + const groups = xcodeProject.hash.project.objects.PBXGroup; + let mainAppGroupKey = null; + // Debug: log all group names to understand the structure + ScreenRecorderLog_1.ScreenRecorderLog.log('Available groups:'); + for (const key in groups) { + const group = groups[key]; + if (group && group.name) { + ScreenRecorderLog_1.ScreenRecorderLog.log(` - ${group.name} (key: ${key})`); + } + } + // Try different variations of the project name + const searchNames = [ + `"${projectName}"`, // Quoted version + projectName, // Unquoted version + `"${projectName}/"`, // With trailing slash + `${projectName}/`, // Unquoted with trailing slash + ]; + for (const searchName of searchNames) { + for (const key in groups) { + const group = groups[key]; + if (group && group.name === searchName) { + mainAppGroupKey = key; + ScreenRecorderLog_1.ScreenRecorderLog.log(`Found main app group with name: ${searchName}`); + break; + } + } + if (mainAppGroupKey) + break; + } + // If still not found, try to find the group that contains AppDelegate or main source files + if (!mainAppGroupKey) { + ScreenRecorderLog_1.ScreenRecorderLog.log('Trying to find main app group by looking for AppDelegate...'); + for (const key in groups) { + const group = groups[key]; + if (group && group.children) { + // Check if this group contains typical main app files + const hasMainAppFiles = group.children.some((childKey) => { + var _a, _b, _c; + const file = files[childKey]; + return (file && + (((_a = file.path) === null || _a === void 0 ? void 0 : _a.includes('AppDelegate')) || + ((_b = file.path) === null || _b === void 0 ? void 0 : _b.includes('Info.plist')) || + ((_c = file.name) === null || _c === void 0 ? void 0 : _c.includes('AppDelegate')))); + }); + if (hasMainAppFiles) { + mainAppGroupKey = key; + ScreenRecorderLog_1.ScreenRecorderLog.log(`Found main app group by AppDelegate: ${group.name || 'unnamed'}`); + break; + } + } + } + } + if (!mainAppGroupKey) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Could not find main app group for ${projectName}. Available groups logged above.`); + return newConfig; + } + // Add the entitlements file to the project + try { + // Create the file reference + const fileRef = xcodeProject.addFile(entitlementsPath, mainAppGroupKey, { + lastKnownFileType: 'text.plist.entitlements', + defaultEncoding: 4, + target: undefined, + }); + if (fileRef) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Successfully added ${entitlementsFileName} to Xcode project navigator`); + } + } + catch (error) { + ScreenRecorderLog_1.ScreenRecorderLog.log(`Error adding entitlements file to project: ${error}`); + } + return newConfig; + }); +}; +exports.withMainAppEntitlementsFile = withMainAppEntitlementsFile; diff --git a/lib/typescript/expo-plugin/support/BEUpdateManager.d.ts b/lib/typescript/expo-plugin/support/BEUpdateManager.d.ts new file mode 100644 index 0000000..6b4f0a6 --- /dev/null +++ b/lib/typescript/expo-plugin/support/BEUpdateManager.d.ts @@ -0,0 +1,20 @@ +import { type ConfigProps } from '../@types'; +export default class BEUpdaterManager { + private extensionPath; + constructor(iosPath: string, props: ConfigProps); + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + updateEntitlements(groupIdentifier: string): Promise; + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + updateInfoPlist(version: string, groupIdentifier: string): Promise; + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + updateBundleShortVersion(version: string): Promise; +} diff --git a/lib/typescript/expo-plugin/support/BEUpdateManager.js b/lib/typescript/expo-plugin/support/BEUpdateManager.js new file mode 100644 index 0000000..1401c50 --- /dev/null +++ b/lib/typescript/expo-plugin/support/BEUpdateManager.js @@ -0,0 +1,47 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const FileManager_1 = require("./FileManager"); +const iosConstants_1 = require("./iosConstants"); +// project `ios/${BROADCAST_EXT_TARGET_NAME}` directory +const entitlementsFileName = `BroadcastExtension.entitlements`; +const plistFileName = `BroadcastExtension-Info.plist`; +class BEUpdaterManager { + constructor(iosPath, props) { + this.extensionPath = ''; + const targetName = (0, iosConstants_1.getBroadcastExtensionTargetName)(props); + this.extensionPath = `${iosPath}/${targetName}`; + } + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + async updateEntitlements(groupIdentifier) { + const entitlementsFilePath = `${this.extensionPath}/${entitlementsFileName}`; + let entitlementsFile = await FileManager_1.FileManager.readFile(entitlementsFilePath); + entitlementsFile = entitlementsFile.replace(iosConstants_1.GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + await FileManager_1.FileManager.writeFile(entitlementsFilePath, entitlementsFile); + } + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + async updateInfoPlist(version, groupIdentifier) { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager_1.FileManager.readFile(plistFilePath); + plistFile = plistFile + .replace(iosConstants_1.BUNDLE_VERSION_TEMPLATE_REGEX, version) + .replace(iosConstants_1.GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + await FileManager_1.FileManager.writeFile(plistFilePath, plistFile); + } + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + async updateBundleShortVersion(version) { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager_1.FileManager.readFile(plistFilePath); + plistFile = plistFile.replace(iosConstants_1.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, version); + await FileManager_1.FileManager.writeFile(plistFilePath, plistFile); + } +} +exports.default = BEUpdaterManager; diff --git a/lib/typescript/expo-plugin/support/BEUpdateManager.ts b/lib/typescript/expo-plugin/support/BEUpdateManager.ts new file mode 100644 index 0000000..6ee0411 --- /dev/null +++ b/lib/typescript/expo-plugin/support/BEUpdateManager.ts @@ -0,0 +1,68 @@ +import { type ConfigProps } from '../@types'; +import { FileManager } from './FileManager'; +import { + BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, + BUNDLE_VERSION_TEMPLATE_REGEX, + getBroadcastExtensionTargetName, + GROUP_IDENTIFIER_TEMPLATE_REGEX, +} from './iosConstants'; + +// project `ios/${BROADCAST_EXT_TARGET_NAME}` directory +const entitlementsFileName = `BroadcastExtension.entitlements`; +const plistFileName = `BroadcastExtension-Info.plist`; + +export default class BEUpdaterManager { + private extensionPath = ''; + + constructor(iosPath: string, props: ConfigProps) { + const targetName = getBroadcastExtensionTargetName(props); + this.extensionPath = `${iosPath}/${targetName}`; + } + + /** + * Injects the real App Group identifier into the entitlements file so the + * Broadcast Upload Extension can share storage with the main app. + */ + async updateEntitlements(groupIdentifier: string): Promise { + const entitlementsFilePath = `${this.extensionPath}/${entitlementsFileName}`; + let entitlementsFile = await FileManager.readFile(entitlementsFilePath); + + entitlementsFile = entitlementsFile.replace( + GROUP_IDENTIFIER_TEMPLATE_REGEX, + groupIdentifier + ); + + await FileManager.writeFile(entitlementsFilePath, entitlementsFile); + } + + /** + * Makes CFBundleVersion of the Broadcast Extension match the host appโ€™s + * build number to avoid Appย Store validation errors. + */ + async updateInfoPlist( + version: string, + groupIdentifier: string + ): Promise { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager.readFile(plistFilePath); + + plistFile = plistFile + .replace(BUNDLE_VERSION_TEMPLATE_REGEX, version) + .replace(GROUP_IDENTIFIER_TEMPLATE_REGEX, groupIdentifier); + + await FileManager.writeFile(plistFilePath, plistFile); + } + + /** + * Syncs CFBundleShortVersionString (marketing version) with the main app so + * TestFlight/Appย Store show a single coherent version. + */ + async updateBundleShortVersion(version: string): Promise { + const plistFilePath = `${this.extensionPath}/${plistFileName}`; + let plistFile = await FileManager.readFile(plistFilePath); + + plistFile = plistFile.replace(BUNDLE_SHORT_VERSION_TEMPLATE_REGEX, version); + + await FileManager.writeFile(plistFilePath, plistFile); + } +} diff --git a/lib/typescript/expo-plugin/support/FileManager.d.ts b/lib/typescript/expo-plugin/support/FileManager.d.ts new file mode 100644 index 0000000..3c4e3fb --- /dev/null +++ b/lib/typescript/expo-plugin/support/FileManager.d.ts @@ -0,0 +1,9 @@ +/** + * FileManager contains static *awaitable* file-system functions + */ +export declare class FileManager { + static readFile(path: string): Promise; + static writeFile(path: string, contents: string): Promise; + static copyFile(path1: string, path2: string): Promise; + static dirExists(path: string): boolean; +} diff --git a/lib/typescript/expo-plugin/support/FileManager.js b/lib/typescript/expo-plugin/support/FileManager.js new file mode 100644 index 0000000..9581566 --- /dev/null +++ b/lib/typescript/expo-plugin/support/FileManager.js @@ -0,0 +1,75 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FileManager = void 0; +const fs = __importStar(require("fs")); +const ScreenRecorderLog_1 = require("./ScreenRecorderLog"); +/** + * FileManager contains static *awaitable* file-system functions + */ +class FileManager { + static async readFile(path) { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err || !data) { + ScreenRecorderLog_1.ScreenRecorderLog.error("Couldn't read file:" + path); + reject(err); + return; + } + resolve(data); + }); + }); + } + static async writeFile(path, contents) { + return new Promise((resolve, reject) => { + fs.writeFile(path, contents, 'utf8', (err) => { + if (err) { + ScreenRecorderLog_1.ScreenRecorderLog.error("Couldn't write file:" + path); + reject(err); + return; + } + resolve(); + }); + }); + } + static async copyFile(path1, path2) { + const fileContents = await FileManager.readFile(path1); + await FileManager.writeFile(path2, fileContents); + } + static dirExists(path) { + return fs.existsSync(path); + } +} +exports.FileManager = FileManager; diff --git a/lib/typescript/expo-plugin/support/FileManager.ts b/lib/typescript/expo-plugin/support/FileManager.ts new file mode 100644 index 0000000..1d61d78 --- /dev/null +++ b/lib/typescript/expo-plugin/support/FileManager.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import { ScreenRecorderLog } from './ScreenRecorderLog'; + +/** + * FileManager contains static *awaitable* file-system functions + */ +export class FileManager { + static async readFile(path: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(path, 'utf8', (err, data) => { + if (err || !data) { + ScreenRecorderLog.error("Couldn't read file:" + path); + reject(err); + return; + } + resolve(data); + }); + }); + } + + static async writeFile(path: string, contents: string): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(path, contents, 'utf8', (err) => { + if (err) { + ScreenRecorderLog.error("Couldn't write file:" + path); + reject(err); + return; + } + resolve(); + }); + }); + } + + static async copyFile(path1: string, path2: string): Promise { + const fileContents = await FileManager.readFile(path1); + await FileManager.writeFile(path2, fileContents); + } + + static dirExists(path: string): boolean { + return fs.existsSync(path); + } +} diff --git a/lib/typescript/expo-plugin/support/ScreenRecorderLog.d.ts b/lib/typescript/expo-plugin/support/ScreenRecorderLog.d.ts new file mode 100644 index 0000000..051192d --- /dev/null +++ b/lib/typescript/expo-plugin/support/ScreenRecorderLog.d.ts @@ -0,0 +1,5 @@ +export declare class ScreenRecorderLog { + private static readonly PLUGIN; + static log(message: string, ...optional: any[]): void; + static error(message: string, ...optional: any[]): void; +} diff --git a/lib/typescript/expo-plugin/support/ScreenRecorderLog.js b/lib/typescript/expo-plugin/support/ScreenRecorderLog.js new file mode 100644 index 0000000..ae6080f --- /dev/null +++ b/lib/typescript/expo-plugin/support/ScreenRecorderLog.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ScreenRecorderLog = void 0; +class ScreenRecorderLog { + static log(message, ...optional) { + const green = '\x1b[32m'; + const reset = '\x1b[0m'; + console.log(`${green}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } + static error(message, ...optional) { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + console.error(`${red}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } +} +exports.ScreenRecorderLog = ScreenRecorderLog; +ScreenRecorderLog.PLUGIN = 'react-native-nitro-screen-recorder'; diff --git a/lib/typescript/expo-plugin/support/ScreenRecorderLog.ts b/lib/typescript/expo-plugin/support/ScreenRecorderLog.ts new file mode 100644 index 0000000..edc5325 --- /dev/null +++ b/lib/typescript/expo-plugin/support/ScreenRecorderLog.ts @@ -0,0 +1,15 @@ +export class ScreenRecorderLog { + private static readonly PLUGIN = 'react-native-nitro-screen-recorder'; + + static log(message: string, ...optional: any[]) { + const green = '\x1b[32m'; + const reset = '\x1b[0m'; + console.log(`${green}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } + + static error(message: string, ...optional: any[]) { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + console.error(`${red}[${this.PLUGIN}]${reset} ${message}`, ...optional); + } +} diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h new file mode 100644 index 0000000..187805c --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Bridging-Header.h @@ -0,0 +1 @@ +#import "BroadcastHelper.h" \ No newline at end of file diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist new file mode 100644 index 0000000..3bff812 --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleName + $(PRODUCT_NAME) + + CFBundleDisplayName + $(PRODUCT_NAME) + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + + CFBundleExecutable + $(EXECUTABLE_NAME) + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + CFBundleShortVersionString + $(MARKETING_VERSION) + + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SampleHandler + + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + + BroadcastExtensionAppGroupIdentifier + {{GROUP_IDENTIFIER}} + + \ No newline at end of file diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy new file mode 100644 index 0000000..73c00be --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension-PrivacyInfo.xcprivacy @@ -0,0 +1,26 @@ + + + + + NSPrivacy + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryScreenCapture + NSPrivacyAccessedAPITypeReason + User-initiated screen recording via ReplayKit + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryAudio + NSPrivacyAccessedAPITypeReason + User-initiated microphone capture in screen recording + + + NSPrivacyCollectedDataTypes + + + + diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements new file mode 100644 index 0000000..470ed66 --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + {{GROUP_IDENTIFIER}} + + + \ No newline at end of file diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h new file mode 100644 index 0000000..9830154 --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.h @@ -0,0 +1,7 @@ +#import + +/// Finishes a broadcast without triggering the โ€œerrorโ€ alert. +/// (RPBroadcastSampleHandlerโ€™s parameter is formally non-null, so we suppress +/// the compiler warning.) +/// Refer to https://mehmetbaykar.com/posts/how-to-gracefully-stop-a-broadcast-upload-extension/ +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull handler); \ No newline at end of file diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m new file mode 100644 index 0000000..0a67791 --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastHelper.m @@ -0,0 +1,8 @@ +#import "BroadcastHelper.h" + +void finishBroadcastGracefully(RPBroadcastSampleHandler * _Nonnull handler) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [handler finishBroadcastWithError:nil]; // โ† the magic line โœจ +#pragma clang diagnostic pop +} \ No newline at end of file diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift new file mode 100644 index 0000000..44d82b1 --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -0,0 +1,434 @@ +// MARK: Broadcast Writer + +// Copied from the repo: +// https://github.com/romiroma/BroadcastWriter + +import AVFoundation +import CoreGraphics +import Foundation +import ReplayKit + +extension AVAssetWriter.Status { + var description: String { + switch self { + case .cancelled: return "cancelled" + case .completed: return "completed" + case .failed: return "failed" + case .unknown: return "unknown" + case .writing: return "writing" + @unknown default: return "@unknown default" + } + } +} + +extension CGFloat { + var nsNumber: NSNumber { + return .init(value: native) + } +} + +extension Int { + var nsNumber: NSNumber { + return .init(value: self) + } +} + +enum Error: Swift.Error { + case wrongAssetWriterStatus(AVAssetWriter.Status) + case selfDeallocated +} + +public final class BroadcastWriter { + + private var assetWriterSessionStarted: Bool = false + private var audioAssetWriterSessionStarted: Bool = false + private let assetWriterQueue: DispatchQueue + private let assetWriter: AVAssetWriter + + // Separate audio writer + private var separateAudioWriter: AVAssetWriter? + private let separateAudioFile: Bool + private let audioOutputURL: URL? + + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in + let videoWidth = screenSize.width * screenScale + let videoHeight = screenSize.height * screenScale + + // Ensure encoder-friendly even dimensions + let w = (Int(videoWidth) / 2) * 2 + let h = (Int(videoHeight) / 2) * 2 + + // Decide codec: prefer HEVC when available + let hevcSupported: Bool = { + if #available(iOS 11.0, *) { + return self.assetWriter.canApply( + outputSettings: [AVVideoCodecKey: AVVideoCodecType.hevc], + forMediaType: .video + ) + } + return false + }() + + let codec: AVVideoCodecType = hevcSupported ? .hevc : .h264 + + var compressionProperties: [String: Any] = [ + AVVideoExpectedSourceFrameRateKey: 60.nsNumber + ] + if hevcSupported { + // Works broadly; adjust if you need different profiles + compressionProperties[AVVideoProfileLevelKey] = "HEVC_Main_AutoLevel" + } else { + compressionProperties[AVVideoProfileLevelKey] = AVVideoProfileLevelH264HighAutoLevel + } + + let videoSettings: [String: Any] = [ + AVVideoCodecKey: codec, + AVVideoWidthKey: w.nsNumber, + AVVideoHeightKey: h.nsNumber, + AVVideoCompressionPropertiesKey: compressionProperties, + ] + + let input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + input.expectsMediaDataInRealTime = true + return input + }() + + private var audioSampleRate: Double { + AVAudioSession.sharedInstance().sampleRate + } + private lazy var audioInput: AVAssetWriterInput = { + + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + private lazy var microphoneInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Separate audio file input (for microphone audio only) + private lazy var separateAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + private lazy var inputs: [AVAssetWriterInput] = [ + videoInput, + audioInput, + microphoneInput, + ] + + private let screenSize: CGSize + private let screenScale: CGFloat + + public init( + outputURL url: URL, + audioOutputURL: URL? = nil, + assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), + screenSize: CGSize, + screenScale: CGFloat, + separateAudioFile: Bool = false + ) throws { + assetWriterQueue = queue + assetWriter = try .init(url: url, fileType: .mp4) + assetWriter.shouldOptimizeForNetworkUse = true + + self.screenSize = screenSize + self.screenScale = screenScale + self.separateAudioFile = separateAudioFile + self.audioOutputURL = audioOutputURL + + // Initialize separate audio writer if needed + if separateAudioFile, let audioURL = audioOutputURL { + separateAudioWriter = try .init(url: audioURL, fileType: .m4a) + separateAudioWriter?.shouldOptimizeForNetworkUse = true + } + } + + public func start() throws { + try assetWriterQueue.sync { + let status = assetWriter.status + guard status == .unknown else { + throw Error.wrongAssetWriterStatus(status) + } + try assetWriter.error.map { + throw $0 + } + inputs + .lazy + .filter(assetWriter.canAdd(_:)) + .forEach(assetWriter.add(_:)) + try assetWriter.error.map { + throw $0 + } + assetWriter.startWriting() + try assetWriter.error.map { + throw $0 + } + + // Start separate audio writer if enabled + if separateAudioFile, let audioWriter = separateAudioWriter { + let audioStatus = audioWriter.status + guard audioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(audioStatus) + } + try audioWriter.error.map { throw $0 } + if audioWriter.canAdd(separateAudioInput) { + audioWriter.add(separateAudioInput) + } + try audioWriter.error.map { throw $0 } + audioWriter.startWriting() + try audioWriter.error.map { throw $0 } + } + } + } + + public func processSampleBuffer( + _ sampleBuffer: CMSampleBuffer, + with sampleBufferType: RPSampleBufferType + ) throws -> Bool { + + guard sampleBuffer.isValid, + CMSampleBufferDataIsReady(sampleBuffer) + else { + debugPrint( + "sampleBuffer.isValid", sampleBuffer.isValid, + "CMSampleBufferDataIsReady(sampleBuffer)", CMSampleBufferDataIsReady(sampleBuffer) + ) + return false + } + + let isWriting = assetWriterQueue.sync { + assetWriter.status == .writing + } + + guard isWriting else { + debugPrint( + "assetWriter.status", + assetWriter.status.description, + "assetWriter.error:", + assetWriter.error ?? "no error" + ) + return false + } + + assetWriterQueue.sync { + startSessionIfNeeded(sampleBuffer: sampleBuffer) + } + + let capture: (CMSampleBuffer) -> Bool + switch sampleBufferType { + case .video: + capture = captureVideoOutput + case .audioApp: + capture = captureAudioOutput + case .audioMic: + capture = captureMicrophoneOutput + // Also write to separate audio file if enabled + if separateAudioFile { + assetWriterQueue.sync { + _ = captureSeparateAudioOutput(sampleBuffer) + } + } + @unknown default: + debugPrint(#file, "Unknown type of sample buffer, \(sampleBufferType)") + capture = { _ in false } + } + + return assetWriterQueue.sync { + capture(sampleBuffer) + } + } + + public func pause() { + // TODO: Pause + } + + public func resume() { + // TODO: Resume + } + + /// Result containing both video and optional audio URLs + public struct FinishResult { + public let videoURL: URL + public let audioURL: URL? + } + + public func finish() throws -> URL { + let result = try finishWithAudio() + return result.videoURL + } + + public func finishWithAudio() throws -> FinishResult { + return try assetWriterQueue.sync { + let group: DispatchGroup = .init() + + inputs + .lazy + .filter { $0.isReadyForMoreMediaData } + .forEach { $0.markAsFinished() } + + let status = assetWriter.status + guard status == .writing else { + throw Error.wrongAssetWriterStatus(status) + } + group.enter() + + var error: Swift.Error? + assetWriter.finishWriting { [weak self] in + + defer { + group.leave() + } + + guard let self = self else { + error = Error.selfDeallocated + return + } + + if let e = self.assetWriter.error { + error = e + return + } + + let status = self.assetWriter.status + guard status == .completed else { + error = Error.wrongAssetWriterStatus(status) + return + } + } + group.wait() + try error.map { throw $0 } + + // Finish separate audio writer if enabled + var audioURL: URL? = nil + if separateAudioFile, let audioWriter = separateAudioWriter { + if separateAudioInput.isReadyForMoreMediaData { + separateAudioInput.markAsFinished() + } + + if audioWriter.status == .writing { + let audioGroup = DispatchGroup() + audioGroup.enter() + + var audioError: Swift.Error? + audioWriter.finishWriting { + defer { audioGroup.leave() } + if let e = audioWriter.error { + audioError = e + return + } + if audioWriter.status != .completed { + audioError = Error.wrongAssetWriterStatus(audioWriter.status) + } + } + audioGroup.wait() + + if audioError == nil { + audioURL = audioWriter.outputURL + } + } + } + + return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + } + } +} + +extension BroadcastWriter { + + fileprivate func startSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !assetWriterSessionStarted else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + assetWriter.startSession(atSourceTime: sourceTime) + assetWriterSessionStarted = true + } + + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + audioWriter.startSession(atSourceTime: sourceTime) + audioAssetWriterSessionStarted = true + } + + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard videoInput.isReadyForMoreMediaData else { + debugPrint("videoInput is not ready") + return false + } + return videoInput.append(sampleBuffer) + } + + fileprivate func captureAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard audioInput.isReadyForMoreMediaData else { + debugPrint("audioInput is not ready") + return false + } + return audioInput.append(sampleBuffer) + } + + fileprivate func captureMicrophoneOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + + guard microphoneInput.isReadyForMoreMediaData else { + debugPrint("microphoneInput is not ready") + return false + } + return microphoneInput.append(sampleBuffer) + } + + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let audioWriter = separateAudioWriter else { + return false + } + + // Check if audio writer is still writing + guard audioWriter.status == .writing else { + debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") + return false + } + + // Start session if needed + startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard separateAudioInput.isReadyForMoreMediaData else { + debugPrint("separateAudioInput is not ready") + return false + } + return separateAudioInput.append(sampleBuffer) + } +} diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift new file mode 100644 index 0000000..f8feae7 --- /dev/null +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -0,0 +1,244 @@ +import AVFoundation +import ReplayKit +import UserNotifications +import Darwin + +@_silgen_name("finishBroadcastGracefully") +func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) + +/* + Handles the main processing of the global broadcast. + The app-group identifier is fetched from the extension's Info.plist + ("BroadcastExtensionAppGroupIdentifier" key) so you don't have to hard-code it here. + */ +final class SampleHandler: RPBroadcastSampleHandler { + + // MARK: โ€“ Properties + + private func appGroupIDFromPlist() -> String? { + guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + !value.isEmpty + else { + return nil + } + return value + } + + // Store both the CFString and CFNotificationName versions + private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString + private static let stopNotificationName = CFNotificationName(stopNotificationString) + + private lazy var hostAppGroupIdentifier: String? = { + return appGroupIDFromPlist() + }() + + private var writer: BroadcastWriter? + private let fileManager: FileManager = .default + private let nodeURL: URL + private let audioNodeURL: URL + private var sawMicBuffers = false + private var separateAudioFile: Bool = false + + // MARK: โ€“ Init + override init() { + let uuid = UUID().uuidString + nodeURL = fileManager.temporaryDirectory + .appendingPathComponent(uuid) + .appendingPathExtension(for: .mpeg4Movie) + + audioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_audio") + .appendingPathExtension("m4a") + + fileManager.removeFileIfExists(url: nodeURL) + fileManager.removeFileIfExists(url: audioNodeURL) + super.init() + } + + deinit { + CFNotificationCenterRemoveObserver( + CFNotificationCenterGetDarwinNotifyCenter(), + UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + SampleHandler.stopNotificationName, + nil + ) + } + + private func startListeningForStopSignal() { + let center = CFNotificationCenterGetDarwinNotifyCenter() + + CFNotificationCenterAddObserver( + center, + UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()), + { _, observer, name, _, _ in + guard + let observer, + let name, + name == SampleHandler.stopNotificationName + else { return } + + let me = Unmanaged + .fromOpaque(observer) + .takeUnretainedValue() + me.stopBroadcastGracefully() + }, + SampleHandler.stopNotificationString, + nil, + .deliverImmediately + ) + } + + // MARK: โ€“ Broadcast lifecycle + override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { + startListeningForStopSignal() + + guard let groupID = hostAppGroupIdentifier else { + finishBroadcastWithError( + NSError( + domain: "SampleHandler", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] + ) + ) + return + } + + // Check if separate audio file is requested + if let userDefaults = UserDefaults(suiteName: groupID) { + separateAudioFile = userDefaults.bool(forKey: "SeparateAudioFileEnabled") + } + + // Clean up old recordings + cleanupOldRecordings(in: groupID) + + // Start recording + let screen: UIScreen = .main + do { + writer = try .init( + outputURL: nodeURL, + audioOutputURL: separateAudioFile ? audioNodeURL : nil, + screenSize: screen.bounds.size, + screenScale: screen.scale, + separateAudioFile: separateAudioFile + ) + try writer?.start() + } catch { + finishBroadcastWithError(error) + } + } + + private func cleanupOldRecordings(in groupID: String) { + guard let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) + else { return } + + do { + let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) + for url in items where url.pathExtension.lowercased() == "mp4" { + try? fileManager.removeItem(at: url) + } + } catch { + // Non-critical error, continue with broadcast + } + } + + override func processSampleBuffer( + _ sampleBuffer: CMSampleBuffer, + with sampleBufferType: RPSampleBufferType + ) { + guard let writer else { return } + + if sampleBufferType == .audioMic { + sawMicBuffers = true + } + + do { + _ = try writer.processSampleBuffer(sampleBuffer, with: sampleBufferType) + } catch { + finishBroadcastWithError(error) + } + } + + override func broadcastPaused() { + writer?.pause() + } + + override func broadcastResumed() { + writer?.resume() + } + + private func stopBroadcastGracefully() { + finishBroadcastGracefully(self) + } + + override func broadcastFinished() { + guard let writer else { return } + + // Finish writing - use finishWithAudio to get both video and audio URLs + let result: BroadcastWriter.FinishResult + do { + result = try writer.finishWithAudio() + } catch { + // Writer failed, but we can't call finishBroadcastWithError here + // as we're already in the finish process + return + } + + guard let groupID = hostAppGroupIdentifier else { return } + + // Get container directory + guard let containerURL = fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) + else { return } + + // Create directory if needed + do { + try fileManager.createDirectory(at: containerURL, withIntermediateDirectories: true) + } catch { + return + } + + // Move video file to shared container + let videoDestination = containerURL.appendingPathComponent(result.videoURL.lastPathComponent) + do { + try fileManager.moveItem(at: result.videoURL, to: videoDestination) + } catch { + // File move failed, but we can't error out at this point + return + } + + // Move audio file to shared container if it exists + if let audioURL = result.audioURL { + let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) + do { + try fileManager.moveItem(at: audioURL, to: audioDestination) + // Store audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") + } catch { + // Audio file move failed, but video is already saved + debugPrint("Failed to move audio file: \(error)") + } + } else { + // Clear audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAudioFileName") + } + + // Persist microphone state and audio file state + UserDefaults(suiteName: groupID)? + .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") + UserDefaults(suiteName: groupID)? + .set(separateAudioFile, forKey: "LastBroadcastHadSeparateAudio") + } +} + +// MARK: โ€“ Helpers +extension FileManager { + fileprivate func removeFileIfExists(url: URL) { + guard fileExists(atPath: url.path) else { return } + try? removeItem(at: url) + } +} \ No newline at end of file diff --git a/lib/typescript/expo-plugin/support/iosConstants.d.ts b/lib/typescript/expo-plugin/support/iosConstants.d.ts new file mode 100644 index 0000000..71413cc --- /dev/null +++ b/lib/typescript/expo-plugin/support/iosConstants.d.ts @@ -0,0 +1,16 @@ +import type { ConfigProps } from '../@types'; +export declare const IPHONEOS_DEPLOYMENT_TARGET = "11.0"; +export declare const TARGETED_DEVICE_FAMILY = "\"1,2\""; +export declare const getBroadcastExtensionTargetName: (props: ConfigProps) => string; +export declare const getBroadcastExtensionPodfileSnippet: (props: ConfigProps) => string; +export declare const GROUP_IDENTIFIER_TEMPLATE_REGEX: RegExp; +export declare const BUNDLE_SHORT_VERSION_TEMPLATE_REGEX: RegExp; +export declare const BUNDLE_VERSION_TEMPLATE_REGEX: RegExp; +export declare const SCHEME_TEMPLATE_REGEX: RegExp; +export declare const DEFAULT_BUNDLE_VERSION = "1"; +export declare const DEFAULT_BUNDLE_SHORT_VERSION = "1.0"; +export declare const BROADCAST_EXT_SOURCE_FILES: string[]; +export declare const BROADCAST_EXT_CONFIG_FILES: string[]; +export declare const BROADCAST_EXT_ALL_FILES: string[]; +export declare const getAppGroup: (mainAppBundleId: string, props: ConfigProps) => string; +export declare function getBroadcastExtensionBundleIdentifier(mainAppBundleId: string, props: ConfigProps): string; diff --git a/lib/typescript/expo-plugin/support/iosConstants.js b/lib/typescript/expo-plugin/support/iosConstants.js new file mode 100644 index 0000000..7893815 --- /dev/null +++ b/lib/typescript/expo-plugin/support/iosConstants.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAppGroup = exports.BROADCAST_EXT_ALL_FILES = exports.BROADCAST_EXT_CONFIG_FILES = exports.BROADCAST_EXT_SOURCE_FILES = exports.DEFAULT_BUNDLE_SHORT_VERSION = exports.DEFAULT_BUNDLE_VERSION = exports.SCHEME_TEMPLATE_REGEX = exports.BUNDLE_VERSION_TEMPLATE_REGEX = exports.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = exports.GROUP_IDENTIFIER_TEMPLATE_REGEX = exports.getBroadcastExtensionPodfileSnippet = exports.getBroadcastExtensionTargetName = exports.TARGETED_DEVICE_FAMILY = exports.IPHONEOS_DEPLOYMENT_TARGET = void 0; +exports.getBroadcastExtensionBundleIdentifier = getBroadcastExtensionBundleIdentifier; +exports.IPHONEOS_DEPLOYMENT_TARGET = '11.0'; +exports.TARGETED_DEVICE_FAMILY = `"1,2"`; +const getBroadcastExtensionTargetName = (props) => { + if (props.iosBroadcastExtensionTargetName) + return props.iosBroadcastExtensionTargetName; + return `BroadcastExtension`; +}; +exports.getBroadcastExtensionTargetName = getBroadcastExtensionTargetName; +// Podfile configuration for ReplayKit (if needed for dependencies) +const getBroadcastExtensionPodfileSnippet = (props) => { + const targetName = (0, exports.getBroadcastExtensionTargetName)(props); + return ` + target '${targetName}' do + # ReplayKit is a system framework, no pods needed typically + # Add any specific pods for broadcast extension here if needed + end`; +}; +exports.getBroadcastExtensionPodfileSnippet = getBroadcastExtensionPodfileSnippet; +// Template replacement patterns +exports.GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; +exports.BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = /{{BUNDLE_SHORT_VERSION}}/gm; +exports.BUNDLE_VERSION_TEMPLATE_REGEX = /{{BUNDLE_VERSION}}/gm; +exports.SCHEME_TEMPLATE_REGEX = /{{SCHEME}}/gm; +exports.DEFAULT_BUNDLE_VERSION = '1'; +exports.DEFAULT_BUNDLE_SHORT_VERSION = '1.0'; +// Broadcast Extension specific constants +exports.BROADCAST_EXT_SOURCE_FILES = [ + 'SampleHandler.swift', + 'BroadcastWriter.swift', + 'BroadcastHelper.m', +]; +exports.BROADCAST_EXT_CONFIG_FILES = [ + `BroadcastExtension-Info.plist`, + `BroadcastExtension.entitlements`, + 'BroadcastExtension-PrivacyInfo.xcprivacy', + 'BroadcastHelper.h', + 'BroadcastExtension-Bridging-Header.h', +]; +// All extension files combined +exports.BROADCAST_EXT_ALL_FILES = [ + ...exports.BROADCAST_EXT_SOURCE_FILES, + ...exports.BROADCAST_EXT_CONFIG_FILES, +]; +const getAppGroup = (mainAppBundleId, props) => { + if (props.iosAppGroupIdentifier) + return props.iosAppGroupIdentifier; + return `group.${mainAppBundleId}.screen-recorder`; +}; +exports.getAppGroup = getAppGroup; +// Helper function to get broadcast extension bundle identifier +function getBroadcastExtensionBundleIdentifier(mainAppBundleId, props) { + if (props.iosExtensionBundleIdentifier) + return props.iosExtensionBundleIdentifier; + const targetName = (0, exports.getBroadcastExtensionTargetName)(props); + return `${mainAppBundleId}.${targetName}`; +} diff --git a/lib/typescript/expo-plugin/support/iosConstants.ts b/lib/typescript/expo-plugin/support/iosConstants.ts new file mode 100644 index 0000000..2f102d1 --- /dev/null +++ b/lib/typescript/expo-plugin/support/iosConstants.ts @@ -0,0 +1,66 @@ +import type { ConfigProps } from '../@types'; + +export const IPHONEOS_DEPLOYMENT_TARGET = '11.0'; +export const TARGETED_DEVICE_FAMILY = `"1,2"`; + +export const getBroadcastExtensionTargetName = (props: ConfigProps) => { + if (props.iosBroadcastExtensionTargetName) + return props.iosBroadcastExtensionTargetName; + return `BroadcastExtension`; +}; + +// Podfile configuration for ReplayKit (if needed for dependencies) +export const getBroadcastExtensionPodfileSnippet = (props: ConfigProps) => { + const targetName = getBroadcastExtensionTargetName(props); + return ` + target '${targetName}' do + # ReplayKit is a system framework, no pods needed typically + # Add any specific pods for broadcast extension here if needed + end`; +}; + +// Template replacement patterns +export const GROUP_IDENTIFIER_TEMPLATE_REGEX = /{{GROUP_IDENTIFIER}}/gm; +export const BUNDLE_SHORT_VERSION_TEMPLATE_REGEX = /{{BUNDLE_SHORT_VERSION}}/gm; +export const BUNDLE_VERSION_TEMPLATE_REGEX = /{{BUNDLE_VERSION}}/gm; +export const SCHEME_TEMPLATE_REGEX = /{{SCHEME}}/gm; + +export const DEFAULT_BUNDLE_VERSION = '1'; +export const DEFAULT_BUNDLE_SHORT_VERSION = '1.0'; + +// Broadcast Extension specific constants +export const BROADCAST_EXT_SOURCE_FILES = [ + 'SampleHandler.swift', + 'BroadcastWriter.swift', + 'BroadcastHelper.m', +]; + +export const BROADCAST_EXT_CONFIG_FILES = [ + `BroadcastExtension-Info.plist`, + `BroadcastExtension.entitlements`, + 'BroadcastExtension-PrivacyInfo.xcprivacy', + 'BroadcastHelper.h', + 'BroadcastExtension-Bridging-Header.h', +]; + +// All extension files combined +export const BROADCAST_EXT_ALL_FILES = [ + ...BROADCAST_EXT_SOURCE_FILES, + ...BROADCAST_EXT_CONFIG_FILES, +]; + +export const getAppGroup = (mainAppBundleId: string, props: ConfigProps) => { + if (props.iosAppGroupIdentifier) return props.iosAppGroupIdentifier; + return `group.${mainAppBundleId}.screen-recorder`; +}; + +// Helper function to get broadcast extension bundle identifier +export function getBroadcastExtensionBundleIdentifier( + mainAppBundleId: string, + props: ConfigProps +): string { + if (props.iosExtensionBundleIdentifier) + return props.iosExtensionBundleIdentifier; + const targetName = getBroadcastExtensionTargetName(props); + return `${mainAppBundleId}.${targetName}`; +} diff --git a/lib/typescript/expo-plugin/support/updatePodfile.d.ts b/lib/typescript/expo-plugin/support/updatePodfile.d.ts new file mode 100644 index 0000000..3e078ad --- /dev/null +++ b/lib/typescript/expo-plugin/support/updatePodfile.d.ts @@ -0,0 +1,2 @@ +import type { ConfigProps } from '../@types'; +export declare function updatePodfile(iosPath: string, props: ConfigProps): Promise; diff --git a/lib/typescript/expo-plugin/support/updatePodfile.js b/lib/typescript/expo-plugin/support/updatePodfile.js new file mode 100644 index 0000000..dc94234 --- /dev/null +++ b/lib/typescript/expo-plugin/support/updatePodfile.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.updatePodfile = updatePodfile; +// updatePodfile.ts +const fs_1 = __importDefault(require("fs")); +const iosConstants_1 = require("./iosConstants"); +const ScreenRecorderLog_1 = require("./ScreenRecorderLog"); +const FileManager_1 = require("./FileManager"); +async function updatePodfile(iosPath, props) { + const podfilePath = `${iosPath}/Podfile`; + let podfile = await FileManager_1.FileManager.readFile(podfilePath); + // Skip if already present + if (podfile.includes((0, iosConstants_1.getBroadcastExtensionTargetName)(props))) { + ScreenRecorderLog_1.ScreenRecorderLog.log('Extension target already in Podfile. Skippingโ€ฆ'); + return; + } + // Inject snippet into every `target 'Something' do โ€ฆ end` that looks like an iOS app + podfile = podfile.replace(/target ['"][^'"]+['"] do([\s\S]*?)end/g, (block) => block.replace(/\nend$/, `${(0, iosConstants_1.getBroadcastExtensionPodfileSnippet)(props)}\nend`)); + await fs_1.default.promises.writeFile(podfilePath, podfile, 'utf8'); + ScreenRecorderLog_1.ScreenRecorderLog.log('Inserted BroadcastExtension into Podfile.'); +} diff --git a/lib/typescript/expo-plugin/support/updatePodfile.ts b/lib/typescript/expo-plugin/support/updatePodfile.ts new file mode 100644 index 0000000..2053f49 --- /dev/null +++ b/lib/typescript/expo-plugin/support/updatePodfile.ts @@ -0,0 +1,31 @@ +// updatePodfile.ts +import fs from 'fs'; +import { + getBroadcastExtensionPodfileSnippet, + getBroadcastExtensionTargetName, +} from './iosConstants'; +import { ScreenRecorderLog } from './ScreenRecorderLog'; +import { FileManager } from './FileManager'; +import type { ConfigProps } from '../@types'; + +export async function updatePodfile(iosPath: string, props: ConfigProps) { + const podfilePath = `${iosPath}/Podfile`; + let podfile = await FileManager.readFile(podfilePath); + + // Skip if already present + if (podfile.includes(getBroadcastExtensionTargetName(props))) { + ScreenRecorderLog.log('Extension target already in Podfile. Skippingโ€ฆ'); + return; + } + + // Inject snippet into every `target 'Something' do โ€ฆ end` that looks like an iOS app + podfile = podfile.replace(/target ['"][^'"]+['"] do([\s\S]*?)end/g, (block) => + block.replace( + /\nend$/, + `${getBroadcastExtensionPodfileSnippet(props)}\nend` + ) + ); + + await fs.promises.writeFile(podfilePath, podfile, 'utf8'); + ScreenRecorderLog.log('Inserted BroadcastExtension into Podfile.'); +} diff --git a/lib/typescript/expo-plugin/support/validatePluginProps.d.ts b/lib/typescript/expo-plugin/support/validatePluginProps.d.ts new file mode 100644 index 0000000..90e812f --- /dev/null +++ b/lib/typescript/expo-plugin/support/validatePluginProps.d.ts @@ -0,0 +1,5 @@ +import type { ConfigProps } from '../@types'; +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +export declare function validatePluginProps(props: ConfigProps): void; diff --git a/lib/typescript/expo-plugin/support/validatePluginProps.js b/lib/typescript/expo-plugin/support/validatePluginProps.js new file mode 100644 index 0000000..f92447c --- /dev/null +++ b/lib/typescript/expo-plugin/support/validatePluginProps.js @@ -0,0 +1,54 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validatePluginProps = validatePluginProps; +const PLUGIN_NAME = 'Nitro Screen Recorder Expo Plugin'; +const VALID_PLUGIN_PROP_NAMES = [ + 'enableCameraPermission', + 'cameraPermissionText', + 'enableMicrophonePermission', + 'microphonePermissionText', + 'showPluginLogs', + 'iosBroadcastExtensionTargetName', + 'iosAppGroupIdentifier', + 'iosExtensionBundleIdentifier', +]; +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +function validatePluginProps(props) { + if (props == null || typeof props !== 'object') { + throw new Error(`${PLUGIN_NAME}: expected props to be an object, got ${typeof props}`); + } + if (props.enableCameraPermission !== undefined && + typeof props.enableCameraPermission !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'enableCameraPermission' must be a boolean.`); + } + if (props.cameraPermissionText !== undefined && + typeof props.cameraPermissionText !== 'string') { + throw new Error(`${PLUGIN_NAME}: 'cameraPermissionText' must be a string.`); + } + if (props.enableMicrophonePermission !== undefined && + typeof props.enableMicrophonePermission !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'enableMicrophonePermission' must be a boolean.`); + } + if (props.microphonePermissionText !== undefined && + typeof props.microphonePermissionText !== 'string') { + throw new Error(`${PLUGIN_NAME}: 'microphonePermissionText' must be a string.`); + } + if (props.showPluginLogs !== undefined && + typeof props.showPluginLogs !== 'boolean') { + throw new Error(`${PLUGIN_NAME}: 'showPluginLogs' must be a boolean.`); + } + if (props.iosBroadcastExtensionTargetName !== undefined && + props.iosBroadcastExtensionTargetName.includes(' ')) { + throw new Error(`${PLUGIN_NAME}: 'iosBroadcastExtensionTargetName' cannot have spaces.`); + } + if (props.iosAppGroupIdentifier !== undefined && + !props.iosAppGroupIdentifier.startsWith('group')) { + throw new Error(`${PLUGIN_NAME}: 'iosAppGroupIdentifier' must start with group! Try changing to "group.(insert main app bundle id) or removing this line and letting the plugin manage the app group name for you.`); + } + const invalidKeys = Object.keys(props).filter((k) => !VALID_PLUGIN_PROP_NAMES.includes(k)); + if (invalidKeys.length > 0) { + throw new Error(`${PLUGIN_NAME}: invalid propert${invalidKeys.length === 1 ? 'y' : 'ies'} ${invalidKeys.map((p) => `"${p}"`).join(', ')} provided.`); + } +} diff --git a/lib/typescript/expo-plugin/support/validatePluginProps.ts b/lib/typescript/expo-plugin/support/validatePluginProps.ts new file mode 100644 index 0000000..15e4962 --- /dev/null +++ b/lib/typescript/expo-plugin/support/validatePluginProps.ts @@ -0,0 +1,95 @@ +import type { ConfigProps } from '../@types'; + +const PLUGIN_NAME = 'Nitro Screen Recorder Expo Plugin'; + +const VALID_PLUGIN_PROP_NAMES: string[] = [ + 'enableCameraPermission', + 'cameraPermissionText', + 'enableMicrophonePermission', + 'microphonePermissionText', + 'showPluginLogs', + 'iosBroadcastExtensionTargetName', + 'iosAppGroupIdentifier', + 'iosExtensionBundleIdentifier', +]; + +/** + * Validate a single props object. Throws on invalid types or unknown properties. + */ +export function validatePluginProps(props: ConfigProps): void { + if (props == null || typeof props !== 'object') { + throw new Error( + `${PLUGIN_NAME}: expected props to be an object, got ${typeof props}` + ); + } + + if ( + props.enableCameraPermission !== undefined && + typeof props.enableCameraPermission !== 'boolean' + ) { + throw new Error( + `${PLUGIN_NAME}: 'enableCameraPermission' must be a boolean.` + ); + } + + if ( + props.cameraPermissionText !== undefined && + typeof props.cameraPermissionText !== 'string' + ) { + throw new Error(`${PLUGIN_NAME}: 'cameraPermissionText' must be a string.`); + } + + if ( + props.enableMicrophonePermission !== undefined && + typeof props.enableMicrophonePermission !== 'boolean' + ) { + throw new Error( + `${PLUGIN_NAME}: 'enableMicrophonePermission' must be a boolean.` + ); + } + + if ( + props.microphonePermissionText !== undefined && + typeof props.microphonePermissionText !== 'string' + ) { + throw new Error( + `${PLUGIN_NAME}: 'microphonePermissionText' must be a string.` + ); + } + + if ( + props.showPluginLogs !== undefined && + typeof props.showPluginLogs !== 'boolean' + ) { + throw new Error(`${PLUGIN_NAME}: 'showPluginLogs' must be a boolean.`); + } + + if ( + props.iosBroadcastExtensionTargetName !== undefined && + props.iosBroadcastExtensionTargetName.includes(' ') + ) { + throw new Error( + `${PLUGIN_NAME}: 'iosBroadcastExtensionTargetName' cannot have spaces.` + ); + } + + if ( + props.iosAppGroupIdentifier !== undefined && + !props.iosAppGroupIdentifier.startsWith('group') + ) { + throw new Error( + `${PLUGIN_NAME}: 'iosAppGroupIdentifier' must start with group! Try changing to "group.(insert main app bundle id) or removing this line and letting the plugin manage the app group name for you.` + ); + } + + const invalidKeys = Object.keys(props).filter( + (k) => !VALID_PLUGIN_PROP_NAMES.includes(k) + ); + if (invalidKeys.length > 0) { + throw new Error( + `${PLUGIN_NAME}: invalid propert${ + invalidKeys.length === 1 ? 'y' : 'ies' + } ${invalidKeys.map((p) => `"${p}"`).join(', ')} provided.` + ); + } +} diff --git a/lib/typescript/expo-plugin/withScreenRecorder.d.ts b/lib/typescript/expo-plugin/withScreenRecorder.d.ts new file mode 100644 index 0000000..8dc55e7 --- /dev/null +++ b/lib/typescript/expo-plugin/withScreenRecorder.d.ts @@ -0,0 +1,4 @@ +import type { ConfigPlugin } from '@expo/config-plugins'; +import type { ConfigProps } from './@types'; +declare const _default: ConfigPlugin; +export default _default; diff --git a/lib/typescript/expo-plugin/withScreenRecorder.js b/lib/typescript/expo-plugin/withScreenRecorder.js new file mode 100644 index 0000000..ab7d3f6 --- /dev/null +++ b/lib/typescript/expo-plugin/withScreenRecorder.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const config_plugins_1 = require("@expo/config-plugins"); +const withBroadcastExtension_1 = require("./ios/withBroadcastExtension"); +const withAndroidScreenRecording_1 = require("./android/withAndroidScreenRecording"); +const validatePluginProps_1 = require("./support/validatePluginProps"); +const pkg = require('../package.json'); +const CAMERA_USAGE = 'Allow $(PRODUCT_NAME) to access your camera'; +const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone'; +const withScreenRecorder = (config, props = {}) => { + var _a, _b, _c, _d; + (0, validatePluginProps_1.validatePluginProps)(props); + /*---------------IOS-------------------- */ + if (config.ios == null) + config.ios = {}; + if (config.ios.infoPlist == null) + config.ios.infoPlist = {}; + if (props.enableCameraPermission === true) { + config.ios.infoPlist.NSCameraUsageDescription = + (_b = (_a = props.cameraPermissionText) !== null && _a !== void 0 ? _a : config.ios.infoPlist.NSCameraUsageDescription) !== null && _b !== void 0 ? _b : CAMERA_USAGE; + } + if (props.enableMicrophonePermission === true) { + config.ios.infoPlist.NSMicrophoneUsageDescription = + (_d = (_c = props.microphonePermissionText) !== null && _c !== void 0 ? _c : config.ios.infoPlist.NSMicrophoneUsageDescription) !== null && _d !== void 0 ? _d : MICROPHONE_USAGE; + } + config = (0, withBroadcastExtension_1.withBroadcastExtension)(config, props); + /*---------------ANDROID-------------------- */ + const androidPermissions = [ + // already conditionally added + ...(props.enableMicrophonePermission !== false + ? ['android.permission.RECORD_AUDIO'] + : []), + 'android.permission.FOREGROUND_SERVICE', + 'android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION', + 'android.permission.POST_NOTIFICATIONS', + ]; + return (0, config_plugins_1.withPlugins)(config, [ + // Android plugins + [config_plugins_1.AndroidConfig.Permissions.withPermissions, androidPermissions], + [withAndroidScreenRecording_1.withAndroidScreenRecording, props], + ]); +}; +exports.default = (0, config_plugins_1.createRunOncePlugin)(withScreenRecorder, pkg.name, pkg.version); diff --git a/lib/typescript/functions.d.ts b/lib/typescript/functions.d.ts new file mode 100644 index 0000000..3483257 --- /dev/null +++ b/lib/typescript/functions.d.ts @@ -0,0 +1,206 @@ +import type { ScreenRecordingFile, PermissionResponse, InAppRecordingInput, ScreenRecordingEvent, PermissionStatus, GlobalRecordingInput, BroadcastPickerPresentationEvent } from './types'; +/** + * Gets the current camera permission status without requesting permission. + * + * @platform iOS, Android + * @returns The current permission status for camera access + * @example + * ```typescript + * const status = getCameraPermissionStatus(); + * if (status === 'granted') { + * // Camera is available + * } + * ``` + */ +export declare function getCameraPermissionStatus(): PermissionStatus; +/** + * Gets the current microphone permission status without requesting permission. + * + * @platform iOS, Android + * @returns The current permission status for microphone access + * @example + * ```typescript + * const status = getMicrophonePermissionStatus(); + * if (status === 'granted') { + * // Microphone is available + * } + * ``` + */ +export declare function getMicrophonePermissionStatus(): PermissionStatus; +/** + * Requests camera permission from the user if not already granted. + * Shows the system permission dialog if permission hasn't been determined. + * + * @platform iOS, Android + * @returns Promise that resolves with the permission response + * @example + * ```typescript + * const response = await requestCameraPermission(); + * if (response.status === 'granted') { + * // Permission granted, can use camera + * } + * ``` + */ +export declare function requestCameraPermission(): Promise; +/** + * Requests microphone permission from the user if not already granted. + * Shows the system permission dialog if permission hasn't been determined. + * + * @platform iOS, Android + * @returns Promise that resolves with the permission response + * @example + * ```typescript + * const response = await requestMicrophonePermission(); + * if (response.status === 'granted') { + * // Permission granted, can record audio + * } + * ``` + */ +export declare function requestMicrophonePermission(): Promise; +/** + * Starts in-app screen recording with the specified configuration. + * Records only the current app's content, not system-wide screen content. + * + * @platform iOS + * @param input Configuration object containing recording options and callbacks + * @returns Promise that resolves when recording starts successfully + * @example + * ```typescript + * await startInAppRecording({ + * options: { + * enableMic: true, + * enableCamera: true, + * cameraDevice: 'front', + * cameraPreviewStyle: { width: 100, height: 150, top: 30, left: 10 } + * }, + * onRecordingFinished: (file) => { + * console.log('Recording saved:', file.path); + * } + * }); + * ``` + */ +export declare function startInAppRecording(input: InAppRecordingInput): Promise; +/** + * Stops the current in-app recording and saves the recorded video. + * The recording file will be provided through the onRecordingFinished callback. + * + * @platform iOS-only + * @example + * ```typescript + * stopInAppRecording(); // File will be available in onRecordingFinished callback + * ``` + */ +export declare function stopInAppRecording(): Promise; +/** + * Cancels the current in-app recording without saving the video. + * No file will be generated and onRecordingFinished will not be called. + * + * @platform iOS-only + * @example + * ```typescript + * cancelInAppRecording(); // Recording discarded, no file saved + * ``` + */ +export declare function cancelInAppRecording(): Promise; +/** + * Starts global screen recording that captures the entire device screen. + * Records system-wide content, including other apps and system UI. + * Requires screen recording permission on iOS. + * + * @platform iOS, Android + * @example + * ```typescript + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues + * ``` + */ +export declare function startGlobalRecording(input: GlobalRecordingInput): void; +/** + * Stops the current global screen recording and saves the video. + * The recorded file can be retrieved using retrieveLastGlobalRecording(). + * + * @platform Android/ios + * @param options.settledTimeMs A "delay" time to wait before the function + * tries to retrieve the file from the asset writer. It can take some time + * to finish completion and correclty return the file. Default = 500ms + * @example + * ```typescript + * const file = await stopGlobalRecording({ settledTimeMs: 1000 }); + * if (file) { + * console.log('Global recording saved:', file.path); + * } + * ``` + */ +export declare function stopGlobalRecording(options?: { + settledTimeMs: number; +}): Promise; +/** + * Retrieves the most recently completed global recording file. + * Returns undefined if no global recording has been completed. + * + * @platform iOS, Android + * @returns The last global recording file or undefined if none exists + * @example + * ```typescript + * const lastRecording = retrieveLastGlobalRecording(); + * if (lastRecording) { + * console.log('Duration:', lastRecording.duration); + * console.log('File size:', lastRecording.size); + * } + * ``` + */ +export declare function retrieveLastGlobalRecording(): ScreenRecordingFile | undefined; +/** + * Adds a listener for screen recording events (began, ended, etc.). + * Returns a cleanup function to remove the listener when no longer needed. + * + * @platform iOS, Android + * @param listener Callback function that receives screen recording events + * @returns Cleanup function to remove the listener + * @example + * ```typescript + * useEffect(() => { + * const removeListener = addScreenRecordingListener((event: ScreenRecordingEvent) => { + * console.log("Event type:", event.type, "Event reason:", event.reason) + * }); + * // Later, remove the listener + * return () => removeListener(); + * },[]) + * ``` + */ +export declare function addScreenRecordingListener({ listener, ignoreRecordingsInitiatedElsewhere, }: { + listener: (event: ScreenRecordingEvent) => void; + ignoreRecordingsInitiatedElsewhere: boolean; +}): () => void; +/** + * Adds a listener for ios only to track whether (start, stop, error, etc.). + * Returns a cleanup function to remove the listener when no longer needed. + * + * @platform iOS + * @param listener Callback function that receives the status of the BroadcastPickerView + * on ios + * @returns Cleanup function to remove the listener + * @example + * ```typescript + * useEffect(() => { + * const removeListener = addBroadcastPickerListener((event: BroadcastPickerPresentationEvent) => { + * console.log("Picker status", event) + * }); + * // Later, remove the listener + * return () => removeListener(); + * },[]) + * ``` + */ +export declare function addBroadcastPickerListener(listener: (event: BroadcastPickerPresentationEvent) => void): () => void; +/** + * Clears all cached recording files to free up storage space. + * This will delete temporary files but not files that have been explicitly saved. + * + * @platform iOS, Android + * @example + * ```typescript + * clearCache(); // Frees up storage by removing temporary recording files + * ``` + */ +export declare function clearCache(): void; +//# sourceMappingURL=functions.d.ts.map \ No newline at end of file diff --git a/lib/typescript/functions.d.ts.map b/lib/typescript/functions.d.ts.map new file mode 100644 index 0000000..0294cb9 --- /dev/null +++ b/lib/typescript/functions.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../src/functions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAYjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,gBAAgB,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,IAAI,gBAAgB,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE/E;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CACjD,mBAAmB,GAAG,SAAS,CAChC,CAMA;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAetE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAe3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAE7E;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,kCAA0C,GAC3C,EAAE;IACD,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,kCAAkC,EAAE,OAAO,CAAC;CAC7C,GAAG,MAAM,IAAI,CASb;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,IAAI,CAWZ;AAMD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"} \ No newline at end of file diff --git a/lib/typescript/hooks/index.d.ts b/lib/typescript/hooks/index.d.ts new file mode 100644 index 0000000..a546cb0 --- /dev/null +++ b/lib/typescript/hooks/index.d.ts @@ -0,0 +1,2 @@ +export * from './useGlobalRecording'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/typescript/hooks/index.d.ts.map b/lib/typescript/hooks/index.d.ts.map new file mode 100644 index 0000000..ea426e4 --- /dev/null +++ b/lib/typescript/hooks/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC"} \ No newline at end of file diff --git a/lib/typescript/hooks/useCameraMicPermissions.d.ts b/lib/typescript/hooks/useCameraMicPermissions.d.ts new file mode 100644 index 0000000..078ad37 --- /dev/null +++ b/lib/typescript/hooks/useCameraMicPermissions.d.ts @@ -0,0 +1,46 @@ +interface PermissionState { + /** + * Whether the specified permission has explicitly been granted. + * By default, this will be `false`. To request permission, call `requestPermission()`. + */ + hasPermission: boolean; + /** + * Requests the specified permission from the user. + * @returns Whether the specified permission has now been granted, or not. + */ + requestPermission: () => Promise; +} +/** + * Returns whether the user has granted permission to use the Camera, or not. + * + * If the user doesn't grant Camera Permission, you cannot use the ``. + * + * @example + * ```tsx + * const { hasPermission, requestPermission } = useCameraPermission() + * + * if (!hasPermission) { + * return + * } else { + * return + * } + * ``` + */ +export declare function useCameraPermission(): PermissionState; +/** + * Returns whether the user has granted permission to use the Microphone, or not. + * + * If the user doesn't grant Audio Permission, you can use the `` but you cannot + * record videos with audio (the `audio={..}` prop). + * + * @example + * ```tsx + * const { hasPermission, requestPermission } = useMicrophonePermission() + * const canRecordAudio = hasPermission + * + * return + * ``` + */ +export declare function useMicrophonePermission(): PermissionState; +export {}; +//# sourceMappingURL=useCameraMicPermissions.d.ts.map \ No newline at end of file diff --git a/lib/typescript/hooks/useCameraMicPermissions.d.ts.map b/lib/typescript/hooks/useCameraMicPermissions.d.ts.map new file mode 100644 index 0000000..863aabe --- /dev/null +++ b/lib/typescript/hooks/useCameraMicPermissions.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"useCameraMicPermissions.d.ts","sourceRoot":"","sources":["../../../src/hooks/useCameraMicPermissions.ts"],"names":[],"mappings":"AAUA,UAAU,eAAe;IACvB;;;OAGG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,iBAAiB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3C;AAgCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,IAAI,eAAe,CAErD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,IAAI,eAAe,CAKzD"} \ No newline at end of file diff --git a/lib/typescript/hooks/useGlobalRecording.d.ts b/lib/typescript/hooks/useGlobalRecording.d.ts new file mode 100644 index 0000000..de1d201 --- /dev/null +++ b/lib/typescript/hooks/useGlobalRecording.d.ts @@ -0,0 +1,97 @@ +import type { ScreenRecordingFile } from '../types'; +/** + * Configuration options for the global recording hook. + */ +type GlobalRecordingHookInput = { + /** + * Callback invoked when a global screen recording begins. + * Use this to update your UI to indicate recording is in progress. + */ + onRecordingStarted?: () => void; + /** + * Callback invoked when a global screen recording finishes. + * Receives the recorded file (if successfully retrieved) or undefined if retrieval failed. + * + * @param file The screen recording file, or undefined if retrieval failed + */ + onRecordingFinished?: (file?: ScreenRecordingFile) => void; + /** + * A callback for iOS when the broadcast modal shows, in case you want to + * perform some analytics or tasks. Is a no-op on android. + */ + onBroadcastModalShown?: () => void; + onBroadcastModalDismissed?: () => void; + /** + * Time in milliseconds to wait after recording ends before attempting to retrieve the file. + * This allows the system time to finish writing the recording to disk. + * + * @default 500 + */ + settledTimeMs?: number; + /** + * This property is passed to the underlying listener to ignore recordings that were initiated by the + * external system. This is useful if you only want to track global recordings that were started via the startGlobalRecording function. + */ + ignoreRecordingsInitiatedElsewhere?: boolean; +}; +/** + * Return value from the global recording hook. + */ +type GlobalRecordingHookOutput = { + /** + * Whether a global screen recording is currently active. + * Updates automatically as recordings start and stop. + */ + isRecording: boolean; +}; +/** + * React hook for monitoring and responding to global screen recording events. + * + * This hook automatically tracks the state of global screen recordings (recordings + * that capture the entire device screen, not just your app) and provides callbacks + * for when recordings start and finish. It also manages the timing of file retrieval + * to ensure the recording file is fully written before attempting to access it. + * + * **Key Features:** + * - Automatically tracks global recording state + * - Provides lifecycle callbacks for recording start/finish events + * - Handles timing delays for safe file retrieval + * - Filters out within-app recordings (only responds to global recordings) + * + * **Use Cases:** + * - Show recording indicators in your UI + * - Automatically upload or process completed recordings + * - Trigger analytics events for recording usage + * - Update app state based on recording activity + * + * @param props Configuration options for the hook + * @returns Object containing the current recording state + * + * @example + * ```tsx + * const { isRecording } = useGlobalRecording({ + * onRecordingStarted: () => { + * analytics.track('recording_started'); + * }, + * onBroadcastModalShown: () => { + * console.log("User tried to initiate recording") + * }, + * onBroadcastModalDismissed: () => { + * redirectToAnotherApp() + * }, + * onRecordingFinished: async (file) => { + * if (file) { + * try { + * await uploadRecording(file); + * showSuccessToast('Recording uploaded successfully!'); + * } catch (error) { + * showErrorToast('Failed to upload recording'); + * } + * } + * }, + * }); + * ``` + */ +export declare const useGlobalRecording: (props?: GlobalRecordingHookInput) => GlobalRecordingHookOutput; +export {}; +//# sourceMappingURL=useGlobalRecording.d.ts.map \ No newline at end of file diff --git a/lib/typescript/hooks/useGlobalRecording.d.ts.map b/lib/typescript/hooks/useGlobalRecording.d.ts.map new file mode 100644 index 0000000..8578193 --- /dev/null +++ b/lib/typescript/hooks/useGlobalRecording.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"useGlobalRecording.d.ts","sourceRoot":"","sources":["../../../src/hooks/useGlobalRecording.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAUpD;;GAEG;AACH,KAAK,wBAAwB,GAAG;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAC3D;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,IAAI,CAAC;IAInC,yBAAyB,CAAC,EAAE,MAAM,IAAI,CAAC;IACvC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,kCAAkC,CAAC,EAAE,OAAO,CAAC;CAC9C,CAAC;AAEF;;GAEG;AACH,KAAK,yBAAyB,GAAG;IAC/B;;;OAGG;IACH,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,wBAAwB,KAC/B,yBAsCF,CAAC"} \ No newline at end of file diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts new file mode 100644 index 0000000..7e3faa0 --- /dev/null +++ b/lib/typescript/index.d.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './functions'; +export * from './hooks'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/typescript/index.d.ts.map b/lib/typescript/index.d.ts.map new file mode 100644 index 0000000..7982343 --- /dev/null +++ b/lib/typescript/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"} \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts new file mode 100644 index 0000000..2c77d6d --- /dev/null +++ b/lib/typescript/types.d.ts @@ -0,0 +1,326 @@ +/** + * Represents the current status of a device permission. + * + * @example + * ```typescript + * const status: PermissionStatus = 'granted'; + * ``` + */ +export type PermissionStatus = 'denied' | 'granted' | 'undetermined'; +/** + * Represents when a permission expires. + * Most permissions never expire, but some may have a timestamp. + * + * @example + * ```typescript + * const expiration: PermissionExpiration = never; // Most common case + * const timedExpiration: PermissionExpiration = Date.now() + 3600000; // Expires in 1 hour + * ``` + */ +export type PermissionExpiration = never | number; +/** + * Complete response object returned when requesting device permissions. + * Contains all information about the permission state and user interaction. + * + * @example + * ```typescript + * const response: PermissionResponse = { + * canAskAgain: true, + * granted: true, + * status: 'granted', + * expiresAt: never + * }; + * ``` + */ +export type PermissionResponse = { + /** Whether the permission dialog can be shown again if denied */ + canAskAgain: boolean; + /** Simplified boolean indicating if permission was granted */ + granted: boolean; + /** Detailed permission status */ + status: PermissionStatus; + /** When this permission expires, if applicable */ + expiresAt: PermissionExpiration; +}; +/** + * Styling configuration for the camera preview overlay during recording. + * All dimensions are in points/pixels relative to the screen. + * + * @example + * ```typescript + * const cameraStyle: RecorderCameraStyle = { + * top: 50, + * left: 20, + * width: 120, + * height: 160, + * borderRadius: 8, + * borderWidth: 2 + * }; + * ``` + */ +export type RecorderCameraStyle = { + /** Distance from top of screen */ + top?: number; + /** Distance from left of screen */ + left?: number; + /** Width of camera preview */ + width?: number; + /** Height of camera preview */ + height?: number; + /** Corner radius for rounded corners */ + borderRadius?: number; + /** Border thickness around camera preview */ + borderWidth?: number; +}; +/** + * Specifies which camera to use for recording. + * + * @example + * ```typescript + * const camera: CameraDevice = 'front'; // For selfie camera + * const backCamera: CameraDevice = 'back'; // For rear camera + * ``` + */ +export type CameraDevice = 'front' | 'back'; +/** + * Recording configuration options. Uses discriminated union to ensure + * camera-related options are only available when camera is enabled. + * + * @example + * ```typescript + * // With camera enabled (iOS only) + * const withCamera: RecordingOptions = { + * enableMic: true, + * enableCamera: true, + * cameraPreviewStyle: { width: 100, height: 100 }, + * cameraDevice: 'front' + * }; + * + * // Without camera + * const withoutCamera: RecordingOptions = { + * enableCamera: false, + * enableMic: true + * }; + * ``` + */ +export type InAppRecordingOptions = { + /** Whether to record microphone audio */ + enableMic: boolean; + /** iOS Only: Camera is enabled - requires camera options */ + enableCamera: true; + /** Styling for camera preview overlay */ + cameraPreviewStyle: RecorderCameraStyle; + /** Which camera to use */ + cameraDevice: CameraDevice; + /** + * Whether to write audio to a separate file alongside the video. + * When enabled, the audioFile property will be populated in ScreenRecordingFile. + * @default false + */ + separateAudioFile?: boolean; +} | { + /** Camera is disabled - no camera options needed */ + enableCamera: false; + /** Whether to record microphone audio */ + enableMic: boolean; + /** + * Whether to write audio to a separate file alongside the video. + * When enabled, the audioFile property will be populated in ScreenRecordingFile. + * @default false + */ + separateAudioFile?: boolean; +}; +/** + * Complete input configuration for starting an in-app recording session. + * + * @example + * ```typescript + * const recordingInput: InAppRecordingInput = { + * options: { + * enableMic: true, + * enableCamera: true, + * cameraPreviewStyle: { width: 120, height: 160, top: 50, left: 20 }, + * cameraDevice: 'front' + * }, + * onRecordingFinished: (file) => { + * console.log('Recording completed:', file.path); + * } + * }; + * ``` + */ +export type InAppRecordingInput = { + /** Recording configuration options */ + options: InAppRecordingOptions; + /** Callback invoked when recording completes successfully */ + onRecordingFinished: (file: ScreenRecordingFile) => void; +}; +/** + * Options for a global screen recording session. + */ +export type GlobalRecordingInputOptions = { + /** Whether to record microphone audio during the global recording. */ + enableMic: boolean; + /** + * Whether to write audio to a separate file alongside the video. + * When enabled, the audioFile property will be populated in ScreenRecordingFile. + * The separate audio file will contain microphone audio (if enabled). + * + * On both iOS and Android, the video will contain embedded audio AND + * a separate audio file will be created. On Android, the audio is extracted + * from the video after recording stops. + * + * @default false + */ + separateAudioFile?: boolean; +}; +/** + * Complete input configuration for starting a global recording session. + * + * @example + * ```typescript + * const globalInput: GlobalRecordingInput = { + * options: { + * enableMic: true, // Enable microphone audio for the recording + * }, + * onRecordingError: (error) => { + * console.error('Global recording failed:', error.message); + * // Handle the error, e.g., display an alert to the user. + * } + * }; + * ``` + */ +export type GlobalRecordingInput = { + /** Optional configuration options for the global recording session. */ + options?: GlobalRecordingInputOptions; + /** Callback invoked when the global recording encounters an error during start or execution. */ + onRecordingError: (error: RecordingError) => void; +}; +/** + * Represents a separate audio file recorded alongside the video. + * + * @example + * ```typescript + * const audioFile: AudioRecordingFile = { + * path: '/path/to/recording.m4a', + * name: 'screen_recording_2024_01_15.m4a', + * size: 1048576, // 1MB in bytes + * duration: 30.5 // 30.5 seconds + * }; + * ``` + */ +export interface AudioRecordingFile { + /** Full file system path to the audio file */ + path: string; + /** Display name of the audio file */ + name: string; + /** File size in bytes */ + size: number; + /** Audio duration in seconds */ + duration: number; +} +/** + * Represents a completed screen recording file with metadata. + * Contains all information needed to access and display the recording. + * + * @example + * ```typescript + * const recordingFile: ScreenRecordingFile = { + * path: '/path/to/recording.mp4', + * name: 'screen_recording_2024_01_15.mp4', + * size: 15728640, // 15MB in bytes + * duration: 30.5, // 30.5 seconds + * enabledMicrophone: true, + * audioFile: { + * path: '/path/to/recording.m4a', + * name: 'screen_recording_2024_01_15.m4a', + * size: 1048576, + * duration: 30.5 + * } + * }; + * ``` + */ +export interface ScreenRecordingFile { + /** Full file system path to the recording */ + path: string; + /** Display name of the recording file */ + name: string; + /** File size in bytes */ + size: number; + /** Recording duration in seconds */ + duration: number; + /** Whether microphone audio was recorded */ + enabledMicrophone: boolean; + /** Optional separate audio file (when separateAudioFile option is enabled) */ + audioFile?: AudioRecordingFile; +} +/** + * Error object returned when recording operations fail. + * + * @example + * ```typescript + * const error: RecordingError = { + * name: 'PermissionError', + * message: 'Camera permission was denied by user' + * }; + * ``` + */ +export interface RecordingError { + /** Error type/category name */ + name: string; + /** Human-readable error description */ + message: string; +} +/** + * Indicates what happened in a recording lifecycle event. + * + * @example + * ```typescript + * const reason: RecordingEventReason = 'began'; // Recording started + * const endReason: RecordingEventReason = 'ended'; // Recording stopped + * ``` + */ +export type RecordingEventReason = 'began' | 'ended'; +/** + * Specifies the type of recording that triggered an event. + * Note: This type is deprecated but still supported for backwards compatibility. + * + * @example + * ```typescript + * const eventType: RecordingEventType = 'global'; // Global screen recording + * const appType: RecordingEventType = 'withinApp'; // In-app recording + * ``` + */ +export type RecordingEventType = 'global' | 'withinApp'; +/** + * Event object emitted during recording lifecycle changes. + * Provides information about what type of recording changed and how. + * + * @example + * ```typescript + * const event: ScreenRecordingEvent = { + * type: 'global', + * reason: 'began' + * }; + * + * // Usage in event listener + * addScreenRecordingListener((event) => { + * if (event.reason === 'began') { + * console.log(`${event.type} recording started`); + * } else { + * console.log(`${event.type} recording ended`); + * } + * }); + * ``` + */ +export interface ScreenRecordingEvent { + /** Type of recording (deprecated but still functional) */ + type: RecordingEventType; + /** What happened to the recording */ + reason: RecordingEventReason; +} +/** + * @platform ios-only + * Track the status of the broadcast picker view for fine tuning system recordings. + */ +export type BroadcastPickerPresentationEvent = 'showing' | 'dismissed'; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/lib/typescript/types.d.ts.map b/lib/typescript/types.d.ts.map new file mode 100644 index 0000000..db6f2b5 --- /dev/null +++ b/lib/typescript/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file From 2334bec1542b95237c4ff2ab6d5fb6dacaa95870 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 5 Dec 2025 17:52:01 -0800 Subject: [PATCH 04/32] chore: nitrogen --- .gitignore | 2 - nitrogen/generated/.gitattributes | 1 + .../android/c++/JAudioRecordingFile.hpp | 69 +++ .../c++/JBroadcastPickerPresentationEvent.hpp | 59 +++ .../generated/android/c++/JCameraDevice.hpp | 59 +++ ..._void_BroadcastPickerPresentationEvent.hpp | 76 ++++ .../android/c++/JFunc_void_RecordingError.hpp | 77 ++++ .../c++/JFunc_void_ScreenRecordingEvent.hpp | 80 ++++ .../c++/JFunc_void_ScreenRecordingFile.hpp | 80 ++++ .../c++/JHybridNitroScreenRecorderSpec.cpp | 222 ++++++++++ .../c++/JHybridNitroScreenRecorderSpec.hpp | 79 ++++ .../android/c++/JPermissionResponse.hpp | 70 ++++ .../android/c++/JPermissionStatus.hpp | 62 +++ .../android/c++/JRecorderCameraStyle.hpp | 77 ++++ .../generated/android/c++/JRecordingError.hpp | 61 +++ .../android/c++/JRecordingEventReason.hpp | 59 +++ .../android/c++/JRecordingEventType.hpp | 59 +++ .../android/c++/JScreenRecordingEvent.hpp | 64 +++ .../android/c++/JScreenRecordingFile.hpp | 80 ++++ .../nitroscreenrecorder/AudioRecordingFile.kt | 47 +++ .../BroadcastPickerPresentationEvent.kt | 21 + .../nitro/nitroscreenrecorder/CameraDevice.kt | 21 + ...c_void_BroadcastPickerPresentationEvent.kt | 80 ++++ .../Func_void_RecordingError.kt | 80 ++++ .../Func_void_ScreenRecordingEvent.kt | 80 ++++ .../Func_void_ScreenRecordingFile.kt | 80 ++++ .../HybridNitroScreenRecorderSpec.kt | 134 ++++++ .../nitroscreenrecorder/PermissionResponse.kt | 47 +++ .../nitroscreenrecorder/PermissionStatus.kt | 22 + .../RecorderCameraStyle.kt | 53 +++ .../nitroscreenrecorder/RecordingError.kt | 41 ++ .../RecordingEventReason.kt | 21 + .../nitroscreenrecorder/RecordingEventType.kt | 21 + .../ScreenRecordingEvent.kt | 41 ++ .../ScreenRecordingFile.kt | 53 +++ .../nitroscreenrecorderOnLoad.kt | 35 ++ .../nitroscreenrecorder+autolinking.cmake | 81 ++++ .../nitroscreenrecorder+autolinking.gradle | 27 ++ .../android/nitroscreenrecorderOnLoad.cpp | 52 +++ .../android/nitroscreenrecorderOnLoad.hpp | 25 ++ .../ios/NitroScreenRecorder+autolinking.rb | 60 +++ .../NitroScreenRecorder-Swift-Cxx-Bridge.cpp | 96 +++++ .../NitroScreenRecorder-Swift-Cxx-Bridge.hpp | 394 ++++++++++++++++++ ...NitroScreenRecorder-Swift-Cxx-Umbrella.hpp | 80 ++++ .../ios/NitroScreenRecorderAutolinking.mm | 33 ++ .../ios/NitroScreenRecorderAutolinking.swift | 25 ++ .../HybridNitroScreenRecorderSpecSwift.cpp | 11 + .../HybridNitroScreenRecorderSpecSwift.hpp | 213 ++++++++++ .../ios/swift/AudioRecordingFile.swift | 68 +++ .../BroadcastPickerPresentationEvent.swift | 40 ++ .../generated/ios/swift/CameraDevice.swift | 40 ++ nitrogen/generated/ios/swift/Func_void.swift | 47 +++ ...oid_BroadcastPickerPresentationEvent.swift | 47 +++ .../swift/Func_void_PermissionResponse.swift | 47 +++ .../ios/swift/Func_void_RecordingError.swift | 47 +++ .../Func_void_ScreenRecordingEvent.swift | 47 +++ .../swift/Func_void_ScreenRecordingFile.swift | 47 +++ .../swift/Func_void_std__exception_ptr.swift | 47 +++ ...d_std__optional_ScreenRecordingFile_.swift | 47 +++ .../swift/HybridNitroScreenRecorderSpec.swift | 71 ++++ .../HybridNitroScreenRecorderSpec_cxx.swift | 368 ++++++++++++++++ .../ios/swift/PermissionResponse.swift | 68 +++ .../ios/swift/PermissionStatus.swift | 44 ++ .../ios/swift/RecorderCameraStyle.swift | 162 +++++++ .../generated/ios/swift/RecordingError.swift | 46 ++ .../ios/swift/RecordingEventReason.swift | 40 ++ .../ios/swift/RecordingEventType.swift | 40 ++ .../ios/swift/ScreenRecordingEvent.swift | 46 ++ .../ios/swift/ScreenRecordingFile.swift | 102 +++++ .../shared/c++/AudioRecordingFile.hpp | 87 ++++ .../c++/BroadcastPickerPresentationEvent.hpp | 76 ++++ .../generated/shared/c++/CameraDevice.hpp | 76 ++++ .../c++/HybridNitroScreenRecorderSpec.cpp | 35 ++ .../c++/HybridNitroScreenRecorderSpec.hpp | 101 +++++ .../shared/c++/PermissionResponse.hpp | 88 ++++ .../generated/shared/c++/PermissionStatus.hpp | 80 ++++ .../shared/c++/RecorderCameraStyle.hpp | 95 +++++ .../generated/shared/c++/RecordingError.hpp | 79 ++++ .../shared/c++/RecordingEventReason.hpp | 76 ++++ .../shared/c++/RecordingEventType.hpp | 76 ++++ .../shared/c++/ScreenRecordingEvent.hpp | 83 ++++ .../shared/c++/ScreenRecordingFile.hpp | 98 +++++ 82 files changed, 5869 insertions(+), 2 deletions(-) create mode 100644 nitrogen/generated/.gitattributes create mode 100644 nitrogen/generated/android/c++/JAudioRecordingFile.hpp create mode 100644 nitrogen/generated/android/c++/JBroadcastPickerPresentationEvent.hpp create mode 100644 nitrogen/generated/android/c++/JCameraDevice.hpp create mode 100644 nitrogen/generated/android/c++/JFunc_void_BroadcastPickerPresentationEvent.hpp create mode 100644 nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp create mode 100644 nitrogen/generated/android/c++/JFunc_void_ScreenRecordingEvent.hpp create mode 100644 nitrogen/generated/android/c++/JFunc_void_ScreenRecordingFile.hpp create mode 100644 nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp create mode 100644 nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp create mode 100644 nitrogen/generated/android/c++/JPermissionResponse.hpp create mode 100644 nitrogen/generated/android/c++/JPermissionStatus.hpp create mode 100644 nitrogen/generated/android/c++/JRecorderCameraStyle.hpp create mode 100644 nitrogen/generated/android/c++/JRecordingError.hpp create mode 100644 nitrogen/generated/android/c++/JRecordingEventReason.hpp create mode 100644 nitrogen/generated/android/c++/JRecordingEventType.hpp create mode 100644 nitrogen/generated/android/c++/JScreenRecordingEvent.hpp create mode 100644 nitrogen/generated/android/c++/JScreenRecordingFile.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/AudioRecordingFile.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/BroadcastPickerPresentationEvent.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/CameraDevice.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_BroadcastPickerPresentationEvent.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingEvent.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingFile.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionResponse.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionStatus.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecorderCameraStyle.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventReason.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventType.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingEvent.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/nitroscreenrecorderOnLoad.kt create mode 100644 nitrogen/generated/android/nitroscreenrecorder+autolinking.cmake create mode 100644 nitrogen/generated/android/nitroscreenrecorder+autolinking.gradle create mode 100644 nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp create mode 100644 nitrogen/generated/android/nitroscreenrecorderOnLoad.hpp create mode 100644 nitrogen/generated/ios/NitroScreenRecorder+autolinking.rb create mode 100644 nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp create mode 100644 nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp create mode 100644 nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp create mode 100644 nitrogen/generated/ios/NitroScreenRecorderAutolinking.mm create mode 100644 nitrogen/generated/ios/NitroScreenRecorderAutolinking.swift create mode 100644 nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.cpp create mode 100644 nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp create mode 100644 nitrogen/generated/ios/swift/AudioRecordingFile.swift create mode 100644 nitrogen/generated/ios/swift/BroadcastPickerPresentationEvent.swift create mode 100644 nitrogen/generated/ios/swift/CameraDevice.swift create mode 100644 nitrogen/generated/ios/swift/Func_void.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_BroadcastPickerPresentationEvent.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_PermissionResponse.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_RecordingError.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_ScreenRecordingEvent.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_ScreenRecordingFile.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift create mode 100644 nitrogen/generated/ios/swift/Func_void_std__optional_ScreenRecordingFile_.swift create mode 100644 nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift create mode 100644 nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift create mode 100644 nitrogen/generated/ios/swift/PermissionResponse.swift create mode 100644 nitrogen/generated/ios/swift/PermissionStatus.swift create mode 100644 nitrogen/generated/ios/swift/RecorderCameraStyle.swift create mode 100644 nitrogen/generated/ios/swift/RecordingError.swift create mode 100644 nitrogen/generated/ios/swift/RecordingEventReason.swift create mode 100644 nitrogen/generated/ios/swift/RecordingEventType.swift create mode 100644 nitrogen/generated/ios/swift/ScreenRecordingEvent.swift create mode 100644 nitrogen/generated/ios/swift/ScreenRecordingFile.swift create mode 100644 nitrogen/generated/shared/c++/AudioRecordingFile.hpp create mode 100644 nitrogen/generated/shared/c++/BroadcastPickerPresentationEvent.hpp create mode 100644 nitrogen/generated/shared/c++/CameraDevice.hpp create mode 100644 nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.cpp create mode 100644 nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp create mode 100644 nitrogen/generated/shared/c++/PermissionResponse.hpp create mode 100644 nitrogen/generated/shared/c++/PermissionStatus.hpp create mode 100644 nitrogen/generated/shared/c++/RecorderCameraStyle.hpp create mode 100644 nitrogen/generated/shared/c++/RecordingError.hpp create mode 100644 nitrogen/generated/shared/c++/RecordingEventReason.hpp create mode 100644 nitrogen/generated/shared/c++/RecordingEventType.hpp create mode 100644 nitrogen/generated/shared/c++/ScreenRecordingEvent.hpp create mode 100644 nitrogen/generated/shared/c++/ScreenRecordingFile.hpp diff --git a/.gitignore b/.gitignore index 4e0faa7..4e855c0 100644 --- a/.gitignore +++ b/.gitignore @@ -82,5 +82,3 @@ android/keystores/debug.keystore ios/generated android/generated -# React Native Nitro Modules -nitrogen/ diff --git a/nitrogen/generated/.gitattributes b/nitrogen/generated/.gitattributes new file mode 100644 index 0000000..fb7a0d5 --- /dev/null +++ b/nitrogen/generated/.gitattributes @@ -0,0 +1 @@ +** linguist-generated=true diff --git a/nitrogen/generated/android/c++/JAudioRecordingFile.hpp b/nitrogen/generated/android/c++/JAudioRecordingFile.hpp new file mode 100644 index 0000000..8c3317d --- /dev/null +++ b/nitrogen/generated/android/c++/JAudioRecordingFile.hpp @@ -0,0 +1,69 @@ +/// +/// JAudioRecordingFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "AudioRecordingFile.hpp" + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "AudioRecordingFile" and the the Kotlin data class "AudioRecordingFile". + */ + struct JAudioRecordingFile final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/AudioRecordingFile;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct AudioRecordingFile by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + AudioRecordingFile toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldPath = clazz->getField("path"); + jni::local_ref path = this->getFieldValue(fieldPath); + static const auto fieldName = clazz->getField("name"); + jni::local_ref name = this->getFieldValue(fieldName); + static const auto fieldSize = clazz->getField("size"); + double size = this->getFieldValue(fieldSize); + static const auto fieldDuration = clazz->getField("duration"); + double duration = this->getFieldValue(fieldDuration); + return AudioRecordingFile( + path->toStdString(), + name->toStdString(), + size, + duration + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const AudioRecordingFile& value) { + using JSignature = JAudioRecordingFile(jni::alias_ref, jni::alias_ref, double, double); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.path), + jni::make_jstring(value.name), + value.size, + value.duration + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JBroadcastPickerPresentationEvent.hpp b/nitrogen/generated/android/c++/JBroadcastPickerPresentationEvent.hpp new file mode 100644 index 0000000..5f5eb64 --- /dev/null +++ b/nitrogen/generated/android/c++/JBroadcastPickerPresentationEvent.hpp @@ -0,0 +1,59 @@ +/// +/// JBroadcastPickerPresentationEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "BroadcastPickerPresentationEvent.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "BroadcastPickerPresentationEvent" and the the Kotlin enum "BroadcastPickerPresentationEvent". + */ + struct JBroadcastPickerPresentationEvent final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/BroadcastPickerPresentationEvent;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum BroadcastPickerPresentationEvent. + */ + [[maybe_unused]] + [[nodiscard]] + BroadcastPickerPresentationEvent toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(BroadcastPickerPresentationEvent value) { + static const auto clazz = javaClassStatic(); + static const auto fieldSHOWING = clazz->getStaticField("SHOWING"); + static const auto fieldDISMISSED = clazz->getStaticField("DISMISSED"); + + switch (value) { + case BroadcastPickerPresentationEvent::SHOWING: + return clazz->getStaticFieldValue(fieldSHOWING); + case BroadcastPickerPresentationEvent::DISMISSED: + return clazz->getStaticFieldValue(fieldDISMISSED); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JCameraDevice.hpp b/nitrogen/generated/android/c++/JCameraDevice.hpp new file mode 100644 index 0000000..617ff74 --- /dev/null +++ b/nitrogen/generated/android/c++/JCameraDevice.hpp @@ -0,0 +1,59 @@ +/// +/// JCameraDevice.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "CameraDevice.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "CameraDevice" and the the Kotlin enum "CameraDevice". + */ + struct JCameraDevice final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/CameraDevice;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum CameraDevice. + */ + [[maybe_unused]] + [[nodiscard]] + CameraDevice toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(CameraDevice value) { + static const auto clazz = javaClassStatic(); + static const auto fieldFRONT = clazz->getStaticField("FRONT"); + static const auto fieldBACK = clazz->getStaticField("BACK"); + + switch (value) { + case CameraDevice::FRONT: + return clazz->getStaticFieldValue(fieldFRONT); + case CameraDevice::BACK: + return clazz->getStaticFieldValue(fieldBACK); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JFunc_void_BroadcastPickerPresentationEvent.hpp b/nitrogen/generated/android/c++/JFunc_void_BroadcastPickerPresentationEvent.hpp new file mode 100644 index 0000000..44ff6c6 --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_BroadcastPickerPresentationEvent.hpp @@ -0,0 +1,76 @@ +/// +/// JFunc_void_BroadcastPickerPresentationEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include "BroadcastPickerPresentationEvent.hpp" +#include +#include "JBroadcastPickerPresentationEvent.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(event: BroadcastPickerPresentationEvent) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_BroadcastPickerPresentationEvent: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_BroadcastPickerPresentationEvent;"; + + public: + /** + * Invokes the function this `JFunc_void_BroadcastPickerPresentationEvent` instance holds through JNI. + */ + void invoke(BroadcastPickerPresentationEvent event) const { + static const auto method = javaClassStatic()->getMethod /* event */)>("invoke"); + method(self(), JBroadcastPickerPresentationEvent::fromCpp(event)); + } + }; + + /** + * An implementation of Func_void_BroadcastPickerPresentationEvent that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_BroadcastPickerPresentationEvent_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_BroadcastPickerPresentationEvent_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_BroadcastPickerPresentationEvent_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref event) { + _func(event->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_BroadcastPickerPresentationEvent_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_BroadcastPickerPresentationEvent_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_BroadcastPickerPresentationEvent_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp b/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp new file mode 100644 index 0000000..dce2ade --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp @@ -0,0 +1,77 @@ +/// +/// JFunc_void_RecordingError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include "RecordingError.hpp" +#include +#include "JRecordingError.hpp" +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(error: RecordingError) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_RecordingError: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError;"; + + public: + /** + * Invokes the function this `JFunc_void_RecordingError` instance holds through JNI. + */ + void invoke(const RecordingError& error) const { + static const auto method = javaClassStatic()->getMethod /* error */)>("invoke"); + method(self(), JRecordingError::fromCpp(error)); + } + }; + + /** + * An implementation of Func_void_RecordingError that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_RecordingError_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_RecordingError_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_RecordingError_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref error) { + _func(error->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_RecordingError_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_RecordingError_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JFunc_void_ScreenRecordingEvent.hpp b/nitrogen/generated/android/c++/JFunc_void_ScreenRecordingEvent.hpp new file mode 100644 index 0000000..7d060dc --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_ScreenRecordingEvent.hpp @@ -0,0 +1,80 @@ +/// +/// JFunc_void_ScreenRecordingEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include "ScreenRecordingEvent.hpp" +#include +#include "JScreenRecordingEvent.hpp" +#include "RecordingEventType.hpp" +#include "JRecordingEventType.hpp" +#include "RecordingEventReason.hpp" +#include "JRecordingEventReason.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(event: ScreenRecordingEvent) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_ScreenRecordingEvent: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingEvent;"; + + public: + /** + * Invokes the function this `JFunc_void_ScreenRecordingEvent` instance holds through JNI. + */ + void invoke(const ScreenRecordingEvent& event) const { + static const auto method = javaClassStatic()->getMethod /* event */)>("invoke"); + method(self(), JScreenRecordingEvent::fromCpp(event)); + } + }; + + /** + * An implementation of Func_void_ScreenRecordingEvent that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_ScreenRecordingEvent_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_ScreenRecordingEvent_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_ScreenRecordingEvent_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref event) { + _func(event->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingEvent_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_ScreenRecordingEvent_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_ScreenRecordingEvent_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JFunc_void_ScreenRecordingFile.hpp b/nitrogen/generated/android/c++/JFunc_void_ScreenRecordingFile.hpp new file mode 100644 index 0000000..3e859a3 --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_ScreenRecordingFile.hpp @@ -0,0 +1,80 @@ +/// +/// JFunc_void_ScreenRecordingFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include "ScreenRecordingFile.hpp" +#include +#include "JScreenRecordingFile.hpp" +#include +#include "AudioRecordingFile.hpp" +#include +#include "JAudioRecordingFile.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(file: ScreenRecordingFile) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_ScreenRecordingFile: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingFile;"; + + public: + /** + * Invokes the function this `JFunc_void_ScreenRecordingFile` instance holds through JNI. + */ + void invoke(const ScreenRecordingFile& file) const { + static const auto method = javaClassStatic()->getMethod /* file */)>("invoke"); + method(self(), JScreenRecordingFile::fromCpp(file)); + } + }; + + /** + * An implementation of Func_void_ScreenRecordingFile that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_ScreenRecordingFile_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_ScreenRecordingFile_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_ScreenRecordingFile_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref file) { + _func(file->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingFile_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_ScreenRecordingFile_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_ScreenRecordingFile_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp new file mode 100644 index 0000000..a1cb1e8 --- /dev/null +++ b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp @@ -0,0 +1,222 @@ +/// +/// JHybridNitroScreenRecorderSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#include "JHybridNitroScreenRecorderSpec.hpp" + +// Forward declaration of `PermissionStatus` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } +// Forward declaration of `PermissionResponse` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } +// Forward declaration of `ScreenRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } +// Forward declaration of `AudioRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } +// Forward declaration of `ScreenRecordingEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingEvent; } +// Forward declaration of `RecordingEventType` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventType; } +// Forward declaration of `RecordingEventReason` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } +// Forward declaration of `BroadcastPickerPresentationEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresentationEvent; } +// Forward declaration of `RecorderCameraStyle` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } +// Forward declaration of `CameraDevice` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } + +#include "PermissionStatus.hpp" +#include "JPermissionStatus.hpp" +#include "PermissionResponse.hpp" +#include +#include +#include "JPermissionResponse.hpp" +#include "ScreenRecordingFile.hpp" +#include +#include "JScreenRecordingFile.hpp" +#include +#include "AudioRecordingFile.hpp" +#include "JAudioRecordingFile.hpp" +#include "ScreenRecordingEvent.hpp" +#include +#include "JFunc_void_ScreenRecordingEvent.hpp" +#include "JScreenRecordingEvent.hpp" +#include "RecordingEventType.hpp" +#include "JRecordingEventType.hpp" +#include "RecordingEventReason.hpp" +#include "JRecordingEventReason.hpp" +#include "BroadcastPickerPresentationEvent.hpp" +#include "JFunc_void_BroadcastPickerPresentationEvent.hpp" +#include "JBroadcastPickerPresentationEvent.hpp" +#include "RecorderCameraStyle.hpp" +#include "JRecorderCameraStyle.hpp" +#include "CameraDevice.hpp" +#include "JCameraDevice.hpp" +#include "JFunc_void_ScreenRecordingFile.hpp" +#include "RecordingError.hpp" +#include "JFunc_void_RecordingError.hpp" +#include "JRecordingError.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + jni::local_ref JHybridNitroScreenRecorderSpec::initHybrid(jni::alias_ref jThis) { + return makeCxxInstance(jThis); + } + + void JHybridNitroScreenRecorderSpec::registerNatives() { + registerHybrid({ + makeNativeMethod("initHybrid", JHybridNitroScreenRecorderSpec::initHybrid), + }); + } + + size_t JHybridNitroScreenRecorderSpec::getExternalMemorySize() noexcept { + static const auto method = javaClassStatic()->getMethod("getMemorySize"); + return method(_javaPart); + } + + void JHybridNitroScreenRecorderSpec::dispose() noexcept { + static const auto method = javaClassStatic()->getMethod("dispose"); + method(_javaPart); + } + + std::string JHybridNitroScreenRecorderSpec::toString() { + static const auto method = javaClassStatic()->getMethod("toString"); + auto javaString = method(_javaPart); + return javaString->toStdString(); + } + + // Properties + + + // Methods + PermissionStatus JHybridNitroScreenRecorderSpec::getCameraPermissionStatus() { + static const auto method = javaClassStatic()->getMethod()>("getCameraPermissionStatus"); + auto __result = method(_javaPart); + return __result->toCpp(); + } + PermissionStatus JHybridNitroScreenRecorderSpec::getMicrophonePermissionStatus() { + static const auto method = javaClassStatic()->getMethod()>("getMicrophonePermissionStatus"); + auto __result = method(_javaPart); + return __result->toCpp(); + } + std::shared_ptr> JHybridNitroScreenRecorderSpec::requestCameraPermission() { + static const auto method = javaClassStatic()->getMethod()>("requestCameraPermission"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridNitroScreenRecorderSpec::requestMicrophonePermission() { + static const auto method = javaClassStatic()->getMethod()>("requestMicrophonePermission"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->toCpp()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + double JHybridNitroScreenRecorderSpec::addScreenRecordingListener(bool ignoreRecordingsInitiatedElsewhere, const std::function& callback) { + static const auto method = javaClassStatic()->getMethod /* callback */)>("addScreenRecordingListener_cxx"); + auto __result = method(_javaPart, ignoreRecordingsInitiatedElsewhere, JFunc_void_ScreenRecordingEvent_cxx::fromCpp(callback)); + return __result; + } + void JHybridNitroScreenRecorderSpec::removeScreenRecordingListener(double id) { + static const auto method = javaClassStatic()->getMethod("removeScreenRecordingListener"); + method(_javaPart, id); + } + double JHybridNitroScreenRecorderSpec::addBroadcastPickerListener(const std::function& callback) { + static const auto method = javaClassStatic()->getMethod /* callback */)>("addBroadcastPickerListener_cxx"); + auto __result = method(_javaPart, JFunc_void_BroadcastPickerPresentationEvent_cxx::fromCpp(callback)); + return __result; + } + void JHybridNitroScreenRecorderSpec::removeBroadcastPickerListener(double id) { + static const auto method = javaClassStatic()->getMethod("removeBroadcastPickerListener"); + method(_javaPart, id); + } + void JHybridNitroScreenRecorderSpec::startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) { + static const auto method = javaClassStatic()->getMethod /* cameraPreviewStyle */, jni::alias_ref /* cameraDevice */, jboolean /* separateAudioFile */, jni::alias_ref /* onRecordingFinished */)>("startInAppRecording_cxx"); + method(_javaPart, enableMic, enableCamera, JRecorderCameraStyle::fromCpp(cameraPreviewStyle), JCameraDevice::fromCpp(cameraDevice), separateAudioFile, JFunc_void_ScreenRecordingFile_cxx::fromCpp(onRecordingFinished)); + } + std::shared_ptr>> JHybridNitroScreenRecorderSpec::stopInAppRecording() { + static const auto method = javaClassStatic()->getMethod()>("stopInAppRecording"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::shared_ptr> JHybridNitroScreenRecorderSpec::cancelInAppRecording() { + static const auto method = javaClassStatic()->getMethod()>("cancelInAppRecording"); + auto __result = method(_javaPart); + return [&]() { + auto __promise = Promise::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& /* unit */) { + __promise->resolve(); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + void JHybridNitroScreenRecorderSpec::startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) { + static const auto method = javaClassStatic()->getMethod /* onRecordingError */)>("startGlobalRecording_cxx"); + method(_javaPart, enableMic, separateAudioFile, JFunc_void_RecordingError_cxx::fromCpp(onRecordingError)); + } + std::shared_ptr>> JHybridNitroScreenRecorderSpec::stopGlobalRecording(double settledTimeMs) { + static const auto method = javaClassStatic()->getMethod(double /* settledTimeMs */)>("stopGlobalRecording"); + auto __result = method(_javaPart, settledTimeMs); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } + std::optional JHybridNitroScreenRecorderSpec::retrieveLastGlobalRecording() { + static const auto method = javaClassStatic()->getMethod()>("retrieveLastGlobalRecording"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; + } + void JHybridNitroScreenRecorderSpec::clearRecordingCache() { + static const auto method = javaClassStatic()->getMethod("clearRecordingCache"); + method(_javaPart); + } + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp new file mode 100644 index 0000000..ab5c30f --- /dev/null +++ b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp @@ -0,0 +1,79 @@ +/// +/// HybridNitroScreenRecorderSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include +#include "HybridNitroScreenRecorderSpec.hpp" + + + + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + class JHybridNitroScreenRecorderSpec: public jni::HybridClass, + public virtual HybridNitroScreenRecorderSpec { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec;"; + static jni::local_ref initHybrid(jni::alias_ref jThis); + static void registerNatives(); + + protected: + // C++ constructor (called from Java via `initHybrid()`) + explicit JHybridNitroScreenRecorderSpec(jni::alias_ref jThis) : + HybridObject(HybridNitroScreenRecorderSpec::TAG), + HybridBase(jThis), + _javaPart(jni::make_global(jThis)) {} + + public: + ~JHybridNitroScreenRecorderSpec() override { + // Hermes GC can destroy JS objects on a non-JNI Thread. + jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); }); + } + + public: + size_t getExternalMemorySize() noexcept override; + void dispose() noexcept override; + std::string toString() override; + + public: + inline const jni::global_ref& getJavaPart() const noexcept { + return _javaPart; + } + + public: + // Properties + + + public: + // Methods + PermissionStatus getCameraPermissionStatus() override; + PermissionStatus getMicrophonePermissionStatus() override; + std::shared_ptr> requestCameraPermission() override; + std::shared_ptr> requestMicrophonePermission() override; + double addScreenRecordingListener(bool ignoreRecordingsInitiatedElsewhere, const std::function& callback) override; + void removeScreenRecordingListener(double id) override; + double addBroadcastPickerListener(const std::function& callback) override; + void removeBroadcastPickerListener(double id) override; + void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) override; + std::shared_ptr>> stopInAppRecording() override; + std::shared_ptr> cancelInAppRecording() override; + void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) override; + std::shared_ptr>> stopGlobalRecording(double settledTimeMs) override; + std::optional retrieveLastGlobalRecording() override; + void clearRecordingCache() override; + + private: + friend HybridBase; + using HybridBase::HybridBase; + jni::global_ref _javaPart; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JPermissionResponse.hpp b/nitrogen/generated/android/c++/JPermissionResponse.hpp new file mode 100644 index 0000000..35da220 --- /dev/null +++ b/nitrogen/generated/android/c++/JPermissionResponse.hpp @@ -0,0 +1,70 @@ +/// +/// JPermissionResponse.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "PermissionResponse.hpp" + +#include "JPermissionStatus.hpp" +#include "PermissionStatus.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "PermissionResponse" and the the Kotlin data class "PermissionResponse". + */ + struct JPermissionResponse final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/PermissionResponse;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct PermissionResponse by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + PermissionResponse toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldCanAskAgain = clazz->getField("canAskAgain"); + jboolean canAskAgain = this->getFieldValue(fieldCanAskAgain); + static const auto fieldGranted = clazz->getField("granted"); + jboolean granted = this->getFieldValue(fieldGranted); + static const auto fieldStatus = clazz->getField("status"); + jni::local_ref status = this->getFieldValue(fieldStatus); + static const auto fieldExpiresAt = clazz->getField("expiresAt"); + double expiresAt = this->getFieldValue(fieldExpiresAt); + return PermissionResponse( + static_cast(canAskAgain), + static_cast(granted), + status->toCpp(), + expiresAt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const PermissionResponse& value) { + using JSignature = JPermissionResponse(jboolean, jboolean, jni::alias_ref, double); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + value.canAskAgain, + value.granted, + JPermissionStatus::fromCpp(value.status), + value.expiresAt + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JPermissionStatus.hpp b/nitrogen/generated/android/c++/JPermissionStatus.hpp new file mode 100644 index 0000000..b35a4d3 --- /dev/null +++ b/nitrogen/generated/android/c++/JPermissionStatus.hpp @@ -0,0 +1,62 @@ +/// +/// JPermissionStatus.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "PermissionStatus.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "PermissionStatus" and the the Kotlin enum "PermissionStatus". + */ + struct JPermissionStatus final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/PermissionStatus;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum PermissionStatus. + */ + [[maybe_unused]] + [[nodiscard]] + PermissionStatus toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(PermissionStatus value) { + static const auto clazz = javaClassStatic(); + static const auto fieldDENIED = clazz->getStaticField("DENIED"); + static const auto fieldGRANTED = clazz->getStaticField("GRANTED"); + static const auto fieldUNDETERMINED = clazz->getStaticField("UNDETERMINED"); + + switch (value) { + case PermissionStatus::DENIED: + return clazz->getStaticFieldValue(fieldDENIED); + case PermissionStatus::GRANTED: + return clazz->getStaticFieldValue(fieldGRANTED); + case PermissionStatus::UNDETERMINED: + return clazz->getStaticFieldValue(fieldUNDETERMINED); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JRecorderCameraStyle.hpp b/nitrogen/generated/android/c++/JRecorderCameraStyle.hpp new file mode 100644 index 0000000..a92f315 --- /dev/null +++ b/nitrogen/generated/android/c++/JRecorderCameraStyle.hpp @@ -0,0 +1,77 @@ +/// +/// JRecorderCameraStyle.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "RecorderCameraStyle.hpp" + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "RecorderCameraStyle" and the the Kotlin data class "RecorderCameraStyle". + */ + struct JRecorderCameraStyle final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/RecorderCameraStyle;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct RecorderCameraStyle by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + RecorderCameraStyle toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldTop = clazz->getField("top"); + jni::local_ref top = this->getFieldValue(fieldTop); + static const auto fieldLeft = clazz->getField("left"); + jni::local_ref left = this->getFieldValue(fieldLeft); + static const auto fieldWidth = clazz->getField("width"); + jni::local_ref width = this->getFieldValue(fieldWidth); + static const auto fieldHeight = clazz->getField("height"); + jni::local_ref height = this->getFieldValue(fieldHeight); + static const auto fieldBorderRadius = clazz->getField("borderRadius"); + jni::local_ref borderRadius = this->getFieldValue(fieldBorderRadius); + static const auto fieldBorderWidth = clazz->getField("borderWidth"); + jni::local_ref borderWidth = this->getFieldValue(fieldBorderWidth); + return RecorderCameraStyle( + top != nullptr ? std::make_optional(top->value()) : std::nullopt, + left != nullptr ? std::make_optional(left->value()) : std::nullopt, + width != nullptr ? std::make_optional(width->value()) : std::nullopt, + height != nullptr ? std::make_optional(height->value()) : std::nullopt, + borderRadius != nullptr ? std::make_optional(borderRadius->value()) : std::nullopt, + borderWidth != nullptr ? std::make_optional(borderWidth->value()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const RecorderCameraStyle& value) { + using JSignature = JRecorderCameraStyle(jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + value.top.has_value() ? jni::JDouble::valueOf(value.top.value()) : nullptr, + value.left.has_value() ? jni::JDouble::valueOf(value.left.value()) : nullptr, + value.width.has_value() ? jni::JDouble::valueOf(value.width.value()) : nullptr, + value.height.has_value() ? jni::JDouble::valueOf(value.height.value()) : nullptr, + value.borderRadius.has_value() ? jni::JDouble::valueOf(value.borderRadius.value()) : nullptr, + value.borderWidth.has_value() ? jni::JDouble::valueOf(value.borderWidth.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JRecordingError.hpp b/nitrogen/generated/android/c++/JRecordingError.hpp new file mode 100644 index 0000000..ecbb2f7 --- /dev/null +++ b/nitrogen/generated/android/c++/JRecordingError.hpp @@ -0,0 +1,61 @@ +/// +/// JRecordingError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "RecordingError.hpp" + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "RecordingError" and the the Kotlin data class "RecordingError". + */ + struct JRecordingError final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/RecordingError;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct RecordingError by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + RecordingError toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldName = clazz->getField("name"); + jni::local_ref name = this->getFieldValue(fieldName); + static const auto fieldMessage = clazz->getField("message"); + jni::local_ref message = this->getFieldValue(fieldMessage); + return RecordingError( + name->toStdString(), + message->toStdString() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const RecordingError& value) { + using JSignature = JRecordingError(jni::alias_ref, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.name), + jni::make_jstring(value.message) + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JRecordingEventReason.hpp b/nitrogen/generated/android/c++/JRecordingEventReason.hpp new file mode 100644 index 0000000..5e8d46e --- /dev/null +++ b/nitrogen/generated/android/c++/JRecordingEventReason.hpp @@ -0,0 +1,59 @@ +/// +/// JRecordingEventReason.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "RecordingEventReason.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "RecordingEventReason" and the the Kotlin enum "RecordingEventReason". + */ + struct JRecordingEventReason final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/RecordingEventReason;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum RecordingEventReason. + */ + [[maybe_unused]] + [[nodiscard]] + RecordingEventReason toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(RecordingEventReason value) { + static const auto clazz = javaClassStatic(); + static const auto fieldBEGAN = clazz->getStaticField("BEGAN"); + static const auto fieldENDED = clazz->getStaticField("ENDED"); + + switch (value) { + case RecordingEventReason::BEGAN: + return clazz->getStaticFieldValue(fieldBEGAN); + case RecordingEventReason::ENDED: + return clazz->getStaticFieldValue(fieldENDED); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JRecordingEventType.hpp b/nitrogen/generated/android/c++/JRecordingEventType.hpp new file mode 100644 index 0000000..5a6cc33 --- /dev/null +++ b/nitrogen/generated/android/c++/JRecordingEventType.hpp @@ -0,0 +1,59 @@ +/// +/// JRecordingEventType.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "RecordingEventType.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "RecordingEventType" and the the Kotlin enum "RecordingEventType". + */ + struct JRecordingEventType final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/RecordingEventType;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum RecordingEventType. + */ + [[maybe_unused]] + [[nodiscard]] + RecordingEventType toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(RecordingEventType value) { + static const auto clazz = javaClassStatic(); + static const auto fieldGLOBAL = clazz->getStaticField("GLOBAL"); + static const auto fieldWITHINAPP = clazz->getStaticField("WITHINAPP"); + + switch (value) { + case RecordingEventType::GLOBAL: + return clazz->getStaticFieldValue(fieldGLOBAL); + case RecordingEventType::WITHINAPP: + return clazz->getStaticFieldValue(fieldWITHINAPP); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JScreenRecordingEvent.hpp b/nitrogen/generated/android/c++/JScreenRecordingEvent.hpp new file mode 100644 index 0000000..9cd3301 --- /dev/null +++ b/nitrogen/generated/android/c++/JScreenRecordingEvent.hpp @@ -0,0 +1,64 @@ +/// +/// JScreenRecordingEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "ScreenRecordingEvent.hpp" + +#include "JRecordingEventReason.hpp" +#include "JRecordingEventType.hpp" +#include "RecordingEventReason.hpp" +#include "RecordingEventType.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "ScreenRecordingEvent" and the the Kotlin data class "ScreenRecordingEvent". + */ + struct JScreenRecordingEvent final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/ScreenRecordingEvent;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct ScreenRecordingEvent by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + ScreenRecordingEvent toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldType = clazz->getField("type"); + jni::local_ref type = this->getFieldValue(fieldType); + static const auto fieldReason = clazz->getField("reason"); + jni::local_ref reason = this->getFieldValue(fieldReason); + return ScreenRecordingEvent( + type->toCpp(), + reason->toCpp() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const ScreenRecordingEvent& value) { + using JSignature = JScreenRecordingEvent(jni::alias_ref, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + JRecordingEventType::fromCpp(value.type), + JRecordingEventReason::fromCpp(value.reason) + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JScreenRecordingFile.hpp b/nitrogen/generated/android/c++/JScreenRecordingFile.hpp new file mode 100644 index 0000000..2179296 --- /dev/null +++ b/nitrogen/generated/android/c++/JScreenRecordingFile.hpp @@ -0,0 +1,80 @@ +/// +/// JScreenRecordingFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "ScreenRecordingFile.hpp" + +#include "AudioRecordingFile.hpp" +#include "JAudioRecordingFile.hpp" +#include +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "ScreenRecordingFile" and the the Kotlin data class "ScreenRecordingFile". + */ + struct JScreenRecordingFile final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct ScreenRecordingFile by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + ScreenRecordingFile toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldPath = clazz->getField("path"); + jni::local_ref path = this->getFieldValue(fieldPath); + static const auto fieldName = clazz->getField("name"); + jni::local_ref name = this->getFieldValue(fieldName); + static const auto fieldSize = clazz->getField("size"); + double size = this->getFieldValue(fieldSize); + static const auto fieldDuration = clazz->getField("duration"); + double duration = this->getFieldValue(fieldDuration); + static const auto fieldEnabledMicrophone = clazz->getField("enabledMicrophone"); + jboolean enabledMicrophone = this->getFieldValue(fieldEnabledMicrophone); + static const auto fieldAudioFile = clazz->getField("audioFile"); + jni::local_ref audioFile = this->getFieldValue(fieldAudioFile); + return ScreenRecordingFile( + path->toStdString(), + name->toStdString(), + size, + duration, + static_cast(enabledMicrophone), + audioFile != nullptr ? std::make_optional(audioFile->toCpp()) : std::nullopt + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const ScreenRecordingFile& value) { + using JSignature = JScreenRecordingFile(jni::alias_ref, jni::alias_ref, double, double, jboolean, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.path), + jni::make_jstring(value.name), + value.size, + value.duration, + value.enabledMicrophone, + value.audioFile.has_value() ? JAudioRecordingFile::fromCpp(value.audioFile.value()) : nullptr + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/AudioRecordingFile.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/AudioRecordingFile.kt new file mode 100644 index 0000000..f7ebc09 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/AudioRecordingFile.kt @@ -0,0 +1,47 @@ +/// +/// AudioRecordingFile.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "AudioRecordingFile". + */ +@DoNotStrip +@Keep +data class AudioRecordingFile( + @DoNotStrip + @Keep + val path: String, + @DoNotStrip + @Keep + val name: String, + @DoNotStrip + @Keep + val size: Double, + @DoNotStrip + @Keep + val duration: Double +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(path: String, name: String, size: Double, duration: Double): AudioRecordingFile { + return AudioRecordingFile(path, name, size, duration) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/BroadcastPickerPresentationEvent.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/BroadcastPickerPresentationEvent.kt new file mode 100644 index 0000000..4e1b157 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/BroadcastPickerPresentationEvent.kt @@ -0,0 +1,21 @@ +/// +/// BroadcastPickerPresentationEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "BroadcastPickerPresentationEvent". + */ +@DoNotStrip +@Keep +enum class BroadcastPickerPresentationEvent(@DoNotStrip @Keep val value: Int) { + SHOWING(0), + DISMISSED(1); +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/CameraDevice.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/CameraDevice.kt new file mode 100644 index 0000000..e690681 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/CameraDevice.kt @@ -0,0 +1,21 @@ +/// +/// CameraDevice.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "CameraDevice". + */ +@DoNotStrip +@Keep +enum class CameraDevice(@DoNotStrip @Keep val value: Int) { + FRONT(0), + BACK(1); +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_BroadcastPickerPresentationEvent.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_BroadcastPickerPresentationEvent.kt new file mode 100644 index 0000000..921db0c --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_BroadcastPickerPresentationEvent.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_BroadcastPickerPresentationEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(event: enum) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_BroadcastPickerPresentationEvent: (BroadcastPickerPresentationEvent) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(event: BroadcastPickerPresentationEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: enum) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_BroadcastPickerPresentationEvent_cxx: Func_void_BroadcastPickerPresentationEvent { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(event: BroadcastPickerPresentationEvent): Unit + = invoke_cxx(event) + + @FastNative + private external fun invoke_cxx(event: BroadcastPickerPresentationEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: enum) => void`. + * This is implemented in Java/Kotlin, via a `(BroadcastPickerPresentationEvent) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_BroadcastPickerPresentationEvent_java(private val function: (BroadcastPickerPresentationEvent) -> Unit): Func_void_BroadcastPickerPresentationEvent { + @DoNotStrip + @Keep + override fun invoke(event: BroadcastPickerPresentationEvent): Unit { + return this.function(event) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt new file mode 100644 index 0000000..2256914 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_RecordingError.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(error: struct) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_RecordingError: (RecordingError) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(error: RecordingError): Unit +} + +/** + * Represents the JavaScript callback `(error: struct) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_RecordingError_cxx: Func_void_RecordingError { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(error: RecordingError): Unit + = invoke_cxx(error) + + @FastNative + private external fun invoke_cxx(error: RecordingError): Unit +} + +/** + * Represents the JavaScript callback `(error: struct) => void`. + * This is implemented in Java/Kotlin, via a `(RecordingError) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_RecordingError_java(private val function: (RecordingError) -> Unit): Func_void_RecordingError { + @DoNotStrip + @Keep + override fun invoke(error: RecordingError): Unit { + return this.function(error) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingEvent.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingEvent.kt new file mode 100644 index 0000000..d60fee3 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingEvent.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_ScreenRecordingEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_ScreenRecordingEvent: (ScreenRecordingEvent) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(event: ScreenRecordingEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_ScreenRecordingEvent_cxx: Func_void_ScreenRecordingEvent { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(event: ScreenRecordingEvent): Unit + = invoke_cxx(event) + + @FastNative + private external fun invoke_cxx(event: ScreenRecordingEvent): Unit +} + +/** + * Represents the JavaScript callback `(event: struct) => void`. + * This is implemented in Java/Kotlin, via a `(ScreenRecordingEvent) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_ScreenRecordingEvent_java(private val function: (ScreenRecordingEvent) -> Unit): Func_void_ScreenRecordingEvent { + @DoNotStrip + @Keep + override fun invoke(event: ScreenRecordingEvent): Unit { + return this.function(event) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingFile.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingFile.kt new file mode 100644 index 0000000..9433558 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_ScreenRecordingFile.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_ScreenRecordingFile.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(file: struct) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_ScreenRecordingFile: (ScreenRecordingFile) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(file: ScreenRecordingFile): Unit +} + +/** + * Represents the JavaScript callback `(file: struct) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_ScreenRecordingFile_cxx: Func_void_ScreenRecordingFile { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(file: ScreenRecordingFile): Unit + = invoke_cxx(file) + + @FastNative + private external fun invoke_cxx(file: ScreenRecordingFile): Unit +} + +/** + * Represents the JavaScript callback `(file: struct) => void`. + * This is implemented in Java/Kotlin, via a `(ScreenRecordingFile) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_ScreenRecordingFile_java(private val function: (ScreenRecordingFile) -> Unit): Func_void_ScreenRecordingFile { + @DoNotStrip + @Keep + override fun invoke(file: ScreenRecordingFile): Unit { + return this.function(file) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt new file mode 100644 index 0000000..5bbd299 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt @@ -0,0 +1,134 @@ +/// +/// HybridNitroScreenRecorderSpec.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import com.margelo.nitro.core.Promise +import com.margelo.nitro.core.HybridObject + +/** + * A Kotlin class representing the NitroScreenRecorder HybridObject. + * Implement this abstract class to create Kotlin-based instances of NitroScreenRecorder. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "SimpleRedundantLet", + "LocalVariableName", "PropertyName", "PrivatePropertyName", "FunctionName" +) +abstract class HybridNitroScreenRecorderSpec: HybridObject() { + @DoNotStrip + private var mHybridData: HybridData = initHybrid() + + init { + super.updateNative(mHybridData) + } + + override fun updateNative(hybridData: HybridData) { + mHybridData = hybridData + super.updateNative(hybridData) + } + + // Default implementation of `HybridObject.toString()` + override fun toString(): String { + return "[HybridObject NitroScreenRecorder]" + } + + // Properties + + + // Methods + @DoNotStrip + @Keep + abstract fun getCameraPermissionStatus(): PermissionStatus + + @DoNotStrip + @Keep + abstract fun getMicrophonePermissionStatus(): PermissionStatus + + @DoNotStrip + @Keep + abstract fun requestCameraPermission(): Promise + + @DoNotStrip + @Keep + abstract fun requestMicrophonePermission(): Promise + + abstract fun addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere: Boolean, callback: (event: ScreenRecordingEvent) -> Unit): Double + + @DoNotStrip + @Keep + private fun addScreenRecordingListener_cxx(ignoreRecordingsInitiatedElsewhere: Boolean, callback: Func_void_ScreenRecordingEvent): Double { + val __result = addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere, callback) + return __result + } + + @DoNotStrip + @Keep + abstract fun removeScreenRecordingListener(id: Double): Unit + + abstract fun addBroadcastPickerListener(callback: (event: BroadcastPickerPresentationEvent) -> Unit): Double + + @DoNotStrip + @Keep + private fun addBroadcastPickerListener_cxx(callback: Func_void_BroadcastPickerPresentationEvent): Double { + val __result = addBroadcastPickerListener(callback) + return __result + } + + @DoNotStrip + @Keep + abstract fun removeBroadcastPickerListener(id: Double): Unit + + abstract fun startInAppRecording(enableMic: Boolean, enableCamera: Boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: Boolean, onRecordingFinished: (file: ScreenRecordingFile) -> Unit): Unit + + @DoNotStrip + @Keep + private fun startInAppRecording_cxx(enableMic: Boolean, enableCamera: Boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: Boolean, onRecordingFinished: Func_void_ScreenRecordingFile): Unit { + val __result = startInAppRecording(enableMic, enableCamera, cameraPreviewStyle, cameraDevice, separateAudioFile, onRecordingFinished) + return __result + } + + @DoNotStrip + @Keep + abstract fun stopInAppRecording(): Promise + + @DoNotStrip + @Keep + abstract fun cancelInAppRecording(): Promise + + abstract fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: (error: RecordingError) -> Unit): Unit + + @DoNotStrip + @Keep + private fun startGlobalRecording_cxx(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: Func_void_RecordingError): Unit { + val __result = startGlobalRecording(enableMic, separateAudioFile, onRecordingError) + return __result + } + + @DoNotStrip + @Keep + abstract fun stopGlobalRecording(settledTimeMs: Double): Promise + + @DoNotStrip + @Keep + abstract fun retrieveLastGlobalRecording(): ScreenRecordingFile? + + @DoNotStrip + @Keep + abstract fun clearRecordingCache(): Unit + + private external fun initHybrid(): HybridData + + companion object { + protected const val TAG = "HybridNitroScreenRecorderSpec" + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionResponse.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionResponse.kt new file mode 100644 index 0000000..4034602 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionResponse.kt @@ -0,0 +1,47 @@ +/// +/// PermissionResponse.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "PermissionResponse". + */ +@DoNotStrip +@Keep +data class PermissionResponse( + @DoNotStrip + @Keep + val canAskAgain: Boolean, + @DoNotStrip + @Keep + val granted: Boolean, + @DoNotStrip + @Keep + val status: PermissionStatus, + @DoNotStrip + @Keep + val expiresAt: Double +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(canAskAgain: Boolean, granted: Boolean, status: PermissionStatus, expiresAt: Double): PermissionResponse { + return PermissionResponse(canAskAgain, granted, status, expiresAt) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionStatus.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionStatus.kt new file mode 100644 index 0000000..558fe0b --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/PermissionStatus.kt @@ -0,0 +1,22 @@ +/// +/// PermissionStatus.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "PermissionStatus". + */ +@DoNotStrip +@Keep +enum class PermissionStatus(@DoNotStrip @Keep val value: Int) { + DENIED(0), + GRANTED(1), + UNDETERMINED(2); +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecorderCameraStyle.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecorderCameraStyle.kt new file mode 100644 index 0000000..8cd4bc9 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecorderCameraStyle.kt @@ -0,0 +1,53 @@ +/// +/// RecorderCameraStyle.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "RecorderCameraStyle". + */ +@DoNotStrip +@Keep +data class RecorderCameraStyle( + @DoNotStrip + @Keep + val top: Double?, + @DoNotStrip + @Keep + val left: Double?, + @DoNotStrip + @Keep + val width: Double?, + @DoNotStrip + @Keep + val height: Double?, + @DoNotStrip + @Keep + val borderRadius: Double?, + @DoNotStrip + @Keep + val borderWidth: Double? +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(top: Double?, left: Double?, width: Double?, height: Double?, borderRadius: Double?, borderWidth: Double?): RecorderCameraStyle { + return RecorderCameraStyle(top, left, width, height, borderRadius, borderWidth) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt new file mode 100644 index 0000000..7def6fa --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt @@ -0,0 +1,41 @@ +/// +/// RecordingError.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "RecordingError". + */ +@DoNotStrip +@Keep +data class RecordingError( + @DoNotStrip + @Keep + val name: String, + @DoNotStrip + @Keep + val message: String +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(name: String, message: String): RecordingError { + return RecordingError(name, message) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventReason.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventReason.kt new file mode 100644 index 0000000..9f804c6 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventReason.kt @@ -0,0 +1,21 @@ +/// +/// RecordingEventReason.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "RecordingEventReason". + */ +@DoNotStrip +@Keep +enum class RecordingEventReason(@DoNotStrip @Keep val value: Int) { + BEGAN(0), + ENDED(1); +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventType.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventType.kt new file mode 100644 index 0000000..dd1d2b0 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingEventType.kt @@ -0,0 +1,21 @@ +/// +/// RecordingEventType.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "RecordingEventType". + */ +@DoNotStrip +@Keep +enum class RecordingEventType(@DoNotStrip @Keep val value: Int) { + GLOBAL(0), + WITHINAPP(1); +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingEvent.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingEvent.kt new file mode 100644 index 0000000..be4e464 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingEvent.kt @@ -0,0 +1,41 @@ +/// +/// ScreenRecordingEvent.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "ScreenRecordingEvent". + */ +@DoNotStrip +@Keep +data class ScreenRecordingEvent( + @DoNotStrip + @Keep + val type: RecordingEventType, + @DoNotStrip + @Keep + val reason: RecordingEventReason +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(type: RecordingEventType, reason: RecordingEventReason): ScreenRecordingEvent { + return ScreenRecordingEvent(type, reason) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt new file mode 100644 index 0000000..bb7cbbb --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt @@ -0,0 +1,53 @@ +/// +/// ScreenRecordingFile.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "ScreenRecordingFile". + */ +@DoNotStrip +@Keep +data class ScreenRecordingFile( + @DoNotStrip + @Keep + val path: String, + @DoNotStrip + @Keep + val name: String, + @DoNotStrip + @Keep + val size: Double, + @DoNotStrip + @Keep + val duration: Double, + @DoNotStrip + @Keep + val enabledMicrophone: Boolean, + @DoNotStrip + @Keep + val audioFile: AudioRecordingFile? +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(path: String, name: String, size: Double, duration: Double, enabledMicrophone: Boolean, audioFile: AudioRecordingFile?): ScreenRecordingFile { + return ScreenRecordingFile(path, name, size, duration, enabledMicrophone, audioFile) + } + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/nitroscreenrecorderOnLoad.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/nitroscreenrecorderOnLoad.kt new file mode 100644 index 0000000..5403f8c --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/nitroscreenrecorderOnLoad.kt @@ -0,0 +1,35 @@ +/// +/// nitroscreenrecorderOnLoad.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import android.util.Log + +internal class nitroscreenrecorderOnLoad { + companion object { + private const val TAG = "nitroscreenrecorderOnLoad" + private var didLoad = false + /** + * Initializes the native part of "nitroscreenrecorder". + * This method is idempotent and can be called more than once. + */ + @JvmStatic + fun initializeNative() { + if (didLoad) return + try { + Log.i(TAG, "Loading nitroscreenrecorder C++ library...") + System.loadLibrary("nitroscreenrecorder") + Log.i(TAG, "Successfully loaded nitroscreenrecorder C++ library!") + didLoad = true + } catch (e: Error) { + Log.e(TAG, "Failed to load nitroscreenrecorder C++ library! Is it properly installed and linked? " + + "Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e) + throw e + } + } + } +} diff --git a/nitrogen/generated/android/nitroscreenrecorder+autolinking.cmake b/nitrogen/generated/android/nitroscreenrecorder+autolinking.cmake new file mode 100644 index 0000000..f2d4683 --- /dev/null +++ b/nitrogen/generated/android/nitroscreenrecorder+autolinking.cmake @@ -0,0 +1,81 @@ +# +# nitroscreenrecorder+autolinking.cmake +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright ยฉ 2025 Marc Rousavy @ Margelo +# + +# This is a CMake file that adds all files generated by Nitrogen +# to the current CMake project. +# +# To use it, add this to your CMakeLists.txt: +# ```cmake +# include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/nitroscreenrecorder+autolinking.cmake) +# ``` + +# Define a flag to check if we are building properly +add_definitions(-DBUILDING_NITROSCREENRECORDER_WITH_GENERATED_CMAKE_PROJECT) + +# Enable Raw Props parsing in react-native (for Nitro Views) +add_definitions(-DRN_SERIALIZABLE_STATE) + +# Add all headers that were generated by Nitrogen +include_directories( + "../nitrogen/generated/shared/c++" + "../nitrogen/generated/android/c++" + "../nitrogen/generated/android/" +) + +# Add all .cpp sources that were generated by Nitrogen +target_sources( + # CMake project name (Android C++ library name) + nitroscreenrecorder PRIVATE + # Autolinking Setup + ../nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp + # Shared Nitrogen C++ sources + ../nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.cpp + # Android-specific Nitrogen C++ sources + ../nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp +) + +# From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake +# Used in node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake +target_compile_definitions( + nitroscreenrecorder PRIVATE + -DFOLLY_NO_CONFIG=1 + -DFOLLY_HAVE_CLOCK_GETTIME=1 + -DFOLLY_USE_LIBCPP=1 + -DFOLLY_CFG_NO_COROUTINES=1 + -DFOLLY_MOBILE=1 + -DFOLLY_HAVE_RECVMMSG=1 + -DFOLLY_HAVE_PTHREAD=1 + # Once we target android-23 above, we can comment + # the following line. NDK uses GNU style stderror_r() after API 23. + -DFOLLY_HAVE_XSI_STRERROR_R=1 +) + +# Add all libraries required by the generated specs +find_package(fbjni REQUIRED) # <-- Used for communication between Java <-> C++ +find_package(ReactAndroid REQUIRED) # <-- Used to set up React Native bindings (e.g. CallInvoker/TurboModule) +find_package(react-native-nitro-modules REQUIRED) # <-- Used to create all HybridObjects and use the Nitro core library + +# Link all libraries together +target_link_libraries( + nitroscreenrecorder + fbjni::fbjni # <-- Facebook C++ JNI helpers + ReactAndroid::jsi # <-- RN: JSI + react-native-nitro-modules::NitroModules # <-- NitroModules Core :) +) + +# Link react-native (different prefab between RN 0.75 and RN 0.76) +if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76) + target_link_libraries( + nitroscreenrecorder + ReactAndroid::reactnative # <-- RN: Native Modules umbrella prefab + ) +else() + target_link_libraries( + nitroscreenrecorder + ReactAndroid::react_nativemodule_core # <-- RN: TurboModules Core + ) +endif() diff --git a/nitrogen/generated/android/nitroscreenrecorder+autolinking.gradle b/nitrogen/generated/android/nitroscreenrecorder+autolinking.gradle new file mode 100644 index 0000000..1b1651f --- /dev/null +++ b/nitrogen/generated/android/nitroscreenrecorder+autolinking.gradle @@ -0,0 +1,27 @@ +/// +/// nitroscreenrecorder+autolinking.gradle +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +/// This is a Gradle file that adds all files generated by Nitrogen +/// to the current Gradle project. +/// +/// To use it, add this to your build.gradle: +/// ```gradle +/// apply from: '../nitrogen/generated/android/nitroscreenrecorder+autolinking.gradle' +/// ``` + +logger.warn("[NitroModules] ๐Ÿ”ฅ nitroscreenrecorder is boosted by nitro!") + +android { + sourceSets { + main { + java.srcDirs += [ + // Nitrogen files + "${project.projectDir}/../nitrogen/generated/android/kotlin" + ] + } + } +} diff --git a/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp b/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp new file mode 100644 index 0000000..a7bd57b --- /dev/null +++ b/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp @@ -0,0 +1,52 @@ +/// +/// nitroscreenrecorderOnLoad.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#ifndef BUILDING_NITROSCREENRECORDER_WITH_GENERATED_CMAKE_PROJECT +#error nitroscreenrecorderOnLoad.cpp is not being built with the autogenerated CMakeLists.txt project. Is a different CMakeLists.txt building this? +#endif + +#include "nitroscreenrecorderOnLoad.hpp" + +#include +#include +#include + +#include "JHybridNitroScreenRecorderSpec.hpp" +#include "JFunc_void_ScreenRecordingEvent.hpp" +#include "JFunc_void_BroadcastPickerPresentationEvent.hpp" +#include "JFunc_void_ScreenRecordingFile.hpp" +#include "JFunc_void_RecordingError.hpp" +#include + +namespace margelo::nitro::nitroscreenrecorder { + +int initialize(JavaVM* vm) { + using namespace margelo::nitro; + using namespace margelo::nitro::nitroscreenrecorder; + using namespace facebook; + + return facebook::jni::initialize(vm, [] { + // Register native JNI methods + margelo::nitro::nitroscreenrecorder::JHybridNitroScreenRecorderSpec::registerNatives(); + margelo::nitro::nitroscreenrecorder::JFunc_void_ScreenRecordingEvent_cxx::registerNatives(); + margelo::nitro::nitroscreenrecorder::JFunc_void_BroadcastPickerPresentationEvent_cxx::registerNatives(); + margelo::nitro::nitroscreenrecorder::JFunc_void_ScreenRecordingFile_cxx::registerNatives(); + margelo::nitro::nitroscreenrecorder::JFunc_void_RecordingError_cxx::registerNatives(); + + // Register Nitro Hybrid Objects + HybridObjectRegistry::registerHybridObjectConstructor( + "NitroScreenRecorder", + []() -> std::shared_ptr { + static DefaultConstructableObject object("com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder"); + auto instance = object.create(); + return instance->cthis()->shared(); + } + ); + }); +} + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/nitroscreenrecorderOnLoad.hpp b/nitrogen/generated/android/nitroscreenrecorderOnLoad.hpp new file mode 100644 index 0000000..788a41e --- /dev/null +++ b/nitrogen/generated/android/nitroscreenrecorderOnLoad.hpp @@ -0,0 +1,25 @@ +/// +/// nitroscreenrecorderOnLoad.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#include +#include + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * Initializes the native (C++) part of nitroscreenrecorder, and autolinks all Hybrid Objects. + * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`). + * Example: + * ```cpp (cpp-adapter.cpp) + * JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { + * return margelo::nitro::nitroscreenrecorder::initialize(vm); + * } + * ``` + */ + int initialize(JavaVM* vm); + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/ios/NitroScreenRecorder+autolinking.rb b/nitrogen/generated/ios/NitroScreenRecorder+autolinking.rb new file mode 100644 index 0000000..3180e05 --- /dev/null +++ b/nitrogen/generated/ios/NitroScreenRecorder+autolinking.rb @@ -0,0 +1,60 @@ +# +# NitroScreenRecorder+autolinking.rb +# This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +# https://github.com/mrousavy/nitro +# Copyright ยฉ 2025 Marc Rousavy @ Margelo +# + +# This is a Ruby script that adds all files generated by Nitrogen +# to the given podspec. +# +# To use it, add this to your .podspec: +# ```ruby +# Pod::Spec.new do |spec| +# # ... +# +# # Add all files generated by Nitrogen +# load 'nitrogen/generated/ios/NitroScreenRecorder+autolinking.rb' +# add_nitrogen_files(spec) +# end +# ``` + +def add_nitrogen_files(spec) + Pod::UI.puts "[NitroModules] ๐Ÿ”ฅ NitroScreenRecorder is boosted by nitro!" + + spec.dependency "NitroModules" + + current_source_files = Array(spec.attributes_hash['source_files']) + spec.source_files = current_source_files + [ + # Generated cross-platform specs + "nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}", + # Generated bridges for the cross-platform specs + "nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}", + ] + + current_public_header_files = Array(spec.attributes_hash['public_header_files']) + spec.public_header_files = current_public_header_files + [ + # Generated specs + "nitrogen/generated/shared/**/*.{h,hpp}", + # Swift to C++ bridging helpers + "nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp" + ] + + current_private_header_files = Array(spec.attributes_hash['private_header_files']) + spec.private_header_files = current_private_header_files + [ + # iOS specific specs + "nitrogen/generated/ios/c++/**/*.{h,hpp}", + # Views are framework-specific and should be private + "nitrogen/generated/shared/**/views/**/*" + ] + + current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} + spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ + # Use C++ 20 + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + # Enables C++ <-> Swift interop (by default it's only C) + "SWIFT_OBJC_INTEROP_MODE" => "objcxx", + # Enables stricter modular headers + "DEFINES_MODULE" => "YES", + }) +end diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp new file mode 100644 index 0000000..99d48e8 --- /dev/null +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp @@ -0,0 +1,96 @@ +/// +/// NitroScreenRecorder-Swift-Cxx-Bridge.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#include "NitroScreenRecorder-Swift-Cxx-Bridge.hpp" + +// Include C++ implementation defined types +#include "HybridNitroScreenRecorderSpecSwift.hpp" +#include "NitroScreenRecorder-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::nitroscreenrecorder::bridge::swift { + + // pragma MARK: std::function + Func_void_PermissionResponse create_Func_void_PermissionResponse(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_PermissionResponse::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const PermissionResponse& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_std__exception_ptr::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::exception_ptr& error) mutable -> void { + swiftClosure.call(error); + }; + } + + // pragma MARK: std::function + Func_void_ScreenRecordingEvent create_Func_void_ScreenRecordingEvent(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_ScreenRecordingEvent::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const ScreenRecordingEvent& event) mutable -> void { + swiftClosure.call(event); + }; + } + + // pragma MARK: std::function + Func_void_BroadcastPickerPresentationEvent create_Func_void_BroadcastPickerPresentationEvent(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_BroadcastPickerPresentationEvent::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](BroadcastPickerPresentationEvent event) mutable -> void { + swiftClosure.call(static_cast(event)); + }; + } + + // pragma MARK: std::function + Func_void_ScreenRecordingFile create_Func_void_ScreenRecordingFile(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_ScreenRecordingFile::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const ScreenRecordingFile& file) mutable -> void { + swiftClosure.call(file); + }; + } + + // pragma MARK: std::function& /* result */)> + Func_void_std__optional_ScreenRecordingFile_ create_Func_void_std__optional_ScreenRecordingFile_(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_std__optional_ScreenRecordingFile_::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const std::optional& result) mutable -> void { + swiftClosure.call(result); + }; + } + + // pragma MARK: std::function + Func_void create_Func_void(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)]() mutable -> void { + swiftClosure.call(); + }; + } + + // pragma MARK: std::function + Func_void_RecordingError create_Func_void_RecordingError(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_RecordingError::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const RecordingError& error) mutable -> void { + swiftClosure.call(error); + }; + } + + // pragma MARK: std::shared_ptr + std::shared_ptr create_std__shared_ptr_HybridNitroScreenRecorderSpec_(void* NON_NULL swiftUnsafePointer) noexcept { + NitroScreenRecorder::HybridNitroScreenRecorderSpec_cxx swiftPart = NitroScreenRecorder::HybridNitroScreenRecorderSpec_cxx::fromUnsafe(swiftUnsafePointer); + return std::make_shared(swiftPart); + } + void* NON_NULL get_std__shared_ptr_HybridNitroScreenRecorderSpec_(std__shared_ptr_HybridNitroScreenRecorderSpec_ cppType) { + std::shared_ptr swiftWrapper = std::dynamic_pointer_cast(cppType); + #ifdef NITRO_DEBUG + if (swiftWrapper == nullptr) [[unlikely]] { + throw std::runtime_error("Class \"HybridNitroScreenRecorderSpec\" is not implemented in Swift!"); + } + #endif + NitroScreenRecorder::HybridNitroScreenRecorderSpec_cxx& swiftPart = swiftWrapper->getSwiftPart(); + return swiftPart.toUnsafe(); + } + +} // namespace margelo::nitro::nitroscreenrecorder::bridge::swift diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp new file mode 100644 index 0000000..fc95ffb --- /dev/null +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp @@ -0,0 +1,394 @@ +/// +/// NitroScreenRecorder-Swift-Cxx-Bridge.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `AudioRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } +// Forward declaration of `BroadcastPickerPresentationEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresentationEvent; } +// Forward declaration of `HybridNitroScreenRecorderSpec` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { class HybridNitroScreenRecorderSpec; } +// Forward declaration of `PermissionResponse` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } +// Forward declaration of `PermissionStatus` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } +// Forward declaration of `RecordingEventReason` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } +// Forward declaration of `RecordingEventType` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventType; } +// Forward declaration of `ScreenRecordingEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingEvent; } +// Forward declaration of `ScreenRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } + +// Forward declarations of Swift defined types +// Forward declaration of `HybridNitroScreenRecorderSpec_cxx` to properly resolve imports. +namespace NitroScreenRecorder { class HybridNitroScreenRecorderSpec_cxx; } + +// Include C++ defined types +#include "AudioRecordingFile.hpp" +#include "BroadcastPickerPresentationEvent.hpp" +#include "HybridNitroScreenRecorderSpec.hpp" +#include "PermissionResponse.hpp" +#include "PermissionStatus.hpp" +#include "RecordingError.hpp" +#include "RecordingEventReason.hpp" +#include "RecordingEventType.hpp" +#include "ScreenRecordingEvent.hpp" +#include "ScreenRecordingFile.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Contains specialized versions of C++ templated types so they can be accessed from Swift, + * as well as helper functions to interact with those C++ types from Swift. + */ +namespace margelo::nitro::nitroscreenrecorder::bridge::swift { + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_PermissionResponse__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_PermissionResponse__() noexcept { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_PermissionResponse__(std::shared_ptr> promise) noexcept { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_PermissionResponse = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_PermissionResponse_Wrapper final { + public: + explicit Func_void_PermissionResponse_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(PermissionResponse result) const noexcept { + _function->operator()(result); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_PermissionResponse create_Func_void_PermissionResponse(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_PermissionResponse_Wrapper wrap_Func_void_PermissionResponse(Func_void_PermissionResponse value) noexcept { + return Func_void_PermissionResponse_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_std__exception_ptr = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_std__exception_ptr_Wrapper final { + public: + explicit Func_void_std__exception_ptr_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(std::exception_ptr error) const noexcept { + _function->operator()(error); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__exception_ptr create_Func_void_std__exception_ptr(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__exception_ptr_Wrapper wrap_Func_void_std__exception_ptr(Func_void_std__exception_ptr value) noexcept { + return Func_void_std__exception_ptr_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_ScreenRecordingEvent = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_ScreenRecordingEvent_Wrapper final { + public: + explicit Func_void_ScreenRecordingEvent_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(ScreenRecordingEvent event) const noexcept { + _function->operator()(event); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_ScreenRecordingEvent create_Func_void_ScreenRecordingEvent(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_ScreenRecordingEvent_Wrapper wrap_Func_void_ScreenRecordingEvent(Func_void_ScreenRecordingEvent value) noexcept { + return Func_void_ScreenRecordingEvent_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_BroadcastPickerPresentationEvent = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_BroadcastPickerPresentationEvent_Wrapper final { + public: + explicit Func_void_BroadcastPickerPresentationEvent_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(int event) const noexcept { + _function->operator()(static_cast(event)); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_BroadcastPickerPresentationEvent create_Func_void_BroadcastPickerPresentationEvent(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_BroadcastPickerPresentationEvent_Wrapper wrap_Func_void_BroadcastPickerPresentationEvent(Func_void_BroadcastPickerPresentationEvent value) noexcept { + return Func_void_BroadcastPickerPresentationEvent_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_double_ = std::optional; + inline std::optional create_std__optional_double_(const double& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_double_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline double get_std__optional_double_(const std::optional& optional) noexcept { + return *optional; + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_AudioRecordingFile_ = std::optional; + inline std::optional create_std__optional_AudioRecordingFile_(const AudioRecordingFile& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_AudioRecordingFile_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline AudioRecordingFile get_std__optional_AudioRecordingFile_(const std::optional& optional) noexcept { + return *optional; + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_ScreenRecordingFile = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_ScreenRecordingFile_Wrapper final { + public: + explicit Func_void_ScreenRecordingFile_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(ScreenRecordingFile file) const noexcept { + _function->operator()(file); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_ScreenRecordingFile create_Func_void_ScreenRecordingFile(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_ScreenRecordingFile_Wrapper wrap_Func_void_ScreenRecordingFile(Func_void_ScreenRecordingFile value) noexcept { + return Func_void_ScreenRecordingFile_Wrapper(std::move(value)); + } + + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_ScreenRecordingFile_ = std::optional; + inline std::optional create_std__optional_ScreenRecordingFile_(const ScreenRecordingFile& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_ScreenRecordingFile_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline ScreenRecordingFile get_std__optional_ScreenRecordingFile_(const std::optional& optional) noexcept { + return *optional; + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__optional_ScreenRecordingFile___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__optional_ScreenRecordingFile___() noexcept { + return Promise>::create(); + } + inline PromiseHolder> wrap_std__shared_ptr_Promise_std__optional_ScreenRecordingFile___(std::shared_ptr>> promise) noexcept { + return PromiseHolder>(std::move(promise)); + } + + // pragma MARK: std::function& /* result */)> + /** + * Specialized version of `std::function&)>`. + */ + using Func_void_std__optional_ScreenRecordingFile_ = std::function& /* result */)>; + /** + * Wrapper class for a `std::function& / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__optional_ScreenRecordingFile__Wrapper final { + public: + explicit Func_void_std__optional_ScreenRecordingFile__Wrapper(std::function& /* result */)>&& func): _function(std::make_unique& /* result */)>>(std::move(func))) {} + inline void call(std::optional result) const noexcept { + _function->operator()(result); + } + private: + std::unique_ptr& /* result */)>> _function; + } SWIFT_NONCOPYABLE; + Func_void_std__optional_ScreenRecordingFile_ create_Func_void_std__optional_ScreenRecordingFile_(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__optional_ScreenRecordingFile__Wrapper wrap_Func_void_std__optional_ScreenRecordingFile_(Func_void_std__optional_ScreenRecordingFile_ value) noexcept { + return Func_void_std__optional_ScreenRecordingFile__Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr> + /** + * Specialized version of `std::shared_ptr>`. + */ + using std__shared_ptr_Promise_void__ = std::shared_ptr>; + inline std::shared_ptr> create_std__shared_ptr_Promise_void__() noexcept { + return Promise::create(); + } + inline PromiseHolder wrap_std__shared_ptr_Promise_void__(std::shared_ptr> promise) noexcept { + return PromiseHolder(std::move(promise)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_Wrapper final { + public: + explicit Func_void_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call() const noexcept { + _function->operator()(); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void create_Func_void(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_Wrapper wrap_Func_void(Func_void value) noexcept { + return Func_void_Wrapper(std::move(value)); + } + + // pragma MARK: std::function + /** + * Specialized version of `std::function`. + */ + using Func_void_RecordingError = std::function; + /** + * Wrapper class for a `std::function`, this can be used from Swift. + */ + class Func_void_RecordingError_Wrapper final { + public: + explicit Func_void_RecordingError_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(RecordingError error) const noexcept { + _function->operator()(error); + } + private: + std::unique_ptr> _function; + } SWIFT_NONCOPYABLE; + Func_void_RecordingError create_Func_void_RecordingError(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_RecordingError_Wrapper wrap_Func_void_RecordingError(Func_void_RecordingError value) noexcept { + return Func_void_RecordingError_Wrapper(std::move(value)); + } + + // pragma MARK: std::shared_ptr + /** + * Specialized version of `std::shared_ptr`. + */ + using std__shared_ptr_HybridNitroScreenRecorderSpec_ = std::shared_ptr; + std::shared_ptr create_std__shared_ptr_HybridNitroScreenRecorderSpec_(void* NON_NULL swiftUnsafePointer) noexcept; + void* NON_NULL get_std__shared_ptr_HybridNitroScreenRecorderSpec_(std__shared_ptr_HybridNitroScreenRecorderSpec_ cppType); + + // pragma MARK: std::weak_ptr + using std__weak_ptr_HybridNitroScreenRecorderSpec_ = std::weak_ptr; + inline std__weak_ptr_HybridNitroScreenRecorderSpec_ weakify_std__shared_ptr_HybridNitroScreenRecorderSpec_(const std::shared_ptr& strong) noexcept { return strong; } + + // pragma MARK: Result + using Result_PermissionStatus_ = Result; + inline Result_PermissionStatus_ create_Result_PermissionStatus_(PermissionStatus value) noexcept { + return Result::withValue(std::move(value)); + } + inline Result_PermissionStatus_ create_Result_PermissionStatus_(const std::exception_ptr& error) noexcept { + return Result::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_PermissionResponse___ = Result>>; + inline Result_std__shared_ptr_Promise_PermissionResponse___ create_Result_std__shared_ptr_Promise_PermissionResponse___(const std::shared_ptr>& value) noexcept { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_PermissionResponse___ create_Result_std__shared_ptr_Promise_PermissionResponse___(const std::exception_ptr& error) noexcept { + return Result>>::withError(error); + } + + // pragma MARK: Result + using Result_double_ = Result; + inline Result_double_ create_Result_double_(double value) noexcept { + return Result::withValue(std::move(value)); + } + inline Result_double_ create_Result_double_(const std::exception_ptr& error) noexcept { + return Result::withError(error); + } + + // pragma MARK: Result + using Result_void_ = Result; + inline Result_void_ create_Result_void_() noexcept { + return Result::withValue(); + } + inline Result_void_ create_Result_void_(const std::exception_ptr& error) noexcept { + return Result::withError(error); + } + + // pragma MARK: Result>>> + using Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____ = Result>>>; + inline Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____ create_Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____(const std::shared_ptr>>& value) noexcept { + return Result>>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____ create_Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____(const std::exception_ptr& error) noexcept { + return Result>>>::withError(error); + } + + // pragma MARK: Result>> + using Result_std__shared_ptr_Promise_void___ = Result>>; + inline Result_std__shared_ptr_Promise_void___ create_Result_std__shared_ptr_Promise_void___(const std::shared_ptr>& value) noexcept { + return Result>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_void___ create_Result_std__shared_ptr_Promise_void___(const std::exception_ptr& error) noexcept { + return Result>>::withError(error); + } + + // pragma MARK: Result> + using Result_std__optional_ScreenRecordingFile__ = Result>; + inline Result_std__optional_ScreenRecordingFile__ create_Result_std__optional_ScreenRecordingFile__(const std::optional& value) noexcept { + return Result>::withValue(value); + } + inline Result_std__optional_ScreenRecordingFile__ create_Result_std__optional_ScreenRecordingFile__(const std::exception_ptr& error) noexcept { + return Result>::withError(error); + } + +} // namespace margelo::nitro::nitroscreenrecorder::bridge::swift diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp new file mode 100644 index 0000000..4b6bef5 --- /dev/null +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp @@ -0,0 +1,80 @@ +/// +/// NitroScreenRecorder-Swift-Cxx-Umbrella.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +// Forward declarations of C++ defined types +// Forward declaration of `AudioRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } +// Forward declaration of `BroadcastPickerPresentationEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresentationEvent; } +// Forward declaration of `CameraDevice` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } +// Forward declaration of `HybridNitroScreenRecorderSpec` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { class HybridNitroScreenRecorderSpec; } +// Forward declaration of `PermissionResponse` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } +// Forward declaration of `PermissionStatus` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } +// Forward declaration of `RecorderCameraStyle` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } +// Forward declaration of `RecordingEventReason` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } +// Forward declaration of `RecordingEventType` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventType; } +// Forward declaration of `ScreenRecordingEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingEvent; } +// Forward declaration of `ScreenRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } + +// Include C++ defined types +#include "AudioRecordingFile.hpp" +#include "BroadcastPickerPresentationEvent.hpp" +#include "CameraDevice.hpp" +#include "HybridNitroScreenRecorderSpec.hpp" +#include "PermissionResponse.hpp" +#include "PermissionStatus.hpp" +#include "RecorderCameraStyle.hpp" +#include "RecordingError.hpp" +#include "RecordingEventReason.hpp" +#include "RecordingEventType.hpp" +#include "ScreenRecordingEvent.hpp" +#include "ScreenRecordingFile.hpp" +#include +#include +#include +#include +#include +#include +#include + +// C++ helpers for Swift +#include "NitroScreenRecorder-Swift-Cxx-Bridge.hpp" + +// Common C++ types used in Swift +#include +#include +#include +#include + +// Forward declarations of Swift defined types +// Forward declaration of `HybridNitroScreenRecorderSpec_cxx` to properly resolve imports. +namespace NitroScreenRecorder { class HybridNitroScreenRecorderSpec_cxx; } + +// Include Swift defined types +#if __has_include("NitroScreenRecorder-Swift.h") +// This header is generated by Xcode/Swift on every app build. +// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "NitroScreenRecorder". +#include "NitroScreenRecorder-Swift.h" +// Same as above, but used when building with frameworks (`use_frameworks`) +#elif __has_include() +#include +#else +#error NitroScreenRecorder's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "NitroScreenRecorder", and try building the app first. +#endif diff --git a/nitrogen/generated/ios/NitroScreenRecorderAutolinking.mm b/nitrogen/generated/ios/NitroScreenRecorderAutolinking.mm new file mode 100644 index 0000000..065d1d8 --- /dev/null +++ b/nitrogen/generated/ios/NitroScreenRecorderAutolinking.mm @@ -0,0 +1,33 @@ +/// +/// NitroScreenRecorderAutolinking.mm +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#import +#import +#import "NitroScreenRecorder-Swift-Cxx-Umbrella.hpp" +#import + +#include "HybridNitroScreenRecorderSpecSwift.hpp" + +@interface NitroScreenRecorderAutolinking : NSObject +@end + +@implementation NitroScreenRecorderAutolinking + ++ (void) load { + using namespace margelo::nitro; + using namespace margelo::nitro::nitroscreenrecorder; + + HybridObjectRegistry::registerHybridObjectConstructor( + "NitroScreenRecorder", + []() -> std::shared_ptr { + std::shared_ptr hybridObject = NitroScreenRecorder::NitroScreenRecorderAutolinking::createNitroScreenRecorder(); + return hybridObject; + } + ); +} + +@end diff --git a/nitrogen/generated/ios/NitroScreenRecorderAutolinking.swift b/nitrogen/generated/ios/NitroScreenRecorderAutolinking.swift new file mode 100644 index 0000000..48e69d9 --- /dev/null +++ b/nitrogen/generated/ios/NitroScreenRecorderAutolinking.swift @@ -0,0 +1,25 @@ +/// +/// NitroScreenRecorderAutolinking.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +public final class NitroScreenRecorderAutolinking { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Creates an instance of a Swift class that implements `HybridNitroScreenRecorderSpec`, + * and wraps it in a Swift class that can directly interop with C++ (`HybridNitroScreenRecorderSpec_cxx`) + * + * This is generated by Nitrogen and will initialize the class specified + * in the `"autolinking"` property of `nitro.json` (in this case, `NitroScreenRecorder`). + */ + public static func createNitroScreenRecorder() -> bridge.std__shared_ptr_HybridNitroScreenRecorderSpec_ { + let hybridObject = NitroScreenRecorder() + return { () -> bridge.std__shared_ptr_HybridNitroScreenRecorderSpec_ in + let __cxxWrapped = hybridObject.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }() + } +} diff --git a/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.cpp b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.cpp new file mode 100644 index 0000000..4a92220 --- /dev/null +++ b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.cpp @@ -0,0 +1,11 @@ +/// +/// HybridNitroScreenRecorderSpecSwift.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#include "HybridNitroScreenRecorderSpecSwift.hpp" + +namespace margelo::nitro::nitroscreenrecorder { +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp new file mode 100644 index 0000000..fa7d5d7 --- /dev/null +++ b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp @@ -0,0 +1,213 @@ +/// +/// HybridNitroScreenRecorderSpecSwift.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include "HybridNitroScreenRecorderSpec.hpp" + +// Forward declaration of `HybridNitroScreenRecorderSpec_cxx` to properly resolve imports. +namespace NitroScreenRecorder { class HybridNitroScreenRecorderSpec_cxx; } + +// Forward declaration of `PermissionStatus` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } +// Forward declaration of `PermissionResponse` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } +// Forward declaration of `ScreenRecordingEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingEvent; } +// Forward declaration of `RecordingEventType` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventType; } +// Forward declaration of `RecordingEventReason` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } +// Forward declaration of `BroadcastPickerPresentationEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresentationEvent; } +// Forward declaration of `RecorderCameraStyle` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } +// Forward declaration of `CameraDevice` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } +// Forward declaration of `ScreenRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } +// Forward declaration of `AudioRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } + +#include "PermissionStatus.hpp" +#include "PermissionResponse.hpp" +#include +#include "ScreenRecordingEvent.hpp" +#include +#include "RecordingEventType.hpp" +#include "RecordingEventReason.hpp" +#include "BroadcastPickerPresentationEvent.hpp" +#include "RecorderCameraStyle.hpp" +#include +#include "CameraDevice.hpp" +#include "ScreenRecordingFile.hpp" +#include +#include "AudioRecordingFile.hpp" +#include "RecordingError.hpp" + +#include "NitroScreenRecorder-Swift-Cxx-Umbrella.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * The C++ part of HybridNitroScreenRecorderSpec_cxx.swift. + * + * HybridNitroScreenRecorderSpecSwift (C++) accesses HybridNitroScreenRecorderSpec_cxx (Swift), and might + * contain some additional bridging code for C++ <> Swift interop. + * + * Since this obviously introduces an overhead, I hope at some point in + * the future, HybridNitroScreenRecorderSpec_cxx can directly inherit from the C++ class HybridNitroScreenRecorderSpec + * to simplify the whole structure and memory management. + */ + class HybridNitroScreenRecorderSpecSwift: public virtual HybridNitroScreenRecorderSpec { + public: + // Constructor from a Swift instance + explicit HybridNitroScreenRecorderSpecSwift(const NitroScreenRecorder::HybridNitroScreenRecorderSpec_cxx& swiftPart): + HybridObject(HybridNitroScreenRecorderSpec::TAG), + _swiftPart(swiftPart) { } + + public: + // Get the Swift part + inline NitroScreenRecorder::HybridNitroScreenRecorderSpec_cxx& getSwiftPart() noexcept { + return _swiftPart; + } + + public: + inline size_t getExternalMemorySize() noexcept override { + return _swiftPart.getMemorySize(); + } + void dispose() noexcept override { + _swiftPart.dispose(); + } + std::string toString() override { + return _swiftPart.toString(); + } + + public: + // Properties + + + public: + // Methods + inline PermissionStatus getCameraPermissionStatus() override { + auto __result = _swiftPart.getCameraPermissionStatus(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline PermissionStatus getMicrophonePermissionStatus() override { + auto __result = _swiftPart.getMicrophonePermissionStatus(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> requestCameraPermission() override { + auto __result = _swiftPart.requestCameraPermission(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> requestMicrophonePermission() override { + auto __result = _swiftPart.requestMicrophonePermission(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline double addScreenRecordingListener(bool ignoreRecordingsInitiatedElsewhere, const std::function& callback) override { + auto __result = _swiftPart.addScreenRecordingListener(std::forward(ignoreRecordingsInitiatedElsewhere), callback); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void removeScreenRecordingListener(double id) override { + auto __result = _swiftPart.removeScreenRecordingListener(std::forward(id)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline double addBroadcastPickerListener(const std::function& callback) override { + auto __result = _swiftPart.addBroadcastPickerListener(callback); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void removeBroadcastPickerListener(double id) override { + auto __result = _swiftPart.removeBroadcastPickerListener(std::forward(id)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) override { + auto __result = _swiftPart.startInAppRecording(std::forward(enableMic), std::forward(enableCamera), std::forward(cameraPreviewStyle), static_cast(cameraDevice), std::forward(separateAudioFile), onRecordingFinished); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline std::shared_ptr>> stopInAppRecording() override { + auto __result = _swiftPart.stopInAppRecording(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::shared_ptr> cancelInAppRecording() override { + auto __result = _swiftPart.cancelInAppRecording(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) override { + auto __result = _swiftPart.startGlobalRecording(std::forward(enableMic), std::forward(separateAudioFile), onRecordingError); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + inline std::shared_ptr>> stopGlobalRecording(double settledTimeMs) override { + auto __result = _swiftPart.stopGlobalRecording(std::forward(settledTimeMs)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline std::optional retrieveLastGlobalRecording() override { + auto __result = _swiftPart.retrieveLastGlobalRecording(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } + inline void clearRecordingCache() override { + auto __result = _swiftPart.clearRecordingCache(); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + } + + private: + NitroScreenRecorder::HybridNitroScreenRecorderSpec_cxx _swiftPart; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/ios/swift/AudioRecordingFile.swift b/nitrogen/generated/ios/swift/AudioRecordingFile.swift new file mode 100644 index 0000000..7532a5a --- /dev/null +++ b/nitrogen/generated/ios/swift/AudioRecordingFile.swift @@ -0,0 +1,68 @@ +/// +/// AudioRecordingFile.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `AudioRecordingFile`, backed by a C++ struct. + */ +public typealias AudioRecordingFile = margelo.nitro.nitroscreenrecorder.AudioRecordingFile + +public extension AudioRecordingFile { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `AudioRecordingFile`. + */ + init(path: String, name: String, size: Double, duration: Double) { + self.init(std.string(path), std.string(name), size, duration) + } + + var path: String { + @inline(__always) + get { + return String(self.__path) + } + @inline(__always) + set { + self.__path = std.string(newValue) + } + } + + var name: String { + @inline(__always) + get { + return String(self.__name) + } + @inline(__always) + set { + self.__name = std.string(newValue) + } + } + + var size: Double { + @inline(__always) + get { + return self.__size + } + @inline(__always) + set { + self.__size = newValue + } + } + + var duration: Double { + @inline(__always) + get { + return self.__duration + } + @inline(__always) + set { + self.__duration = newValue + } + } +} diff --git a/nitrogen/generated/ios/swift/BroadcastPickerPresentationEvent.swift b/nitrogen/generated/ios/swift/BroadcastPickerPresentationEvent.swift new file mode 100644 index 0000000..76d02ea --- /dev/null +++ b/nitrogen/generated/ios/swift/BroadcastPickerPresentationEvent.swift @@ -0,0 +1,40 @@ +/// +/// BroadcastPickerPresentationEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `BroadcastPickerPresentationEvent`, backed by a C++ enum. + */ +public typealias BroadcastPickerPresentationEvent = margelo.nitro.nitroscreenrecorder.BroadcastPickerPresentationEvent + +public extension BroadcastPickerPresentationEvent { + /** + * Get a BroadcastPickerPresentationEvent for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "showing": + self = .showing + case "dismissed": + self = .dismissed + default: + return nil + } + } + + /** + * Get the String value this BroadcastPickerPresentationEvent represents. + */ + var stringValue: String { + switch self { + case .showing: + return "showing" + case .dismissed: + return "dismissed" + } + } +} diff --git a/nitrogen/generated/ios/swift/CameraDevice.swift b/nitrogen/generated/ios/swift/CameraDevice.swift new file mode 100644 index 0000000..7d5f57d --- /dev/null +++ b/nitrogen/generated/ios/swift/CameraDevice.swift @@ -0,0 +1,40 @@ +/// +/// CameraDevice.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `CameraDevice`, backed by a C++ enum. + */ +public typealias CameraDevice = margelo.nitro.nitroscreenrecorder.CameraDevice + +public extension CameraDevice { + /** + * Get a CameraDevice for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "front": + self = .front + case "back": + self = .back + default: + return nil + } + } + + /** + * Get the String value this CameraDevice represents. + */ + var stringValue: String { + switch self { + case .front: + return "front" + case .back: + return "back" + } + } +} diff --git a/nitrogen/generated/ios/swift/Func_void.swift b/nitrogen/generated/ios/swift/Func_void.swift new file mode 100644 index 0000000..e014042 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void.swift @@ -0,0 +1,47 @@ +/// +/// Func_void.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `() -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: () -> Void + + public init(_ closure: @escaping () -> Void) { + self.closure = closure + } + + @inline(__always) + public func call() -> Void { + self.closure() + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_BroadcastPickerPresentationEvent.swift b/nitrogen/generated/ios/swift/Func_void_BroadcastPickerPresentationEvent.swift new file mode 100644 index 0000000..f0631fc --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_BroadcastPickerPresentationEvent.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_BroadcastPickerPresentationEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ event: BroadcastPickerPresentationEvent) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_BroadcastPickerPresentationEvent { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ event: BroadcastPickerPresentationEvent) -> Void + + public init(_ closure: @escaping (_ event: BroadcastPickerPresentationEvent) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(event: Int32) -> Void { + self.closure(margelo.nitro.nitroscreenrecorder.BroadcastPickerPresentationEvent(rawValue: event)!) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_BroadcastPickerPresentationEvent`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_BroadcastPickerPresentationEvent { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_PermissionResponse.swift b/nitrogen/generated/ios/swift/Func_void_PermissionResponse.swift new file mode 100644 index 0000000..8df7737 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_PermissionResponse.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_PermissionResponse.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ value: PermissionResponse) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_PermissionResponse { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ value: PermissionResponse) -> Void + + public init(_ closure: @escaping (_ value: PermissionResponse) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: PermissionResponse) -> Void { + self.closure(value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_PermissionResponse`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_PermissionResponse { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_RecordingError.swift b/nitrogen/generated/ios/swift/Func_void_RecordingError.swift new file mode 100644 index 0000000..4d11201 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_RecordingError.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_RecordingError.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ error: RecordingError) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_RecordingError { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ error: RecordingError) -> Void + + public init(_ closure: @escaping (_ error: RecordingError) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(error: RecordingError) -> Void { + self.closure(error) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_RecordingError`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_RecordingError { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_ScreenRecordingEvent.swift b/nitrogen/generated/ios/swift/Func_void_ScreenRecordingEvent.swift new file mode 100644 index 0000000..52573d9 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_ScreenRecordingEvent.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_ScreenRecordingEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ event: ScreenRecordingEvent) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_ScreenRecordingEvent { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ event: ScreenRecordingEvent) -> Void + + public init(_ closure: @escaping (_ event: ScreenRecordingEvent) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(event: ScreenRecordingEvent) -> Void { + self.closure(event) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_ScreenRecordingEvent`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_ScreenRecordingEvent { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_ScreenRecordingFile.swift b/nitrogen/generated/ios/swift/Func_void_ScreenRecordingFile.swift new file mode 100644 index 0000000..f9f97e3 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_ScreenRecordingFile.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_ScreenRecordingFile.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ file: ScreenRecordingFile) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_ScreenRecordingFile { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ file: ScreenRecordingFile) -> Void + + public init(_ closure: @escaping (_ file: ScreenRecordingFile) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(file: ScreenRecordingFile) -> Void { + self.closure(file) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_ScreenRecordingFile`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_ScreenRecordingFile { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift b/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift new file mode 100644 index 0000000..5b9ebcf --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_std__exception_ptr.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ error: Error) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__exception_ptr { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ error: Error) -> Void + + public init(_ closure: @escaping (_ error: Error) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(error: std.exception_ptr) -> Void { + self.closure(RuntimeError.from(cppError: error)) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__exception_ptr`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__exception_ptr { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/Func_void_std__optional_ScreenRecordingFile_.swift b/nitrogen/generated/ios/swift/Func_void_std__optional_ScreenRecordingFile_.swift new file mode 100644 index 0000000..dda9f42 --- /dev/null +++ b/nitrogen/generated/ios/swift/Func_void_std__optional_ScreenRecordingFile_.swift @@ -0,0 +1,47 @@ +/// +/// Func_void_std__optional_ScreenRecordingFile_.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + + +/** + * Wraps a Swift `(_ value: ScreenRecordingFile?) -> Void` as a class. + * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. + */ +public final class Func_void_std__optional_ScreenRecordingFile_ { + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + private let closure: (_ value: ScreenRecordingFile?) -> Void + + public init(_ closure: @escaping (_ value: ScreenRecordingFile?) -> Void) { + self.closure = closure + } + + @inline(__always) + public func call(value: bridge.std__optional_ScreenRecordingFile_) -> Void { + self.closure(value.value) + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + @inline(__always) + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `Func_void_std__optional_ScreenRecordingFile_`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + @inline(__always) + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__optional_ScreenRecordingFile_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } +} diff --git a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift new file mode 100644 index 0000000..e0947f0 --- /dev/null +++ b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift @@ -0,0 +1,71 @@ +/// +/// HybridNitroScreenRecorderSpec.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules +import NitroModules + +/// See ``HybridNitroScreenRecorderSpec`` +public protocol HybridNitroScreenRecorderSpec_protocol: HybridObject { + // Properties + + + // Methods + func getCameraPermissionStatus() throws -> PermissionStatus + func getMicrophonePermissionStatus() throws -> PermissionStatus + func requestCameraPermission() throws -> Promise + func requestMicrophonePermission() throws -> Promise + func addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere: Bool, callback: @escaping (_ event: ScreenRecordingEvent) -> Void) throws -> Double + func removeScreenRecordingListener(id: Double) throws -> Void + func addBroadcastPickerListener(callback: @escaping (_ event: BroadcastPickerPresentationEvent) -> Void) throws -> Double + func removeBroadcastPickerListener(id: Double) throws -> Void + func startInAppRecording(enableMic: Bool, enableCamera: Bool, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: Bool, onRecordingFinished: @escaping (_ file: ScreenRecordingFile) -> Void) throws -> Void + func stopInAppRecording() throws -> Promise + func cancelInAppRecording() throws -> Promise + func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (_ error: RecordingError) -> Void) throws -> Void + func stopGlobalRecording(settledTimeMs: Double) throws -> Promise + func retrieveLastGlobalRecording() throws -> ScreenRecordingFile? + func clearRecordingCache() throws -> Void +} + +public extension HybridNitroScreenRecorderSpec_protocol { + /// Default implementation of ``HybridObject.toString`` + func toString() -> String { + return "[HybridObject NitroScreenRecorder]" + } +} + +/// See ``HybridNitroScreenRecorderSpec`` +open class HybridNitroScreenRecorderSpec_base { + private weak var cxxWrapper: HybridNitroScreenRecorderSpec_cxx? = nil + public init() { } + public func getCxxWrapper() -> HybridNitroScreenRecorderSpec_cxx { + #if DEBUG + guard self is HybridNitroScreenRecorderSpec else { + fatalError("`self` is not a `HybridNitroScreenRecorderSpec`! Did you accidentally inherit from `HybridNitroScreenRecorderSpec_base` instead of `HybridNitroScreenRecorderSpec`?") + } + #endif + if let cxxWrapper = self.cxxWrapper { + return cxxWrapper + } else { + let cxxWrapper = HybridNitroScreenRecorderSpec_cxx(self as! HybridNitroScreenRecorderSpec) + self.cxxWrapper = cxxWrapper + return cxxWrapper + } + } +} + +/** + * A Swift base-protocol representing the NitroScreenRecorder HybridObject. + * Implement this protocol to create Swift-based instances of NitroScreenRecorder. + * ```swift + * class HybridNitroScreenRecorder : HybridNitroScreenRecorderSpec { + * // ... + * } + * ``` + */ +public typealias HybridNitroScreenRecorderSpec = HybridNitroScreenRecorderSpec_protocol & HybridNitroScreenRecorderSpec_base diff --git a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift new file mode 100644 index 0000000..50e2a0f --- /dev/null +++ b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift @@ -0,0 +1,368 @@ +/// +/// HybridNitroScreenRecorderSpec_cxx.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import Foundation +import NitroModules +import NitroModules + +/** + * A class implementation that bridges HybridNitroScreenRecorderSpec over to C++. + * In C++, we cannot use Swift protocols - so we need to wrap it in a class to make it strongly defined. + * + * Also, some Swift types need to be bridged with special handling: + * - Enums need to be wrapped in Structs, otherwise they cannot be accessed bi-directionally (Swift bug: https://github.com/swiftlang/swift/issues/75330) + * - Other HybridObjects need to be wrapped/unwrapped from the Swift TCxx wrapper + * - Throwing methods need to be wrapped with a Result type, as exceptions cannot be propagated to C++ + */ +open class HybridNitroScreenRecorderSpec_cxx { + /** + * The Swift <> C++ bridge's namespace (`margelo::nitro::nitroscreenrecorder::bridge::swift`) + * from `NitroScreenRecorder-Swift-Cxx-Bridge.hpp`. + * This contains specialized C++ templates, and C++ helper functions that can be accessed from Swift. + */ + public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Holds an instance of the `HybridNitroScreenRecorderSpec` Swift protocol. + */ + private var __implementation: any HybridNitroScreenRecorderSpec + + /** + * Holds a weak pointer to the C++ class that wraps the Swift class. + */ + private var __cxxPart: bridge.std__weak_ptr_HybridNitroScreenRecorderSpec_ + + /** + * Create a new `HybridNitroScreenRecorderSpec_cxx` that wraps the given `HybridNitroScreenRecorderSpec`. + * All properties and methods bridge to C++ types. + */ + public init(_ implementation: any HybridNitroScreenRecorderSpec) { + self.__implementation = implementation + self.__cxxPart = .init() + /* no base class */ + } + + /** + * Get the actual `HybridNitroScreenRecorderSpec` instance this class wraps. + */ + @inline(__always) + public func getHybridNitroScreenRecorderSpec() -> any HybridNitroScreenRecorderSpec { + return __implementation + } + + /** + * Casts this instance to a retained unsafe raw pointer. + * This acquires one additional strong reference on the object! + */ + public func toUnsafe() -> UnsafeMutableRawPointer { + return Unmanaged.passRetained(self).toOpaque() + } + + /** + * Casts an unsafe pointer to a `HybridNitroScreenRecorderSpec_cxx`. + * The pointer has to be a retained opaque `Unmanaged`. + * This removes one strong reference from the object! + */ + public class func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> HybridNitroScreenRecorderSpec_cxx { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() + } + + /** + * Gets (or creates) the C++ part of this Hybrid Object. + * The C++ part is a `std::shared_ptr`. + */ + public func getCxxPart() -> bridge.std__shared_ptr_HybridNitroScreenRecorderSpec_ { + let cachedCxxPart = self.__cxxPart.lock() + if Bool(fromCxx: cachedCxxPart) { + return cachedCxxPart + } else { + let newCxxPart = bridge.create_std__shared_ptr_HybridNitroScreenRecorderSpec_(self.toUnsafe()) + __cxxPart = bridge.weakify_std__shared_ptr_HybridNitroScreenRecorderSpec_(newCxxPart) + return newCxxPart + } + } + + + + /** + * Get the memory size of the Swift class (plus size of any other allocations) + * so the JS VM can properly track it and garbage-collect the JS object if needed. + */ + @inline(__always) + public var memorySize: Int { + return MemoryHelper.getSizeOf(self.__implementation) + self.__implementation.memorySize + } + + /** + * Call dispose() on the Swift class. + * This _may_ be called manually from JS. + */ + @inline(__always) + public func dispose() { + self.__implementation.dispose() + } + + /** + * Call toString() on the Swift class. + */ + @inline(__always) + public func toString() -> String { + return self.__implementation.toString() + } + + // Properties + + + // Methods + @inline(__always) + public final func getCameraPermissionStatus() -> bridge.Result_PermissionStatus_ { + do { + let __result = try self.__implementation.getCameraPermissionStatus() + let __resultCpp = __result + return bridge.create_Result_PermissionStatus_(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_PermissionStatus_(__exceptionPtr) + } + } + + @inline(__always) + public final func getMicrophonePermissionStatus() -> bridge.Result_PermissionStatus_ { + do { + let __result = try self.__implementation.getMicrophonePermissionStatus() + let __resultCpp = __result + return bridge.create_Result_PermissionStatus_(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_PermissionStatus_(__exceptionPtr) + } + } + + @inline(__always) + public final func requestCameraPermission() -> bridge.Result_std__shared_ptr_Promise_PermissionResponse___ { + do { + let __result = try self.__implementation.requestCameraPermission() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_PermissionResponse__ in + let __promise = bridge.create_std__shared_ptr_Promise_PermissionResponse__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_PermissionResponse__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_PermissionResponse___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_PermissionResponse___(__exceptionPtr) + } + } + + @inline(__always) + public final func requestMicrophonePermission() -> bridge.Result_std__shared_ptr_Promise_PermissionResponse___ { + do { + let __result = try self.__implementation.requestMicrophonePermission() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_PermissionResponse__ in + let __promise = bridge.create_std__shared_ptr_Promise_PermissionResponse__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_PermissionResponse__(__promise) + __result + .then({ __result in __promiseHolder.resolve(__result) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_PermissionResponse___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_PermissionResponse___(__exceptionPtr) + } + } + + @inline(__always) + public final func addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere: Bool, callback: bridge.Func_void_ScreenRecordingEvent) -> bridge.Result_double_ { + do { + let __result = try self.__implementation.addScreenRecordingListener(ignoreRecordingsInitiatedElsewhere: ignoreRecordingsInitiatedElsewhere, callback: { () -> (ScreenRecordingEvent) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_ScreenRecordingEvent(callback) + return { (__event: ScreenRecordingEvent) -> Void in + __wrappedFunction.call(__event) + } + }()) + let __resultCpp = __result + return bridge.create_Result_double_(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_double_(__exceptionPtr) + } + } + + @inline(__always) + public final func removeScreenRecordingListener(id: Double) -> bridge.Result_void_ { + do { + try self.__implementation.removeScreenRecordingListener(id: id) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func addBroadcastPickerListener(callback: bridge.Func_void_BroadcastPickerPresentationEvent) -> bridge.Result_double_ { + do { + let __result = try self.__implementation.addBroadcastPickerListener(callback: { () -> (BroadcastPickerPresentationEvent) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_BroadcastPickerPresentationEvent(callback) + return { (__event: BroadcastPickerPresentationEvent) -> Void in + __wrappedFunction.call(__event.rawValue) + } + }()) + let __resultCpp = __result + return bridge.create_Result_double_(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_double_(__exceptionPtr) + } + } + + @inline(__always) + public final func removeBroadcastPickerListener(id: Double) -> bridge.Result_void_ { + do { + try self.__implementation.removeBroadcastPickerListener(id: id) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func startInAppRecording(enableMic: Bool, enableCamera: Bool, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: Int32, separateAudioFile: Bool, onRecordingFinished: bridge.Func_void_ScreenRecordingFile) -> bridge.Result_void_ { + do { + try self.__implementation.startInAppRecording(enableMic: enableMic, enableCamera: enableCamera, cameraPreviewStyle: cameraPreviewStyle, cameraDevice: margelo.nitro.nitroscreenrecorder.CameraDevice(rawValue: cameraDevice)!, separateAudioFile: separateAudioFile, onRecordingFinished: { () -> (ScreenRecordingFile) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_ScreenRecordingFile(onRecordingFinished) + return { (__file: ScreenRecordingFile) -> Void in + __wrappedFunction.call(__file) + } + }()) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func stopInAppRecording() -> bridge.Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____ { + do { + let __result = try self.__implementation.stopInAppRecording() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__optional_ScreenRecordingFile___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__optional_ScreenRecordingFile___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__optional_ScreenRecordingFile___(__promise) + __result + .then({ __result in __promiseHolder.resolve({ () -> bridge.std__optional_ScreenRecordingFile_ in + if let __unwrappedValue = __result { + return bridge.create_std__optional_ScreenRecordingFile_(__unwrappedValue) + } else { + return .init() + } + }()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____(__exceptionPtr) + } + } + + @inline(__always) + public final func cancelInAppRecording() -> bridge.Result_std__shared_ptr_Promise_void___ { + do { + let __result = try self.__implementation.cancelInAppRecording() + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_void__ in + let __promise = bridge.create_std__shared_ptr_Promise_void__() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_void__(__promise) + __result + .then({ __result in __promiseHolder.resolve() }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_void___(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr) + } + } + + @inline(__always) + public final func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: bridge.Func_void_RecordingError) -> bridge.Result_void_ { + do { + try self.__implementation.startGlobalRecording(enableMic: enableMic, separateAudioFile: separateAudioFile, onRecordingError: { () -> (RecordingError) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_RecordingError(onRecordingError) + return { (__error: RecordingError) -> Void in + __wrappedFunction.call(__error) + } + }()) + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } + + @inline(__always) + public final func stopGlobalRecording(settledTimeMs: Double) -> bridge.Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____ { + do { + let __result = try self.__implementation.stopGlobalRecording(settledTimeMs: settledTimeMs) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__optional_ScreenRecordingFile___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__optional_ScreenRecordingFile___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__optional_ScreenRecordingFile___(__promise) + __result + .then({ __result in __promiseHolder.resolve({ () -> bridge.std__optional_ScreenRecordingFile_ in + if let __unwrappedValue = __result { + return bridge.create_std__optional_ScreenRecordingFile_(__unwrappedValue) + } else { + return .init() + } + }()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__optional_ScreenRecordingFile____(__exceptionPtr) + } + } + + @inline(__always) + public final func retrieveLastGlobalRecording() -> bridge.Result_std__optional_ScreenRecordingFile__ { + do { + let __result = try self.__implementation.retrieveLastGlobalRecording() + let __resultCpp = { () -> bridge.std__optional_ScreenRecordingFile_ in + if let __unwrappedValue = __result { + return bridge.create_std__optional_ScreenRecordingFile_(__unwrappedValue) + } else { + return .init() + } + }() + return bridge.create_Result_std__optional_ScreenRecordingFile__(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__optional_ScreenRecordingFile__(__exceptionPtr) + } + } + + @inline(__always) + public final func clearRecordingCache() -> bridge.Result_void_ { + do { + try self.__implementation.clearRecordingCache() + return bridge.create_Result_void_() + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_void_(__exceptionPtr) + } + } +} diff --git a/nitrogen/generated/ios/swift/PermissionResponse.swift b/nitrogen/generated/ios/swift/PermissionResponse.swift new file mode 100644 index 0000000..3397a9b --- /dev/null +++ b/nitrogen/generated/ios/swift/PermissionResponse.swift @@ -0,0 +1,68 @@ +/// +/// PermissionResponse.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `PermissionResponse`, backed by a C++ struct. + */ +public typealias PermissionResponse = margelo.nitro.nitroscreenrecorder.PermissionResponse + +public extension PermissionResponse { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `PermissionResponse`. + */ + init(canAskAgain: Bool, granted: Bool, status: PermissionStatus, expiresAt: Double) { + self.init(canAskAgain, granted, status, expiresAt) + } + + var canAskAgain: Bool { + @inline(__always) + get { + return self.__canAskAgain + } + @inline(__always) + set { + self.__canAskAgain = newValue + } + } + + var granted: Bool { + @inline(__always) + get { + return self.__granted + } + @inline(__always) + set { + self.__granted = newValue + } + } + + var status: PermissionStatus { + @inline(__always) + get { + return self.__status + } + @inline(__always) + set { + self.__status = newValue + } + } + + var expiresAt: Double { + @inline(__always) + get { + return self.__expiresAt + } + @inline(__always) + set { + self.__expiresAt = newValue + } + } +} diff --git a/nitrogen/generated/ios/swift/PermissionStatus.swift b/nitrogen/generated/ios/swift/PermissionStatus.swift new file mode 100644 index 0000000..95c5627 --- /dev/null +++ b/nitrogen/generated/ios/swift/PermissionStatus.swift @@ -0,0 +1,44 @@ +/// +/// PermissionStatus.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `PermissionStatus`, backed by a C++ enum. + */ +public typealias PermissionStatus = margelo.nitro.nitroscreenrecorder.PermissionStatus + +public extension PermissionStatus { + /** + * Get a PermissionStatus for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "denied": + self = .denied + case "granted": + self = .granted + case "undetermined": + self = .undetermined + default: + return nil + } + } + + /** + * Get the String value this PermissionStatus represents. + */ + var stringValue: String { + switch self { + case .denied: + return "denied" + case .granted: + return "granted" + case .undetermined: + return "undetermined" + } + } +} diff --git a/nitrogen/generated/ios/swift/RecorderCameraStyle.swift b/nitrogen/generated/ios/swift/RecorderCameraStyle.swift new file mode 100644 index 0000000..200f481 --- /dev/null +++ b/nitrogen/generated/ios/swift/RecorderCameraStyle.swift @@ -0,0 +1,162 @@ +/// +/// RecorderCameraStyle.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `RecorderCameraStyle`, backed by a C++ struct. + */ +public typealias RecorderCameraStyle = margelo.nitro.nitroscreenrecorder.RecorderCameraStyle + +public extension RecorderCameraStyle { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `RecorderCameraStyle`. + */ + init(top: Double?, left: Double?, width: Double?, height: Double?, borderRadius: Double?, borderWidth: Double?) { + self.init({ () -> bridge.std__optional_double_ in + if let __unwrappedValue = top { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = left { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = width { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = height { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = borderRadius { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }(), { () -> bridge.std__optional_double_ in + if let __unwrappedValue = borderWidth { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var top: Double? { + @inline(__always) + get { + return self.__top.value + } + @inline(__always) + set { + self.__top = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var left: Double? { + @inline(__always) + get { + return self.__left.value + } + @inline(__always) + set { + self.__left = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var width: Double? { + @inline(__always) + get { + return self.__width.value + } + @inline(__always) + set { + self.__width = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var height: Double? { + @inline(__always) + get { + return self.__height.value + } + @inline(__always) + set { + self.__height = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var borderRadius: Double? { + @inline(__always) + get { + return self.__borderRadius.value + } + @inline(__always) + set { + self.__borderRadius = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } + + var borderWidth: Double? { + @inline(__always) + get { + return self.__borderWidth.value + } + @inline(__always) + set { + self.__borderWidth = { () -> bridge.std__optional_double_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_double_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/nitrogen/generated/ios/swift/RecordingError.swift b/nitrogen/generated/ios/swift/RecordingError.swift new file mode 100644 index 0000000..d46c945 --- /dev/null +++ b/nitrogen/generated/ios/swift/RecordingError.swift @@ -0,0 +1,46 @@ +/// +/// RecordingError.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `RecordingError`, backed by a C++ struct. + */ +public typealias RecordingError = margelo.nitro.nitroscreenrecorder.RecordingError + +public extension RecordingError { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `RecordingError`. + */ + init(name: String, message: String) { + self.init(std.string(name), std.string(message)) + } + + var name: String { + @inline(__always) + get { + return String(self.__name) + } + @inline(__always) + set { + self.__name = std.string(newValue) + } + } + + var message: String { + @inline(__always) + get { + return String(self.__message) + } + @inline(__always) + set { + self.__message = std.string(newValue) + } + } +} diff --git a/nitrogen/generated/ios/swift/RecordingEventReason.swift b/nitrogen/generated/ios/swift/RecordingEventReason.swift new file mode 100644 index 0000000..d46f901 --- /dev/null +++ b/nitrogen/generated/ios/swift/RecordingEventReason.swift @@ -0,0 +1,40 @@ +/// +/// RecordingEventReason.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `RecordingEventReason`, backed by a C++ enum. + */ +public typealias RecordingEventReason = margelo.nitro.nitroscreenrecorder.RecordingEventReason + +public extension RecordingEventReason { + /** + * Get a RecordingEventReason for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "began": + self = .began + case "ended": + self = .ended + default: + return nil + } + } + + /** + * Get the String value this RecordingEventReason represents. + */ + var stringValue: String { + switch self { + case .began: + return "began" + case .ended: + return "ended" + } + } +} diff --git a/nitrogen/generated/ios/swift/RecordingEventType.swift b/nitrogen/generated/ios/swift/RecordingEventType.swift new file mode 100644 index 0000000..ec9847c --- /dev/null +++ b/nitrogen/generated/ios/swift/RecordingEventType.swift @@ -0,0 +1,40 @@ +/// +/// RecordingEventType.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +/** + * Represents the JS union `RecordingEventType`, backed by a C++ enum. + */ +public typealias RecordingEventType = margelo.nitro.nitroscreenrecorder.RecordingEventType + +public extension RecordingEventType { + /** + * Get a RecordingEventType for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "global": + self = .global + case "withinApp": + self = .withinapp + default: + return nil + } + } + + /** + * Get the String value this RecordingEventType represents. + */ + var stringValue: String { + switch self { + case .global: + return "global" + case .withinapp: + return "withinApp" + } + } +} diff --git a/nitrogen/generated/ios/swift/ScreenRecordingEvent.swift b/nitrogen/generated/ios/swift/ScreenRecordingEvent.swift new file mode 100644 index 0000000..32ca2fb --- /dev/null +++ b/nitrogen/generated/ios/swift/ScreenRecordingEvent.swift @@ -0,0 +1,46 @@ +/// +/// ScreenRecordingEvent.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `ScreenRecordingEvent`, backed by a C++ struct. + */ +public typealias ScreenRecordingEvent = margelo.nitro.nitroscreenrecorder.ScreenRecordingEvent + +public extension ScreenRecordingEvent { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `ScreenRecordingEvent`. + */ + init(type: RecordingEventType, reason: RecordingEventReason) { + self.init(type, reason) + } + + var type: RecordingEventType { + @inline(__always) + get { + return self.__type + } + @inline(__always) + set { + self.__type = newValue + } + } + + var reason: RecordingEventReason { + @inline(__always) + get { + return self.__reason + } + @inline(__always) + set { + self.__reason = newValue + } + } +} diff --git a/nitrogen/generated/ios/swift/ScreenRecordingFile.swift b/nitrogen/generated/ios/swift/ScreenRecordingFile.swift new file mode 100644 index 0000000..9483464 --- /dev/null +++ b/nitrogen/generated/ios/swift/ScreenRecordingFile.swift @@ -0,0 +1,102 @@ +/// +/// ScreenRecordingFile.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `ScreenRecordingFile`, backed by a C++ struct. + */ +public typealias ScreenRecordingFile = margelo.nitro.nitroscreenrecorder.ScreenRecordingFile + +public extension ScreenRecordingFile { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `ScreenRecordingFile`. + */ + init(path: String, name: String, size: Double, duration: Double, enabledMicrophone: Bool, audioFile: AudioRecordingFile?) { + self.init(std.string(path), std.string(name), size, duration, enabledMicrophone, { () -> bridge.std__optional_AudioRecordingFile_ in + if let __unwrappedValue = audioFile { + return bridge.create_std__optional_AudioRecordingFile_(__unwrappedValue) + } else { + return .init() + } + }()) + } + + var path: String { + @inline(__always) + get { + return String(self.__path) + } + @inline(__always) + set { + self.__path = std.string(newValue) + } + } + + var name: String { + @inline(__always) + get { + return String(self.__name) + } + @inline(__always) + set { + self.__name = std.string(newValue) + } + } + + var size: Double { + @inline(__always) + get { + return self.__size + } + @inline(__always) + set { + self.__size = newValue + } + } + + var duration: Double { + @inline(__always) + get { + return self.__duration + } + @inline(__always) + set { + self.__duration = newValue + } + } + + var enabledMicrophone: Bool { + @inline(__always) + get { + return self.__enabledMicrophone + } + @inline(__always) + set { + self.__enabledMicrophone = newValue + } + } + + var audioFile: AudioRecordingFile? { + @inline(__always) + get { + return self.__audioFile.value + } + @inline(__always) + set { + self.__audioFile = { () -> bridge.std__optional_AudioRecordingFile_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_AudioRecordingFile_(__unwrappedValue) + } else { + return .init() + } + }() + } + } +} diff --git a/nitrogen/generated/shared/c++/AudioRecordingFile.hpp b/nitrogen/generated/shared/c++/AudioRecordingFile.hpp new file mode 100644 index 0000000..01df944 --- /dev/null +++ b/nitrogen/generated/shared/c++/AudioRecordingFile.hpp @@ -0,0 +1,87 @@ +/// +/// AudioRecordingFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (AudioRecordingFile). + */ + struct AudioRecordingFile { + public: + std::string path SWIFT_PRIVATE; + std::string name SWIFT_PRIVATE; + double size SWIFT_PRIVATE; + double duration SWIFT_PRIVATE; + + public: + AudioRecordingFile() = default; + explicit AudioRecordingFile(std::string path, std::string name, double size, double duration): path(path), name(name), size(size), duration(duration) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ AudioRecordingFile <> JS AudioRecordingFile (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::AudioRecordingFile fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::AudioRecordingFile( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "path")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "name")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "size")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "duration")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::AudioRecordingFile& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "path", JSIConverter::toJSI(runtime, arg.path)); + obj.setProperty(runtime, "name", JSIConverter::toJSI(runtime, arg.name)); + obj.setProperty(runtime, "size", JSIConverter::toJSI(runtime, arg.size)); + obj.setProperty(runtime, "duration", JSIConverter::toJSI(runtime, arg.duration)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "path"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "name"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "size"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "duration"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/BroadcastPickerPresentationEvent.hpp b/nitrogen/generated/shared/c++/BroadcastPickerPresentationEvent.hpp new file mode 100644 index 0000000..f6fa14e --- /dev/null +++ b/nitrogen/generated/shared/c++/BroadcastPickerPresentationEvent.hpp @@ -0,0 +1,76 @@ +/// +/// BroadcastPickerPresentationEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * An enum which can be represented as a JavaScript union (BroadcastPickerPresentationEvent). + */ + enum class BroadcastPickerPresentationEvent { + SHOWING SWIFT_NAME(showing) = 0, + DISMISSED SWIFT_NAME(dismissed) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ BroadcastPickerPresentationEvent <> JS BroadcastPickerPresentationEvent (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::BroadcastPickerPresentationEvent fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("showing"): return margelo::nitro::nitroscreenrecorder::BroadcastPickerPresentationEvent::SHOWING; + case hashString("dismissed"): return margelo::nitro::nitroscreenrecorder::BroadcastPickerPresentationEvent::DISMISSED; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum BroadcastPickerPresentationEvent - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitroscreenrecorder::BroadcastPickerPresentationEvent arg) { + switch (arg) { + case margelo::nitro::nitroscreenrecorder::BroadcastPickerPresentationEvent::SHOWING: return JSIConverter::toJSI(runtime, "showing"); + case margelo::nitro::nitroscreenrecorder::BroadcastPickerPresentationEvent::DISMISSED: return JSIConverter::toJSI(runtime, "dismissed"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert BroadcastPickerPresentationEvent to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("showing"): + case hashString("dismissed"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/CameraDevice.hpp b/nitrogen/generated/shared/c++/CameraDevice.hpp new file mode 100644 index 0000000..8bde354 --- /dev/null +++ b/nitrogen/generated/shared/c++/CameraDevice.hpp @@ -0,0 +1,76 @@ +/// +/// CameraDevice.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * An enum which can be represented as a JavaScript union (CameraDevice). + */ + enum class CameraDevice { + FRONT SWIFT_NAME(front) = 0, + BACK SWIFT_NAME(back) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ CameraDevice <> JS CameraDevice (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::CameraDevice fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("front"): return margelo::nitro::nitroscreenrecorder::CameraDevice::FRONT; + case hashString("back"): return margelo::nitro::nitroscreenrecorder::CameraDevice::BACK; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum CameraDevice - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitroscreenrecorder::CameraDevice arg) { + switch (arg) { + case margelo::nitro::nitroscreenrecorder::CameraDevice::FRONT: return JSIConverter::toJSI(runtime, "front"); + case margelo::nitro::nitroscreenrecorder::CameraDevice::BACK: return JSIConverter::toJSI(runtime, "back"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert CameraDevice to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("front"): + case hashString("back"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.cpp b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.cpp new file mode 100644 index 0000000..81a86e8 --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.cpp @@ -0,0 +1,35 @@ +/// +/// HybridNitroScreenRecorderSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#include "HybridNitroScreenRecorderSpec.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + void HybridNitroScreenRecorderSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("getCameraPermissionStatus", &HybridNitroScreenRecorderSpec::getCameraPermissionStatus); + prototype.registerHybridMethod("getMicrophonePermissionStatus", &HybridNitroScreenRecorderSpec::getMicrophonePermissionStatus); + prototype.registerHybridMethod("requestCameraPermission", &HybridNitroScreenRecorderSpec::requestCameraPermission); + prototype.registerHybridMethod("requestMicrophonePermission", &HybridNitroScreenRecorderSpec::requestMicrophonePermission); + prototype.registerHybridMethod("addScreenRecordingListener", &HybridNitroScreenRecorderSpec::addScreenRecordingListener); + prototype.registerHybridMethod("removeScreenRecordingListener", &HybridNitroScreenRecorderSpec::removeScreenRecordingListener); + prototype.registerHybridMethod("addBroadcastPickerListener", &HybridNitroScreenRecorderSpec::addBroadcastPickerListener); + prototype.registerHybridMethod("removeBroadcastPickerListener", &HybridNitroScreenRecorderSpec::removeBroadcastPickerListener); + prototype.registerHybridMethod("startInAppRecording", &HybridNitroScreenRecorderSpec::startInAppRecording); + prototype.registerHybridMethod("stopInAppRecording", &HybridNitroScreenRecorderSpec::stopInAppRecording); + prototype.registerHybridMethod("cancelInAppRecording", &HybridNitroScreenRecorderSpec::cancelInAppRecording); + prototype.registerHybridMethod("startGlobalRecording", &HybridNitroScreenRecorderSpec::startGlobalRecording); + prototype.registerHybridMethod("stopGlobalRecording", &HybridNitroScreenRecorderSpec::stopGlobalRecording); + prototype.registerHybridMethod("retrieveLastGlobalRecording", &HybridNitroScreenRecorderSpec::retrieveLastGlobalRecording); + prototype.registerHybridMethod("clearRecordingCache", &HybridNitroScreenRecorderSpec::clearRecordingCache); + }); + } + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp new file mode 100644 index 0000000..d3a2b5e --- /dev/null +++ b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp @@ -0,0 +1,101 @@ +/// +/// HybridNitroScreenRecorderSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `PermissionStatus` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } +// Forward declaration of `PermissionResponse` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } +// Forward declaration of `ScreenRecordingEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingEvent; } +// Forward declaration of `BroadcastPickerPresentationEvent` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresentationEvent; } +// Forward declaration of `RecorderCameraStyle` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } +// Forward declaration of `CameraDevice` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } +// Forward declaration of `ScreenRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } + +#include "PermissionStatus.hpp" +#include "PermissionResponse.hpp" +#include +#include "ScreenRecordingEvent.hpp" +#include +#include "BroadcastPickerPresentationEvent.hpp" +#include "RecorderCameraStyle.hpp" +#include "CameraDevice.hpp" +#include "ScreenRecordingFile.hpp" +#include +#include "RecordingError.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace margelo::nitro; + + /** + * An abstract base class for `NitroScreenRecorder` + * Inherit this class to create instances of `HybridNitroScreenRecorderSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridNitroScreenRecorder: public HybridNitroScreenRecorderSpec { + * public: + * HybridNitroScreenRecorder(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridNitroScreenRecorderSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridNitroScreenRecorderSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridNitroScreenRecorderSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual PermissionStatus getCameraPermissionStatus() = 0; + virtual PermissionStatus getMicrophonePermissionStatus() = 0; + virtual std::shared_ptr> requestCameraPermission() = 0; + virtual std::shared_ptr> requestMicrophonePermission() = 0; + virtual double addScreenRecordingListener(bool ignoreRecordingsInitiatedElsewhere, const std::function& callback) = 0; + virtual void removeScreenRecordingListener(double id) = 0; + virtual double addBroadcastPickerListener(const std::function& callback) = 0; + virtual void removeBroadcastPickerListener(double id) = 0; + virtual void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) = 0; + virtual std::shared_ptr>> stopInAppRecording() = 0; + virtual std::shared_ptr> cancelInAppRecording() = 0; + virtual void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) = 0; + virtual std::shared_ptr>> stopGlobalRecording(double settledTimeMs) = 0; + virtual std::optional retrieveLastGlobalRecording() = 0; + virtual void clearRecordingCache() = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "NitroScreenRecorder"; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/shared/c++/PermissionResponse.hpp b/nitrogen/generated/shared/c++/PermissionResponse.hpp new file mode 100644 index 0000000..805fce5 --- /dev/null +++ b/nitrogen/generated/shared/c++/PermissionResponse.hpp @@ -0,0 +1,88 @@ +/// +/// PermissionResponse.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `PermissionStatus` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } + +#include "PermissionStatus.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (PermissionResponse). + */ + struct PermissionResponse { + public: + bool canAskAgain SWIFT_PRIVATE; + bool granted SWIFT_PRIVATE; + PermissionStatus status SWIFT_PRIVATE; + double expiresAt SWIFT_PRIVATE; + + public: + PermissionResponse() = default; + explicit PermissionResponse(bool canAskAgain, bool granted, PermissionStatus status, double expiresAt): canAskAgain(canAskAgain), granted(granted), status(status), expiresAt(expiresAt) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ PermissionResponse <> JS PermissionResponse (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::PermissionResponse fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::PermissionResponse( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "canAskAgain")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "granted")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "status")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "expiresAt")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::PermissionResponse& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "canAskAgain", JSIConverter::toJSI(runtime, arg.canAskAgain)); + obj.setProperty(runtime, "granted", JSIConverter::toJSI(runtime, arg.granted)); + obj.setProperty(runtime, "status", JSIConverter::toJSI(runtime, arg.status)); + obj.setProperty(runtime, "expiresAt", JSIConverter::toJSI(runtime, arg.expiresAt)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "canAskAgain"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "granted"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "status"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "expiresAt"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/PermissionStatus.hpp b/nitrogen/generated/shared/c++/PermissionStatus.hpp new file mode 100644 index 0000000..909b1a3 --- /dev/null +++ b/nitrogen/generated/shared/c++/PermissionStatus.hpp @@ -0,0 +1,80 @@ +/// +/// PermissionStatus.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * An enum which can be represented as a JavaScript union (PermissionStatus). + */ + enum class PermissionStatus { + DENIED SWIFT_NAME(denied) = 0, + GRANTED SWIFT_NAME(granted) = 1, + UNDETERMINED SWIFT_NAME(undetermined) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ PermissionStatus <> JS PermissionStatus (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::PermissionStatus fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("denied"): return margelo::nitro::nitroscreenrecorder::PermissionStatus::DENIED; + case hashString("granted"): return margelo::nitro::nitroscreenrecorder::PermissionStatus::GRANTED; + case hashString("undetermined"): return margelo::nitro::nitroscreenrecorder::PermissionStatus::UNDETERMINED; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum PermissionStatus - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitroscreenrecorder::PermissionStatus arg) { + switch (arg) { + case margelo::nitro::nitroscreenrecorder::PermissionStatus::DENIED: return JSIConverter::toJSI(runtime, "denied"); + case margelo::nitro::nitroscreenrecorder::PermissionStatus::GRANTED: return JSIConverter::toJSI(runtime, "granted"); + case margelo::nitro::nitroscreenrecorder::PermissionStatus::UNDETERMINED: return JSIConverter::toJSI(runtime, "undetermined"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert PermissionStatus to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("denied"): + case hashString("granted"): + case hashString("undetermined"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/RecorderCameraStyle.hpp b/nitrogen/generated/shared/c++/RecorderCameraStyle.hpp new file mode 100644 index 0000000..15238d1 --- /dev/null +++ b/nitrogen/generated/shared/c++/RecorderCameraStyle.hpp @@ -0,0 +1,95 @@ +/// +/// RecorderCameraStyle.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (RecorderCameraStyle). + */ + struct RecorderCameraStyle { + public: + std::optional top SWIFT_PRIVATE; + std::optional left SWIFT_PRIVATE; + std::optional width SWIFT_PRIVATE; + std::optional height SWIFT_PRIVATE; + std::optional borderRadius SWIFT_PRIVATE; + std::optional borderWidth SWIFT_PRIVATE; + + public: + RecorderCameraStyle() = default; + explicit RecorderCameraStyle(std::optional top, std::optional left, std::optional width, std::optional height, std::optional borderRadius, std::optional borderWidth): top(top), left(left), width(width), height(height), borderRadius(borderRadius), borderWidth(borderWidth) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ RecorderCameraStyle <> JS RecorderCameraStyle (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::RecorderCameraStyle fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::RecorderCameraStyle( + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "top")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "left")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "width")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "height")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "borderRadius")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "borderWidth")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::RecorderCameraStyle& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "top", JSIConverter>::toJSI(runtime, arg.top)); + obj.setProperty(runtime, "left", JSIConverter>::toJSI(runtime, arg.left)); + obj.setProperty(runtime, "width", JSIConverter>::toJSI(runtime, arg.width)); + obj.setProperty(runtime, "height", JSIConverter>::toJSI(runtime, arg.height)); + obj.setProperty(runtime, "borderRadius", JSIConverter>::toJSI(runtime, arg.borderRadius)); + obj.setProperty(runtime, "borderWidth", JSIConverter>::toJSI(runtime, arg.borderWidth)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "top"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "left"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "width"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "height"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "borderRadius"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "borderWidth"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/RecordingError.hpp b/nitrogen/generated/shared/c++/RecordingError.hpp new file mode 100644 index 0000000..f499c5c --- /dev/null +++ b/nitrogen/generated/shared/c++/RecordingError.hpp @@ -0,0 +1,79 @@ +/// +/// RecordingError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (RecordingError). + */ + struct RecordingError { + public: + std::string name SWIFT_PRIVATE; + std::string message SWIFT_PRIVATE; + + public: + RecordingError() = default; + explicit RecordingError(std::string name, std::string message): name(name), message(message) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ RecordingError <> JS RecordingError (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::RecordingError fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::RecordingError( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "name")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "message")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::RecordingError& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "name", JSIConverter::toJSI(runtime, arg.name)); + obj.setProperty(runtime, "message", JSIConverter::toJSI(runtime, arg.message)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "name"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "message"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/RecordingEventReason.hpp b/nitrogen/generated/shared/c++/RecordingEventReason.hpp new file mode 100644 index 0000000..b7a4433 --- /dev/null +++ b/nitrogen/generated/shared/c++/RecordingEventReason.hpp @@ -0,0 +1,76 @@ +/// +/// RecordingEventReason.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * An enum which can be represented as a JavaScript union (RecordingEventReason). + */ + enum class RecordingEventReason { + BEGAN SWIFT_NAME(began) = 0, + ENDED SWIFT_NAME(ended) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ RecordingEventReason <> JS RecordingEventReason (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::RecordingEventReason fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("began"): return margelo::nitro::nitroscreenrecorder::RecordingEventReason::BEGAN; + case hashString("ended"): return margelo::nitro::nitroscreenrecorder::RecordingEventReason::ENDED; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum RecordingEventReason - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitroscreenrecorder::RecordingEventReason arg) { + switch (arg) { + case margelo::nitro::nitroscreenrecorder::RecordingEventReason::BEGAN: return JSIConverter::toJSI(runtime, "began"); + case margelo::nitro::nitroscreenrecorder::RecordingEventReason::ENDED: return JSIConverter::toJSI(runtime, "ended"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert RecordingEventReason to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("began"): + case hashString("ended"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/RecordingEventType.hpp b/nitrogen/generated/shared/c++/RecordingEventType.hpp new file mode 100644 index 0000000..274e10f --- /dev/null +++ b/nitrogen/generated/shared/c++/RecordingEventType.hpp @@ -0,0 +1,76 @@ +/// +/// RecordingEventType.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * An enum which can be represented as a JavaScript union (RecordingEventType). + */ + enum class RecordingEventType { + GLOBAL SWIFT_NAME(global) = 0, + WITHINAPP SWIFT_NAME(withinapp) = 1, + } CLOSED_ENUM; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ RecordingEventType <> JS RecordingEventType (union) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::RecordingEventType fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + std::string unionValue = JSIConverter::fromJSI(runtime, arg); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("global"): return margelo::nitro::nitroscreenrecorder::RecordingEventType::GLOBAL; + case hashString("withinApp"): return margelo::nitro::nitroscreenrecorder::RecordingEventType::WITHINAPP; + default: [[unlikely]] + throw std::invalid_argument("Cannot convert \"" + unionValue + "\" to enum RecordingEventType - invalid value!"); + } + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::nitroscreenrecorder::RecordingEventType arg) { + switch (arg) { + case margelo::nitro::nitroscreenrecorder::RecordingEventType::GLOBAL: return JSIConverter::toJSI(runtime, "global"); + case margelo::nitro::nitroscreenrecorder::RecordingEventType::WITHINAPP: return JSIConverter::toJSI(runtime, "withinApp"); + default: [[unlikely]] + throw std::invalid_argument("Cannot convert RecordingEventType to JS - invalid value: " + + std::to_string(static_cast(arg)) + "!"); + } + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isString()) { + return false; + } + std::string unionValue = JSIConverter::fromJSI(runtime, value); + switch (hashString(unionValue.c_str(), unionValue.size())) { + case hashString("global"): + case hashString("withinApp"): + return true; + default: + return false; + } + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/ScreenRecordingEvent.hpp b/nitrogen/generated/shared/c++/ScreenRecordingEvent.hpp new file mode 100644 index 0000000..c8c1452 --- /dev/null +++ b/nitrogen/generated/shared/c++/ScreenRecordingEvent.hpp @@ -0,0 +1,83 @@ +/// +/// ScreenRecordingEvent.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `RecordingEventType` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventType; } +// Forward declaration of `RecordingEventReason` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } + +#include "RecordingEventType.hpp" +#include "RecordingEventReason.hpp" + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (ScreenRecordingEvent). + */ + struct ScreenRecordingEvent { + public: + RecordingEventType type SWIFT_PRIVATE; + RecordingEventReason reason SWIFT_PRIVATE; + + public: + ScreenRecordingEvent() = default; + explicit ScreenRecordingEvent(RecordingEventType type, RecordingEventReason reason): type(type), reason(reason) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ ScreenRecordingEvent <> JS ScreenRecordingEvent (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::ScreenRecordingEvent fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::ScreenRecordingEvent( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "type")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "reason")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::ScreenRecordingEvent& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "type", JSIConverter::toJSI(runtime, arg.type)); + obj.setProperty(runtime, "reason", JSIConverter::toJSI(runtime, arg.reason)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "type"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "reason"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp b/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp new file mode 100644 index 0000000..7cd063f --- /dev/null +++ b/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp @@ -0,0 +1,98 @@ +/// +/// ScreenRecordingFile.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `AudioRecordingFile` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } + +#include +#include "AudioRecordingFile.hpp" +#include + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (ScreenRecordingFile). + */ + struct ScreenRecordingFile { + public: + std::string path SWIFT_PRIVATE; + std::string name SWIFT_PRIVATE; + double size SWIFT_PRIVATE; + double duration SWIFT_PRIVATE; + bool enabledMicrophone SWIFT_PRIVATE; + std::optional audioFile SWIFT_PRIVATE; + + public: + ScreenRecordingFile() = default; + explicit ScreenRecordingFile(std::string path, std::string name, double size, double duration, bool enabledMicrophone, std::optional audioFile): path(path), name(name), size(size), duration(duration), enabledMicrophone(enabledMicrophone), audioFile(audioFile) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ ScreenRecordingFile <> JS ScreenRecordingFile (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::ScreenRecordingFile fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::ScreenRecordingFile( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "path")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "name")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "size")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "duration")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "enabledMicrophone")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "audioFile")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::ScreenRecordingFile& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "path", JSIConverter::toJSI(runtime, arg.path)); + obj.setProperty(runtime, "name", JSIConverter::toJSI(runtime, arg.name)); + obj.setProperty(runtime, "size", JSIConverter::toJSI(runtime, arg.size)); + obj.setProperty(runtime, "duration", JSIConverter::toJSI(runtime, arg.duration)); + obj.setProperty(runtime, "enabledMicrophone", JSIConverter::toJSI(runtime, arg.enabledMicrophone)); + obj.setProperty(runtime, "audioFile", JSIConverter>::toJSI(runtime, arg.audioFile)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "path"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "name"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "size"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "duration"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "enabledMicrophone"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "audioFile"))) return false; + return true; + } + }; + +} // namespace margelo::nitro From 83f8d32c26c54d84006a8fcb9333f5799a2fb560 Mon Sep 17 00:00:00 2001 From: Samuel Date: Fri, 5 Dec 2025 17:52:21 -0800 Subject: [PATCH 05/32] chore: bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index feb3946..3d8ec26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.7.0", + "version": "0.7.1", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", From 0978500d70c203060d5a9d60b43fb6109f7bad25 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 8 Dec 2025 16:37:08 -0800 Subject: [PATCH 06/32] feat: decouple app audio --- .../NitroScreenRecorder.kt | 3 +- .../BroadcastWriter.swift | 169 +++++++++++++++--- .../SampleHandler.swift | 96 ++++++---- ios/NitroScreenRecorder.swift | 127 +++++++++---- .../BroadcastWriter.swift | 169 +++++++++++++++--- .../SampleHandler.swift | 96 ++++++---- .../BroadcastWriter.swift | 169 +++++++++++++++--- .../SampleHandler.swift | 96 ++++++---- .../BroadcastWriter.swift | 169 +++++++++++++++--- .../SampleHandler.swift | 96 ++++++---- lib/typescript/types.d.ts | 22 ++- lib/typescript/types.d.ts.map | 2 +- .../android/c++/JScreenRecordingFile.hpp | 10 +- .../ScreenRecordingFile.kt | 9 +- .../ios/swift/ScreenRecordingFile.swift | 25 ++- .../shared/c++/ScreenRecordingFile.hpp | 8 +- src/types.ts | 22 ++- 17 files changed, 1000 insertions(+), 288 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt index 2ea13ae..c469554 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt @@ -387,7 +387,8 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { size = file.length().toDouble(), duration = RecorderUtils.getVideoDuration(file), enabledMicrophone = true, // Assume true for global recordings - audioFile = audioFile + audioFile = audioFile, + appAudioFile = null // App audio capture not supported on Android ) } else { null diff --git a/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift index 44d82b1..75e45a7 100644 --- a/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift +++ b/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -1,6 +1,6 @@ // MARK: Broadcast Writer -// Copied from the repo: +// Copied from the repo: // https://github.com/romiroma/BroadcastWriter import AVFoundation @@ -44,12 +44,17 @@ public final class BroadcastWriter { private var audioAssetWriterSessionStarted: Bool = false private let assetWriterQueue: DispatchQueue private let assetWriter: AVAssetWriter - - // Separate audio writer + + // Separate mic audio writer private var separateAudioWriter: AVAssetWriter? private let separateAudioFile: Bool private let audioOutputURL: URL? + // Separate app audio writer + private var appAudioWriter: AVAssetWriter? + private let appAudioOutputURL: URL? + private var appAudioAssetWriterSessionStarted: Bool = false + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in let videoWidth = screenSize.width * screenScale let videoHeight = screenSize.height * screenScale @@ -124,7 +129,7 @@ public final class BroadcastWriter { input.expectsMediaDataInRealTime = true return input }() - + // Separate audio file input (for microphone audio only) private lazy var separateAudioInput: AVAssetWriterInput = { var audioSettings: [String: Any] = [ @@ -141,9 +146,26 @@ public final class BroadcastWriter { return input }() + // Separate app audio file input + private lazy var appAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Main video file inputs: video + mic audio only (no app audio) + // App audio is written to a separate file for Mux compatibility private lazy var inputs: [AVAssetWriterInput] = [ videoInput, - audioInput, microphoneInput, ] @@ -153,6 +175,7 @@ public final class BroadcastWriter { public init( outputURL url: URL, audioOutputURL: URL? = nil, + appAudioOutputURL: URL? = nil, assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), screenSize: CGSize, screenScale: CGFloat, @@ -166,12 +189,19 @@ public final class BroadcastWriter { self.screenScale = screenScale self.separateAudioFile = separateAudioFile self.audioOutputURL = audioOutputURL - - // Initialize separate audio writer if needed + self.appAudioOutputURL = appAudioOutputURL + + // Initialize separate mic audio writer if needed if separateAudioFile, let audioURL = audioOutputURL { separateAudioWriter = try .init(url: audioURL, fileType: .m4a) separateAudioWriter?.shouldOptimizeForNetworkUse = true } + + // Initialize separate app audio writer if needed + if separateAudioFile, let appAudioURL = appAudioOutputURL { + appAudioWriter = try .init(url: appAudioURL, fileType: .m4a) + appAudioWriter?.shouldOptimizeForNetworkUse = true + } } public func start() throws { @@ -194,8 +224,8 @@ public final class BroadcastWriter { try assetWriter.error.map { throw $0 } - - // Start separate audio writer if enabled + + // Start separate mic audio writer if enabled if separateAudioFile, let audioWriter = separateAudioWriter { let audioStatus = audioWriter.status guard audioStatus == .unknown else { @@ -209,6 +239,21 @@ public final class BroadcastWriter { audioWriter.startWriting() try audioWriter.error.map { throw $0 } } + + // Start separate app audio writer if enabled + if separateAudioFile, let appWriter = appAudioWriter { + let appAudioStatus = appWriter.status + guard appAudioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(appAudioStatus) + } + try appWriter.error.map { throw $0 } + if appWriter.canAdd(appAudioInput) { + appWriter.add(appAudioInput) + } + try appWriter.error.map { throw $0 } + appWriter.startWriting() + try appWriter.error.map { throw $0 } + } } } @@ -250,10 +295,17 @@ public final class BroadcastWriter { case .video: capture = captureVideoOutput case .audioApp: - capture = captureAudioOutput + // App audio goes to separate file only (not embedded in main video) + if separateAudioFile { + assetWriterQueue.sync { + _ = captureAppAudioOutput(sampleBuffer) + } + } + // Return early - don't write app audio to main video file + return true case .audioMic: capture = captureMicrophoneOutput - // Also write to separate audio file if enabled + // Also write to separate mic audio file if enabled if separateAudioFile { assetWriterQueue.sync { _ = captureSeparateAudioOutput(sampleBuffer) @@ -277,17 +329,18 @@ public final class BroadcastWriter { // TODO: Resume } - /// Result containing both video and optional audio URLs + /// Result containing video and optional separate audio URLs public struct FinishResult { public let videoURL: URL - public let audioURL: URL? + public let audioURL: URL? // Mic audio file + public let appAudioURL: URL? // App/system audio file } - + public func finish() throws -> URL { let result = try finishWithAudio() return result.videoURL } - + public func finishWithAudio() throws -> FinishResult { return try assetWriterQueue.sync { let group: DispatchGroup = .init() @@ -328,18 +381,18 @@ public final class BroadcastWriter { } group.wait() try error.map { throw $0 } - - // Finish separate audio writer if enabled + + // Finish separate mic audio writer if enabled var audioURL: URL? = nil if separateAudioFile, let audioWriter = separateAudioWriter { if separateAudioInput.isReadyForMoreMediaData { separateAudioInput.markAsFinished() } - + if audioWriter.status == .writing { let audioGroup = DispatchGroup() audioGroup.enter() - + var audioError: Swift.Error? audioWriter.finishWriting { defer { audioGroup.leave() } @@ -352,14 +405,45 @@ public final class BroadcastWriter { } } audioGroup.wait() - + if audioError == nil { audioURL = audioWriter.outputURL } } } - - return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + + // Finish separate app audio writer if enabled + var appAudioURL: URL? = nil + if separateAudioFile, let appWriter = appAudioWriter { + if appAudioInput.isReadyForMoreMediaData { + appAudioInput.markAsFinished() + } + + if appWriter.status == .writing { + let appAudioGroup = DispatchGroup() + appAudioGroup.enter() + + var appAudioError: Swift.Error? + appWriter.finishWriting { + defer { appAudioGroup.leave() } + if let e = appWriter.error { + appAudioError = e + return + } + if appWriter.status != .completed { + appAudioError = Error.wrongAssetWriterStatus(appWriter.status) + } + } + appAudioGroup.wait() + + if appAudioError == nil { + appAudioURL = appWriter.outputURL + } + } + } + + return FinishResult( + videoURL: assetWriter.outputURL, audioURL: audioURL, appAudioURL: appAudioURL) } } } @@ -375,7 +459,7 @@ extension BroadcastWriter { assetWriter.startSession(atSourceTime: sourceTime) assetWriterSessionStarted = true } - + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { return @@ -386,6 +470,16 @@ extension BroadcastWriter { audioAssetWriterSessionStarted = true } + fileprivate func startAppAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !appAudioAssetWriterSessionStarted, let appWriter = appAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + appWriter.startSession(atSourceTime: sourceTime) + appAudioAssetWriterSessionStarted = true + } + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard videoInput.isReadyForMoreMediaData else { debugPrint("videoInput is not ready") @@ -410,25 +504,46 @@ extension BroadcastWriter { } return microphoneInput.append(sampleBuffer) } - + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard separateAudioFile, let audioWriter = separateAudioWriter else { return false } - + // Check if audio writer is still writing guard audioWriter.status == .writing else { debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") return false } - + // Start session if needed startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) - + guard separateAudioInput.isReadyForMoreMediaData else { debugPrint("separateAudioInput is not ready") return false } return separateAudioInput.append(sampleBuffer) } + + fileprivate func captureAppAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let appWriter = appAudioWriter else { + return false + } + + // Check if app audio writer is still writing + guard appWriter.status == .writing else { + debugPrint("appAudioWriter is not writing, status: \(appWriter.status.description)") + return false + } + + // Start session if needed + startAppAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard appAudioInput.isReadyForMoreMediaData else { + debugPrint("appAudioInput is not ready") + return false + } + return appAudioInput.append(sampleBuffer) + } } diff --git a/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift index f8feae7..bbea949 100644 --- a/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift +++ b/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -1,7 +1,7 @@ import AVFoundation +import Darwin import ReplayKit import UserNotifications -import Darwin @_silgen_name("finishBroadcastGracefully") func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) @@ -16,14 +16,16 @@ final class SampleHandler: RPBroadcastSampleHandler { // MARK: โ€“ Properties private func appGroupIDFromPlist() -> String? { - guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + guard + let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") + as? String, !value.isEmpty else { return nil } return value } - + // Store both the CFString and CFNotificationName versions private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString private static let stopNotificationName = CFNotificationName(stopNotificationString) @@ -35,7 +37,8 @@ final class SampleHandler: RPBroadcastSampleHandler { private var writer: BroadcastWriter? private let fileManager: FileManager = .default private let nodeURL: URL - private let audioNodeURL: URL + private let audioNodeURL: URL // Mic audio + private let appAudioNodeURL: URL // App/system audio private var sawMicBuffers = false private var separateAudioFile: Bool = false @@ -45,16 +48,21 @@ final class SampleHandler: RPBroadcastSampleHandler { nodeURL = fileManager.temporaryDirectory .appendingPathComponent(uuid) .appendingPathExtension(for: .mpeg4Movie) - + audioNodeURL = fileManager.temporaryDirectory - .appendingPathComponent("\(uuid)_audio") + .appendingPathComponent("\(uuid)_mic_audio") + .appendingPathExtension("m4a") + + appAudioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_app_audio") .appendingPathExtension("m4a") fileManager.removeFileIfExists(url: nodeURL) fileManager.removeFileIfExists(url: audioNodeURL) + fileManager.removeFileIfExists(url: appAudioNodeURL) super.init() } - + deinit { CFNotificationCenterRemoveObserver( CFNotificationCenterGetDarwinNotifyCenter(), @@ -63,7 +71,7 @@ final class SampleHandler: RPBroadcastSampleHandler { nil ) } - + private func startListeningForStopSignal() { let center = CFNotificationCenterGetDarwinNotifyCenter() @@ -95,7 +103,7 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { finishBroadcastWithError( NSError( - domain: "SampleHandler", + domain: "SampleHandler", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] ) @@ -117,6 +125,7 @@ final class SampleHandler: RPBroadcastSampleHandler { writer = try .init( outputURL: nodeURL, audioOutputURL: separateAudioFile ? audioNodeURL : nil, + appAudioOutputURL: separateAudioFile ? appAudioNodeURL : nil, screenSize: screen.bounds.size, screenScale: screen.scale, separateAudioFile: separateAudioFile @@ -128,15 +137,20 @@ final class SampleHandler: RPBroadcastSampleHandler { } private func cleanupOldRecordings(in groupID: String) { - guard let docs = fileManager.containerURL( - forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } do { let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) - for url in items where url.pathExtension.lowercased() == "mp4" { - try? fileManager.removeItem(at: url) + for url in items { + let ext = url.pathExtension.lowercased() + // Clean up video and audio files from previous recordings + if ext == "mp4" || ext == "m4a" { + try? fileManager.removeItem(at: url) + } } } catch { // Non-critical error, continue with broadcast @@ -149,8 +163,8 @@ final class SampleHandler: RPBroadcastSampleHandler { ) { guard let writer else { return } - if sampleBufferType == .audioMic { - sawMicBuffers = true + if sampleBufferType == .audioMic { + sawMicBuffers = true } do { @@ -160,18 +174,18 @@ final class SampleHandler: RPBroadcastSampleHandler { } } - override func broadcastPaused() { - writer?.pause() + override func broadcastPaused() { + writer?.pause() } - - override func broadcastResumed() { - writer?.resume() + + override func broadcastResumed() { + writer?.resume() } private func stopBroadcastGracefully() { finishBroadcastGracefully(self) } - + override func broadcastFinished() { guard let writer else { return } @@ -188,9 +202,11 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { return } // Get container directory - guard let containerURL = fileManager - .containerURL(forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let containerURL = + fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } // Create directory if needed @@ -208,25 +224,43 @@ final class SampleHandler: RPBroadcastSampleHandler { // File move failed, but we can't error out at this point return } - - // Move audio file to shared container if it exists + + // Move mic audio file to shared container if it exists if let audioURL = result.audioURL { let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) do { try fileManager.moveItem(at: audioURL, to: audioDestination) - // Store audio file name for retrieval + // Store mic audio file name for retrieval UserDefaults(suiteName: groupID)? .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") } catch { // Audio file move failed, but video is already saved - debugPrint("Failed to move audio file: \(error)") + debugPrint("Failed to move mic audio file: \(error)") } } else { - // Clear audio file name if no separate audio + // Clear mic audio file name if no separate audio UserDefaults(suiteName: groupID)? .removeObject(forKey: "LastBroadcastAudioFileName") } + // Move app audio file to shared container if it exists + if let appAudioURL = result.appAudioURL { + let appAudioDestination = containerURL.appendingPathComponent(appAudioURL.lastPathComponent) + do { + try fileManager.moveItem(at: appAudioURL, to: appAudioDestination) + // Store app audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(appAudioDestination.lastPathComponent, forKey: "LastBroadcastAppAudioFileName") + } catch { + // App audio file move failed, but video is already saved + debugPrint("Failed to move app audio file: \(error)") + } + } else { + // Clear app audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAppAudioFileName") + } + // Persist microphone state and audio file state UserDefaults(suiteName: groupID)? .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") @@ -241,4 +275,4 @@ extension FileManager { guard fileExists(atPath: url.path) else { return } try? removeItem(at: url) } -} \ No newline at end of file +} diff --git a/ios/NitroScreenRecorder.swift b/ios/NitroScreenRecorder.swift index 6426f6b..c1c3026 100644 --- a/ios/NitroScreenRecorder.swift +++ b/ios/NitroScreenRecorder.swift @@ -33,7 +33,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { private var recordingEventListeners: [ScreenRecordingListenerType] = [] public var broadcastPickerEventListeners: [Listener] = [] private var nextListenerId: Double = 0 - + // Separate audio file recording private var separateAudioFileEnabled: Bool = false private var audioRecorder: AVAudioRecorder? @@ -171,17 +171,17 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } else { type = .global isGlobalRecordingActive = false - globalRecordingInitiatedByThisPackage = false // Reset when global recording ends + globalRecordingInitiatedByThisPackage = false // Reset when global recording ends } } - + let event = ScreenRecordingEvent(type: type, reason: reason) - + // Filter listeners based on their ignore preference recordingEventListeners.forEach { listener in let isExternalGlobalRecording = type == .global && !globalRecordingInitiatedByThisPackage let shouldIgnore = listener.ignoreRecordingsInitiatedElsewhere && isExternalGlobalRecording - + if !shouldIgnore { listener.callback(event) } @@ -293,12 +293,12 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { recorder.cameraPosition = device } inAppRecordingActive = true - + // Start separate audio recording if enabled and mic is enabled if separateAudioFile && enableMic { startSeparateAudioRecording() } - + recorder.startRecording { [weak self] error in guard let self = self else { return } if let error = error { @@ -307,7 +307,9 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { self.stopSeparateAudioRecording() return } - print("โœ… In-app recording started (mic:\(enableMic) camera:\(enableCamera) separateAudio:\(separateAudioFile))") + print( + "โœ… In-app recording started (mic:\(enableMic) camera:\(enableCamera) separateAudio:\(separateAudioFile))" + ) if enableCamera { DispatchQueue.main.async { @@ -316,30 +318,31 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } } } - + private func startSeparateAudioRecording() { let fileName = "audio_capture_\(UUID().uuidString).m4a" audioFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) - + guard let audioURL = audioFileURL else { return } - + // Remove any existing file try? FileManager.default.removeItem(at: audioURL) - + let audioSettings: [String: Any] = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100.0, AVNumberOfChannelsKey: 1, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, - AVEncoderBitRateKey: 128000 + AVEncoderBitRateKey: 128000, ] - + do { // Configure audio session let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) + try audioSession.setCategory( + .playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) try audioSession.setActive(true) - + audioRecorder = try AVAudioRecorder(url: audioURL, settings: audioSettings) audioRecorder?.record() print("โœ… Separate audio recording started: \(audioURL.path)") @@ -349,28 +352,28 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { audioFileURL = nil } } - + private func stopSeparateAudioRecording() -> AudioRecordingFile? { guard let recorder = audioRecorder, let audioURL = audioFileURL else { return nil } - + recorder.stop() audioRecorder = nil - + // Get audio file info do { let attrs = try FileManager.default.attributesOfItem(atPath: audioURL.path) let asset = AVURLAsset(url: audioURL) let duration = CMTimeGetSeconds(asset.duration) - + let audioFile = AudioRecordingFile( path: audioURL.absoluteString, name: audioURL.lastPathComponent, size: attrs[.size] as? Double ?? 0, duration: duration ) - + print("โœ… Separate audio recording stopped: \(audioURL.path)") return audioFile } catch { @@ -384,7 +387,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { return await withCheckedContinuation { continuation in // Stop separate audio recording first if enabled let audioFile = self.separateAudioFileEnabled ? self.stopSeparateAudioRecording() : nil - + // build a unique temp URL let fileName = "screen_capture_\(UUID().uuidString).mp4" let outputURL = FileManager.default.temporaryDirectory @@ -447,7 +450,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { _ = self.stopSeparateAudioRecording() self.separateAudioFileEnabled = false } - + // If a recording session is in progress, stop it and write out to a temp URL if self.recorder.isRecording { let tempURL = FileManager.default.temporaryDirectory @@ -542,7 +545,9 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } } - func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (RecordingError) -> Void) + func startGlobalRecording( + enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (RecordingError) -> Void + ) throws { guard !isGlobalRecordingActive else { @@ -584,7 +589,7 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { // Present the broadcast picker presentGlobalBroadcastModal(enableMicrophone: enableMic) - + // This is sort of a hack to try and track if the user opened the broadcast modal first // may not be that reliable, because technically they can open this modal and close it without starting a broadcast globalRecordingInitiatedByThisPackage = true @@ -706,42 +711,85 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { let micEnabled = UserDefaults(suiteName: appGroupId)? .bool(forKey: "LastBroadcastMicrophoneWasEnabled") ?? false - - // Check for and retrieve separate audio file + + // Check for and retrieve separate mic audio file var audioFile: AudioRecordingFile? = nil - let hadSeparateAudio = UserDefaults(suiteName: appGroupId)?.bool(forKey: "LastBroadcastHadSeparateAudio") ?? false - + let hadSeparateAudio = + UserDefaults(suiteName: appGroupId)?.bool(forKey: "LastBroadcastHadSeparateAudio") ?? false + if hadSeparateAudio, - let audioFileName = UserDefaults(suiteName: appGroupId)?.string(forKey: "LastBroadcastAudioFileName") { + let audioFileName = UserDefaults(suiteName: appGroupId)?.string( + forKey: "LastBroadcastAudioFileName") + { let audioSourceURL = docsURL.appendingPathComponent(audioFileName) - + if fm.fileExists(atPath: audioSourceURL.path) { - // Copy audio file to caches + // Copy mic audio file to caches var audioDestinationURL = recordingsDir.appendingPathComponent(audioFileName) if fm.fileExists(atPath: audioDestinationURL.path) { let ts = Int(Date().timeIntervalSince1970) let base = audioSourceURL.deletingPathExtension().lastPathComponent audioDestinationURL = recordingsDir.appendingPathComponent("\(base)-\(ts).m4a") } - + do { try fm.copyItem(at: audioSourceURL, to: audioDestinationURL) - + let audioAttrs = try fm.attributesOfItem(atPath: audioDestinationURL.path) let audioSize = (audioAttrs[.size] as? NSNumber)?.doubleValue ?? 0.0 - + let audioAsset = AVURLAsset(url: audioDestinationURL) let audioDuration = CMTimeGetSeconds(audioAsset.duration) - + audioFile = AudioRecordingFile( path: audioDestinationURL.absoluteString, name: audioDestinationURL.lastPathComponent, size: audioSize, duration: audioDuration ) - print("โœ… Retrieved separate audio file: \(audioDestinationURL.path)") + print("โœ… Retrieved separate mic audio file: \(audioDestinationURL.path)") + } catch { + print("โš ๏ธ Failed to copy mic audio file: \(error.localizedDescription)") + } + } + } + + // Check for and retrieve separate app audio file + var appAudioFile: AudioRecordingFile? = nil + + if hadSeparateAudio, + let appAudioFileName = UserDefaults(suiteName: appGroupId)?.string( + forKey: "LastBroadcastAppAudioFileName") + { + let appAudioSourceURL = docsURL.appendingPathComponent(appAudioFileName) + + if fm.fileExists(atPath: appAudioSourceURL.path) { + // Copy app audio file to caches + var appAudioDestinationURL = recordingsDir.appendingPathComponent(appAudioFileName) + if fm.fileExists(atPath: appAudioDestinationURL.path) { + let ts = Int(Date().timeIntervalSince1970) + let base = appAudioSourceURL.deletingPathExtension().lastPathComponent + appAudioDestinationURL = recordingsDir.appendingPathComponent("\(base)-\(ts).m4a") + } + + do { + try fm.copyItem(at: appAudioSourceURL, to: appAudioDestinationURL) + + let appAudioAttrs = try fm.attributesOfItem(atPath: appAudioDestinationURL.path) + let appAudioSize = (appAudioAttrs[.size] as? NSNumber)?.doubleValue ?? 0.0 + + let appAudioAsset = AVURLAsset(url: appAudioDestinationURL) + let appAudioDuration = CMTimeGetSeconds(appAudioAsset.duration) + + appAudioFile = AudioRecordingFile( + path: appAudioDestinationURL.absoluteString, + name: appAudioDestinationURL.lastPathComponent, + size: appAudioSize, + duration: appAudioDuration + ) + print("โœ… Retrieved separate app audio file: \(appAudioDestinationURL.path)") } catch { - print("โš ๏ธ Failed to copy audio file: \(error.localizedDescription)") + print("โš ๏ธ Failed to copy app audio file: \(error.localizedDescription)") } } } @@ -752,7 +800,8 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { size: size, duration: duration, enabledMicrophone: micEnabled, - audioFile: audioFile + audioFile: audioFile, + appAudioFile: appAudioFile ) } diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift index 44d82b1..75e45a7 100644 --- a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -1,6 +1,6 @@ // MARK: Broadcast Writer -// Copied from the repo: +// Copied from the repo: // https://github.com/romiroma/BroadcastWriter import AVFoundation @@ -44,12 +44,17 @@ public final class BroadcastWriter { private var audioAssetWriterSessionStarted: Bool = false private let assetWriterQueue: DispatchQueue private let assetWriter: AVAssetWriter - - // Separate audio writer + + // Separate mic audio writer private var separateAudioWriter: AVAssetWriter? private let separateAudioFile: Bool private let audioOutputURL: URL? + // Separate app audio writer + private var appAudioWriter: AVAssetWriter? + private let appAudioOutputURL: URL? + private var appAudioAssetWriterSessionStarted: Bool = false + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in let videoWidth = screenSize.width * screenScale let videoHeight = screenSize.height * screenScale @@ -124,7 +129,7 @@ public final class BroadcastWriter { input.expectsMediaDataInRealTime = true return input }() - + // Separate audio file input (for microphone audio only) private lazy var separateAudioInput: AVAssetWriterInput = { var audioSettings: [String: Any] = [ @@ -141,9 +146,26 @@ public final class BroadcastWriter { return input }() + // Separate app audio file input + private lazy var appAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Main video file inputs: video + mic audio only (no app audio) + // App audio is written to a separate file for Mux compatibility private lazy var inputs: [AVAssetWriterInput] = [ videoInput, - audioInput, microphoneInput, ] @@ -153,6 +175,7 @@ public final class BroadcastWriter { public init( outputURL url: URL, audioOutputURL: URL? = nil, + appAudioOutputURL: URL? = nil, assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), screenSize: CGSize, screenScale: CGFloat, @@ -166,12 +189,19 @@ public final class BroadcastWriter { self.screenScale = screenScale self.separateAudioFile = separateAudioFile self.audioOutputURL = audioOutputURL - - // Initialize separate audio writer if needed + self.appAudioOutputURL = appAudioOutputURL + + // Initialize separate mic audio writer if needed if separateAudioFile, let audioURL = audioOutputURL { separateAudioWriter = try .init(url: audioURL, fileType: .m4a) separateAudioWriter?.shouldOptimizeForNetworkUse = true } + + // Initialize separate app audio writer if needed + if separateAudioFile, let appAudioURL = appAudioOutputURL { + appAudioWriter = try .init(url: appAudioURL, fileType: .m4a) + appAudioWriter?.shouldOptimizeForNetworkUse = true + } } public func start() throws { @@ -194,8 +224,8 @@ public final class BroadcastWriter { try assetWriter.error.map { throw $0 } - - // Start separate audio writer if enabled + + // Start separate mic audio writer if enabled if separateAudioFile, let audioWriter = separateAudioWriter { let audioStatus = audioWriter.status guard audioStatus == .unknown else { @@ -209,6 +239,21 @@ public final class BroadcastWriter { audioWriter.startWriting() try audioWriter.error.map { throw $0 } } + + // Start separate app audio writer if enabled + if separateAudioFile, let appWriter = appAudioWriter { + let appAudioStatus = appWriter.status + guard appAudioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(appAudioStatus) + } + try appWriter.error.map { throw $0 } + if appWriter.canAdd(appAudioInput) { + appWriter.add(appAudioInput) + } + try appWriter.error.map { throw $0 } + appWriter.startWriting() + try appWriter.error.map { throw $0 } + } } } @@ -250,10 +295,17 @@ public final class BroadcastWriter { case .video: capture = captureVideoOutput case .audioApp: - capture = captureAudioOutput + // App audio goes to separate file only (not embedded in main video) + if separateAudioFile { + assetWriterQueue.sync { + _ = captureAppAudioOutput(sampleBuffer) + } + } + // Return early - don't write app audio to main video file + return true case .audioMic: capture = captureMicrophoneOutput - // Also write to separate audio file if enabled + // Also write to separate mic audio file if enabled if separateAudioFile { assetWriterQueue.sync { _ = captureSeparateAudioOutput(sampleBuffer) @@ -277,17 +329,18 @@ public final class BroadcastWriter { // TODO: Resume } - /// Result containing both video and optional audio URLs + /// Result containing video and optional separate audio URLs public struct FinishResult { public let videoURL: URL - public let audioURL: URL? + public let audioURL: URL? // Mic audio file + public let appAudioURL: URL? // App/system audio file } - + public func finish() throws -> URL { let result = try finishWithAudio() return result.videoURL } - + public func finishWithAudio() throws -> FinishResult { return try assetWriterQueue.sync { let group: DispatchGroup = .init() @@ -328,18 +381,18 @@ public final class BroadcastWriter { } group.wait() try error.map { throw $0 } - - // Finish separate audio writer if enabled + + // Finish separate mic audio writer if enabled var audioURL: URL? = nil if separateAudioFile, let audioWriter = separateAudioWriter { if separateAudioInput.isReadyForMoreMediaData { separateAudioInput.markAsFinished() } - + if audioWriter.status == .writing { let audioGroup = DispatchGroup() audioGroup.enter() - + var audioError: Swift.Error? audioWriter.finishWriting { defer { audioGroup.leave() } @@ -352,14 +405,45 @@ public final class BroadcastWriter { } } audioGroup.wait() - + if audioError == nil { audioURL = audioWriter.outputURL } } } - - return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + + // Finish separate app audio writer if enabled + var appAudioURL: URL? = nil + if separateAudioFile, let appWriter = appAudioWriter { + if appAudioInput.isReadyForMoreMediaData { + appAudioInput.markAsFinished() + } + + if appWriter.status == .writing { + let appAudioGroup = DispatchGroup() + appAudioGroup.enter() + + var appAudioError: Swift.Error? + appWriter.finishWriting { + defer { appAudioGroup.leave() } + if let e = appWriter.error { + appAudioError = e + return + } + if appWriter.status != .completed { + appAudioError = Error.wrongAssetWriterStatus(appWriter.status) + } + } + appAudioGroup.wait() + + if appAudioError == nil { + appAudioURL = appWriter.outputURL + } + } + } + + return FinishResult( + videoURL: assetWriter.outputURL, audioURL: audioURL, appAudioURL: appAudioURL) } } } @@ -375,7 +459,7 @@ extension BroadcastWriter { assetWriter.startSession(atSourceTime: sourceTime) assetWriterSessionStarted = true } - + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { return @@ -386,6 +470,16 @@ extension BroadcastWriter { audioAssetWriterSessionStarted = true } + fileprivate func startAppAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !appAudioAssetWriterSessionStarted, let appWriter = appAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + appWriter.startSession(atSourceTime: sourceTime) + appAudioAssetWriterSessionStarted = true + } + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard videoInput.isReadyForMoreMediaData else { debugPrint("videoInput is not ready") @@ -410,25 +504,46 @@ extension BroadcastWriter { } return microphoneInput.append(sampleBuffer) } - + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard separateAudioFile, let audioWriter = separateAudioWriter else { return false } - + // Check if audio writer is still writing guard audioWriter.status == .writing else { debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") return false } - + // Start session if needed startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) - + guard separateAudioInput.isReadyForMoreMediaData else { debugPrint("separateAudioInput is not ready") return false } return separateAudioInput.append(sampleBuffer) } + + fileprivate func captureAppAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let appWriter = appAudioWriter else { + return false + } + + // Check if app audio writer is still writing + guard appWriter.status == .writing else { + debugPrint("appAudioWriter is not writing, status: \(appWriter.status.description)") + return false + } + + // Start session if needed + startAppAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard appAudioInput.isReadyForMoreMediaData else { + debugPrint("appAudioInput is not ready") + return false + } + return appAudioInput.append(sampleBuffer) + } } diff --git a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift index f8feae7..bbea949 100644 --- a/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift +++ b/lib/commonjs/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -1,7 +1,7 @@ import AVFoundation +import Darwin import ReplayKit import UserNotifications -import Darwin @_silgen_name("finishBroadcastGracefully") func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) @@ -16,14 +16,16 @@ final class SampleHandler: RPBroadcastSampleHandler { // MARK: โ€“ Properties private func appGroupIDFromPlist() -> String? { - guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + guard + let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") + as? String, !value.isEmpty else { return nil } return value } - + // Store both the CFString and CFNotificationName versions private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString private static let stopNotificationName = CFNotificationName(stopNotificationString) @@ -35,7 +37,8 @@ final class SampleHandler: RPBroadcastSampleHandler { private var writer: BroadcastWriter? private let fileManager: FileManager = .default private let nodeURL: URL - private let audioNodeURL: URL + private let audioNodeURL: URL // Mic audio + private let appAudioNodeURL: URL // App/system audio private var sawMicBuffers = false private var separateAudioFile: Bool = false @@ -45,16 +48,21 @@ final class SampleHandler: RPBroadcastSampleHandler { nodeURL = fileManager.temporaryDirectory .appendingPathComponent(uuid) .appendingPathExtension(for: .mpeg4Movie) - + audioNodeURL = fileManager.temporaryDirectory - .appendingPathComponent("\(uuid)_audio") + .appendingPathComponent("\(uuid)_mic_audio") + .appendingPathExtension("m4a") + + appAudioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_app_audio") .appendingPathExtension("m4a") fileManager.removeFileIfExists(url: nodeURL) fileManager.removeFileIfExists(url: audioNodeURL) + fileManager.removeFileIfExists(url: appAudioNodeURL) super.init() } - + deinit { CFNotificationCenterRemoveObserver( CFNotificationCenterGetDarwinNotifyCenter(), @@ -63,7 +71,7 @@ final class SampleHandler: RPBroadcastSampleHandler { nil ) } - + private func startListeningForStopSignal() { let center = CFNotificationCenterGetDarwinNotifyCenter() @@ -95,7 +103,7 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { finishBroadcastWithError( NSError( - domain: "SampleHandler", + domain: "SampleHandler", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] ) @@ -117,6 +125,7 @@ final class SampleHandler: RPBroadcastSampleHandler { writer = try .init( outputURL: nodeURL, audioOutputURL: separateAudioFile ? audioNodeURL : nil, + appAudioOutputURL: separateAudioFile ? appAudioNodeURL : nil, screenSize: screen.bounds.size, screenScale: screen.scale, separateAudioFile: separateAudioFile @@ -128,15 +137,20 @@ final class SampleHandler: RPBroadcastSampleHandler { } private func cleanupOldRecordings(in groupID: String) { - guard let docs = fileManager.containerURL( - forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } do { let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) - for url in items where url.pathExtension.lowercased() == "mp4" { - try? fileManager.removeItem(at: url) + for url in items { + let ext = url.pathExtension.lowercased() + // Clean up video and audio files from previous recordings + if ext == "mp4" || ext == "m4a" { + try? fileManager.removeItem(at: url) + } } } catch { // Non-critical error, continue with broadcast @@ -149,8 +163,8 @@ final class SampleHandler: RPBroadcastSampleHandler { ) { guard let writer else { return } - if sampleBufferType == .audioMic { - sawMicBuffers = true + if sampleBufferType == .audioMic { + sawMicBuffers = true } do { @@ -160,18 +174,18 @@ final class SampleHandler: RPBroadcastSampleHandler { } } - override func broadcastPaused() { - writer?.pause() + override func broadcastPaused() { + writer?.pause() } - - override func broadcastResumed() { - writer?.resume() + + override func broadcastResumed() { + writer?.resume() } private func stopBroadcastGracefully() { finishBroadcastGracefully(self) } - + override func broadcastFinished() { guard let writer else { return } @@ -188,9 +202,11 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { return } // Get container directory - guard let containerURL = fileManager - .containerURL(forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let containerURL = + fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } // Create directory if needed @@ -208,25 +224,43 @@ final class SampleHandler: RPBroadcastSampleHandler { // File move failed, but we can't error out at this point return } - - // Move audio file to shared container if it exists + + // Move mic audio file to shared container if it exists if let audioURL = result.audioURL { let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) do { try fileManager.moveItem(at: audioURL, to: audioDestination) - // Store audio file name for retrieval + // Store mic audio file name for retrieval UserDefaults(suiteName: groupID)? .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") } catch { // Audio file move failed, but video is already saved - debugPrint("Failed to move audio file: \(error)") + debugPrint("Failed to move mic audio file: \(error)") } } else { - // Clear audio file name if no separate audio + // Clear mic audio file name if no separate audio UserDefaults(suiteName: groupID)? .removeObject(forKey: "LastBroadcastAudioFileName") } + // Move app audio file to shared container if it exists + if let appAudioURL = result.appAudioURL { + let appAudioDestination = containerURL.appendingPathComponent(appAudioURL.lastPathComponent) + do { + try fileManager.moveItem(at: appAudioURL, to: appAudioDestination) + // Store app audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(appAudioDestination.lastPathComponent, forKey: "LastBroadcastAppAudioFileName") + } catch { + // App audio file move failed, but video is already saved + debugPrint("Failed to move app audio file: \(error)") + } + } else { + // Clear app audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAppAudioFileName") + } + // Persist microphone state and audio file state UserDefaults(suiteName: groupID)? .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") @@ -241,4 +275,4 @@ extension FileManager { guard fileExists(atPath: url.path) else { return } try? removeItem(at: url) } -} \ No newline at end of file +} diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift index 44d82b1..75e45a7 100644 --- a/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -1,6 +1,6 @@ // MARK: Broadcast Writer -// Copied from the repo: +// Copied from the repo: // https://github.com/romiroma/BroadcastWriter import AVFoundation @@ -44,12 +44,17 @@ public final class BroadcastWriter { private var audioAssetWriterSessionStarted: Bool = false private let assetWriterQueue: DispatchQueue private let assetWriter: AVAssetWriter - - // Separate audio writer + + // Separate mic audio writer private var separateAudioWriter: AVAssetWriter? private let separateAudioFile: Bool private let audioOutputURL: URL? + // Separate app audio writer + private var appAudioWriter: AVAssetWriter? + private let appAudioOutputURL: URL? + private var appAudioAssetWriterSessionStarted: Bool = false + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in let videoWidth = screenSize.width * screenScale let videoHeight = screenSize.height * screenScale @@ -124,7 +129,7 @@ public final class BroadcastWriter { input.expectsMediaDataInRealTime = true return input }() - + // Separate audio file input (for microphone audio only) private lazy var separateAudioInput: AVAssetWriterInput = { var audioSettings: [String: Any] = [ @@ -141,9 +146,26 @@ public final class BroadcastWriter { return input }() + // Separate app audio file input + private lazy var appAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Main video file inputs: video + mic audio only (no app audio) + // App audio is written to a separate file for Mux compatibility private lazy var inputs: [AVAssetWriterInput] = [ videoInput, - audioInput, microphoneInput, ] @@ -153,6 +175,7 @@ public final class BroadcastWriter { public init( outputURL url: URL, audioOutputURL: URL? = nil, + appAudioOutputURL: URL? = nil, assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), screenSize: CGSize, screenScale: CGFloat, @@ -166,12 +189,19 @@ public final class BroadcastWriter { self.screenScale = screenScale self.separateAudioFile = separateAudioFile self.audioOutputURL = audioOutputURL - - // Initialize separate audio writer if needed + self.appAudioOutputURL = appAudioOutputURL + + // Initialize separate mic audio writer if needed if separateAudioFile, let audioURL = audioOutputURL { separateAudioWriter = try .init(url: audioURL, fileType: .m4a) separateAudioWriter?.shouldOptimizeForNetworkUse = true } + + // Initialize separate app audio writer if needed + if separateAudioFile, let appAudioURL = appAudioOutputURL { + appAudioWriter = try .init(url: appAudioURL, fileType: .m4a) + appAudioWriter?.shouldOptimizeForNetworkUse = true + } } public func start() throws { @@ -194,8 +224,8 @@ public final class BroadcastWriter { try assetWriter.error.map { throw $0 } - - // Start separate audio writer if enabled + + // Start separate mic audio writer if enabled if separateAudioFile, let audioWriter = separateAudioWriter { let audioStatus = audioWriter.status guard audioStatus == .unknown else { @@ -209,6 +239,21 @@ public final class BroadcastWriter { audioWriter.startWriting() try audioWriter.error.map { throw $0 } } + + // Start separate app audio writer if enabled + if separateAudioFile, let appWriter = appAudioWriter { + let appAudioStatus = appWriter.status + guard appAudioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(appAudioStatus) + } + try appWriter.error.map { throw $0 } + if appWriter.canAdd(appAudioInput) { + appWriter.add(appAudioInput) + } + try appWriter.error.map { throw $0 } + appWriter.startWriting() + try appWriter.error.map { throw $0 } + } } } @@ -250,10 +295,17 @@ public final class BroadcastWriter { case .video: capture = captureVideoOutput case .audioApp: - capture = captureAudioOutput + // App audio goes to separate file only (not embedded in main video) + if separateAudioFile { + assetWriterQueue.sync { + _ = captureAppAudioOutput(sampleBuffer) + } + } + // Return early - don't write app audio to main video file + return true case .audioMic: capture = captureMicrophoneOutput - // Also write to separate audio file if enabled + // Also write to separate mic audio file if enabled if separateAudioFile { assetWriterQueue.sync { _ = captureSeparateAudioOutput(sampleBuffer) @@ -277,17 +329,18 @@ public final class BroadcastWriter { // TODO: Resume } - /// Result containing both video and optional audio URLs + /// Result containing video and optional separate audio URLs public struct FinishResult { public let videoURL: URL - public let audioURL: URL? + public let audioURL: URL? // Mic audio file + public let appAudioURL: URL? // App/system audio file } - + public func finish() throws -> URL { let result = try finishWithAudio() return result.videoURL } - + public func finishWithAudio() throws -> FinishResult { return try assetWriterQueue.sync { let group: DispatchGroup = .init() @@ -328,18 +381,18 @@ public final class BroadcastWriter { } group.wait() try error.map { throw $0 } - - // Finish separate audio writer if enabled + + // Finish separate mic audio writer if enabled var audioURL: URL? = nil if separateAudioFile, let audioWriter = separateAudioWriter { if separateAudioInput.isReadyForMoreMediaData { separateAudioInput.markAsFinished() } - + if audioWriter.status == .writing { let audioGroup = DispatchGroup() audioGroup.enter() - + var audioError: Swift.Error? audioWriter.finishWriting { defer { audioGroup.leave() } @@ -352,14 +405,45 @@ public final class BroadcastWriter { } } audioGroup.wait() - + if audioError == nil { audioURL = audioWriter.outputURL } } } - - return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + + // Finish separate app audio writer if enabled + var appAudioURL: URL? = nil + if separateAudioFile, let appWriter = appAudioWriter { + if appAudioInput.isReadyForMoreMediaData { + appAudioInput.markAsFinished() + } + + if appWriter.status == .writing { + let appAudioGroup = DispatchGroup() + appAudioGroup.enter() + + var appAudioError: Swift.Error? + appWriter.finishWriting { + defer { appAudioGroup.leave() } + if let e = appWriter.error { + appAudioError = e + return + } + if appWriter.status != .completed { + appAudioError = Error.wrongAssetWriterStatus(appWriter.status) + } + } + appAudioGroup.wait() + + if appAudioError == nil { + appAudioURL = appWriter.outputURL + } + } + } + + return FinishResult( + videoURL: assetWriter.outputURL, audioURL: audioURL, appAudioURL: appAudioURL) } } } @@ -375,7 +459,7 @@ extension BroadcastWriter { assetWriter.startSession(atSourceTime: sourceTime) assetWriterSessionStarted = true } - + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { return @@ -386,6 +470,16 @@ extension BroadcastWriter { audioAssetWriterSessionStarted = true } + fileprivate func startAppAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !appAudioAssetWriterSessionStarted, let appWriter = appAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + appWriter.startSession(atSourceTime: sourceTime) + appAudioAssetWriterSessionStarted = true + } + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard videoInput.isReadyForMoreMediaData else { debugPrint("videoInput is not ready") @@ -410,25 +504,46 @@ extension BroadcastWriter { } return microphoneInput.append(sampleBuffer) } - + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard separateAudioFile, let audioWriter = separateAudioWriter else { return false } - + // Check if audio writer is still writing guard audioWriter.status == .writing else { debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") return false } - + // Start session if needed startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) - + guard separateAudioInput.isReadyForMoreMediaData else { debugPrint("separateAudioInput is not ready") return false } return separateAudioInput.append(sampleBuffer) } + + fileprivate func captureAppAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let appWriter = appAudioWriter else { + return false + } + + // Check if app audio writer is still writing + guard appWriter.status == .writing else { + debugPrint("appAudioWriter is not writing, status: \(appWriter.status.description)") + return false + } + + // Start session if needed + startAppAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard appAudioInput.isReadyForMoreMediaData else { + debugPrint("appAudioInput is not ready") + return false + } + return appAudioInput.append(sampleBuffer) + } } diff --git a/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift index f8feae7..bbea949 100644 --- a/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift +++ b/lib/module/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -1,7 +1,7 @@ import AVFoundation +import Darwin import ReplayKit import UserNotifications -import Darwin @_silgen_name("finishBroadcastGracefully") func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) @@ -16,14 +16,16 @@ final class SampleHandler: RPBroadcastSampleHandler { // MARK: โ€“ Properties private func appGroupIDFromPlist() -> String? { - guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + guard + let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") + as? String, !value.isEmpty else { return nil } return value } - + // Store both the CFString and CFNotificationName versions private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString private static let stopNotificationName = CFNotificationName(stopNotificationString) @@ -35,7 +37,8 @@ final class SampleHandler: RPBroadcastSampleHandler { private var writer: BroadcastWriter? private let fileManager: FileManager = .default private let nodeURL: URL - private let audioNodeURL: URL + private let audioNodeURL: URL // Mic audio + private let appAudioNodeURL: URL // App/system audio private var sawMicBuffers = false private var separateAudioFile: Bool = false @@ -45,16 +48,21 @@ final class SampleHandler: RPBroadcastSampleHandler { nodeURL = fileManager.temporaryDirectory .appendingPathComponent(uuid) .appendingPathExtension(for: .mpeg4Movie) - + audioNodeURL = fileManager.temporaryDirectory - .appendingPathComponent("\(uuid)_audio") + .appendingPathComponent("\(uuid)_mic_audio") + .appendingPathExtension("m4a") + + appAudioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_app_audio") .appendingPathExtension("m4a") fileManager.removeFileIfExists(url: nodeURL) fileManager.removeFileIfExists(url: audioNodeURL) + fileManager.removeFileIfExists(url: appAudioNodeURL) super.init() } - + deinit { CFNotificationCenterRemoveObserver( CFNotificationCenterGetDarwinNotifyCenter(), @@ -63,7 +71,7 @@ final class SampleHandler: RPBroadcastSampleHandler { nil ) } - + private func startListeningForStopSignal() { let center = CFNotificationCenterGetDarwinNotifyCenter() @@ -95,7 +103,7 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { finishBroadcastWithError( NSError( - domain: "SampleHandler", + domain: "SampleHandler", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] ) @@ -117,6 +125,7 @@ final class SampleHandler: RPBroadcastSampleHandler { writer = try .init( outputURL: nodeURL, audioOutputURL: separateAudioFile ? audioNodeURL : nil, + appAudioOutputURL: separateAudioFile ? appAudioNodeURL : nil, screenSize: screen.bounds.size, screenScale: screen.scale, separateAudioFile: separateAudioFile @@ -128,15 +137,20 @@ final class SampleHandler: RPBroadcastSampleHandler { } private func cleanupOldRecordings(in groupID: String) { - guard let docs = fileManager.containerURL( - forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } do { let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) - for url in items where url.pathExtension.lowercased() == "mp4" { - try? fileManager.removeItem(at: url) + for url in items { + let ext = url.pathExtension.lowercased() + // Clean up video and audio files from previous recordings + if ext == "mp4" || ext == "m4a" { + try? fileManager.removeItem(at: url) + } } } catch { // Non-critical error, continue with broadcast @@ -149,8 +163,8 @@ final class SampleHandler: RPBroadcastSampleHandler { ) { guard let writer else { return } - if sampleBufferType == .audioMic { - sawMicBuffers = true + if sampleBufferType == .audioMic { + sawMicBuffers = true } do { @@ -160,18 +174,18 @@ final class SampleHandler: RPBroadcastSampleHandler { } } - override func broadcastPaused() { - writer?.pause() + override func broadcastPaused() { + writer?.pause() } - - override func broadcastResumed() { - writer?.resume() + + override func broadcastResumed() { + writer?.resume() } private func stopBroadcastGracefully() { finishBroadcastGracefully(self) } - + override func broadcastFinished() { guard let writer else { return } @@ -188,9 +202,11 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { return } // Get container directory - guard let containerURL = fileManager - .containerURL(forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let containerURL = + fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } // Create directory if needed @@ -208,25 +224,43 @@ final class SampleHandler: RPBroadcastSampleHandler { // File move failed, but we can't error out at this point return } - - // Move audio file to shared container if it exists + + // Move mic audio file to shared container if it exists if let audioURL = result.audioURL { let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) do { try fileManager.moveItem(at: audioURL, to: audioDestination) - // Store audio file name for retrieval + // Store mic audio file name for retrieval UserDefaults(suiteName: groupID)? .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") } catch { // Audio file move failed, but video is already saved - debugPrint("Failed to move audio file: \(error)") + debugPrint("Failed to move mic audio file: \(error)") } } else { - // Clear audio file name if no separate audio + // Clear mic audio file name if no separate audio UserDefaults(suiteName: groupID)? .removeObject(forKey: "LastBroadcastAudioFileName") } + // Move app audio file to shared container if it exists + if let appAudioURL = result.appAudioURL { + let appAudioDestination = containerURL.appendingPathComponent(appAudioURL.lastPathComponent) + do { + try fileManager.moveItem(at: appAudioURL, to: appAudioDestination) + // Store app audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(appAudioDestination.lastPathComponent, forKey: "LastBroadcastAppAudioFileName") + } catch { + // App audio file move failed, but video is already saved + debugPrint("Failed to move app audio file: \(error)") + } + } else { + // Clear app audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAppAudioFileName") + } + // Persist microphone state and audio file state UserDefaults(suiteName: groupID)? .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") @@ -241,4 +275,4 @@ extension FileManager { guard fileExists(atPath: url.path) else { return } try? removeItem(at: url) } -} \ No newline at end of file +} diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift index 44d82b1..75e45a7 100644 --- a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/BroadcastWriter.swift @@ -1,6 +1,6 @@ // MARK: Broadcast Writer -// Copied from the repo: +// Copied from the repo: // https://github.com/romiroma/BroadcastWriter import AVFoundation @@ -44,12 +44,17 @@ public final class BroadcastWriter { private var audioAssetWriterSessionStarted: Bool = false private let assetWriterQueue: DispatchQueue private let assetWriter: AVAssetWriter - - // Separate audio writer + + // Separate mic audio writer private var separateAudioWriter: AVAssetWriter? private let separateAudioFile: Bool private let audioOutputURL: URL? + // Separate app audio writer + private var appAudioWriter: AVAssetWriter? + private let appAudioOutputURL: URL? + private var appAudioAssetWriterSessionStarted: Bool = false + private lazy var videoInput: AVAssetWriterInput = { [unowned self] in let videoWidth = screenSize.width * screenScale let videoHeight = screenSize.height * screenScale @@ -124,7 +129,7 @@ public final class BroadcastWriter { input.expectsMediaDataInRealTime = true return input }() - + // Separate audio file input (for microphone audio only) private lazy var separateAudioInput: AVAssetWriterInput = { var audioSettings: [String: Any] = [ @@ -141,9 +146,26 @@ public final class BroadcastWriter { return input }() + // Separate app audio file input + private lazy var appAudioInput: AVAssetWriterInput = { + var audioSettings: [String: Any] = [ + AVFormatIDKey: kAudioFormatMPEG4AAC, + AVNumberOfChannelsKey: 1, + AVSampleRateKey: audioSampleRate, + AVEncoderBitRateKey: 128000, + ] + let input: AVAssetWriterInput = .init( + mediaType: .audio, + outputSettings: audioSettings + ) + input.expectsMediaDataInRealTime = true + return input + }() + + // Main video file inputs: video + mic audio only (no app audio) + // App audio is written to a separate file for Mux compatibility private lazy var inputs: [AVAssetWriterInput] = [ videoInput, - audioInput, microphoneInput, ] @@ -153,6 +175,7 @@ public final class BroadcastWriter { public init( outputURL url: URL, audioOutputURL: URL? = nil, + appAudioOutputURL: URL? = nil, assetWriterQueue queue: DispatchQueue = .init(label: "BroadcastSampleHandler.assetWriterQueue"), screenSize: CGSize, screenScale: CGFloat, @@ -166,12 +189,19 @@ public final class BroadcastWriter { self.screenScale = screenScale self.separateAudioFile = separateAudioFile self.audioOutputURL = audioOutputURL - - // Initialize separate audio writer if needed + self.appAudioOutputURL = appAudioOutputURL + + // Initialize separate mic audio writer if needed if separateAudioFile, let audioURL = audioOutputURL { separateAudioWriter = try .init(url: audioURL, fileType: .m4a) separateAudioWriter?.shouldOptimizeForNetworkUse = true } + + // Initialize separate app audio writer if needed + if separateAudioFile, let appAudioURL = appAudioOutputURL { + appAudioWriter = try .init(url: appAudioURL, fileType: .m4a) + appAudioWriter?.shouldOptimizeForNetworkUse = true + } } public func start() throws { @@ -194,8 +224,8 @@ public final class BroadcastWriter { try assetWriter.error.map { throw $0 } - - // Start separate audio writer if enabled + + // Start separate mic audio writer if enabled if separateAudioFile, let audioWriter = separateAudioWriter { let audioStatus = audioWriter.status guard audioStatus == .unknown else { @@ -209,6 +239,21 @@ public final class BroadcastWriter { audioWriter.startWriting() try audioWriter.error.map { throw $0 } } + + // Start separate app audio writer if enabled + if separateAudioFile, let appWriter = appAudioWriter { + let appAudioStatus = appWriter.status + guard appAudioStatus == .unknown else { + throw Error.wrongAssetWriterStatus(appAudioStatus) + } + try appWriter.error.map { throw $0 } + if appWriter.canAdd(appAudioInput) { + appWriter.add(appAudioInput) + } + try appWriter.error.map { throw $0 } + appWriter.startWriting() + try appWriter.error.map { throw $0 } + } } } @@ -250,10 +295,17 @@ public final class BroadcastWriter { case .video: capture = captureVideoOutput case .audioApp: - capture = captureAudioOutput + // App audio goes to separate file only (not embedded in main video) + if separateAudioFile { + assetWriterQueue.sync { + _ = captureAppAudioOutput(sampleBuffer) + } + } + // Return early - don't write app audio to main video file + return true case .audioMic: capture = captureMicrophoneOutput - // Also write to separate audio file if enabled + // Also write to separate mic audio file if enabled if separateAudioFile { assetWriterQueue.sync { _ = captureSeparateAudioOutput(sampleBuffer) @@ -277,17 +329,18 @@ public final class BroadcastWriter { // TODO: Resume } - /// Result containing both video and optional audio URLs + /// Result containing video and optional separate audio URLs public struct FinishResult { public let videoURL: URL - public let audioURL: URL? + public let audioURL: URL? // Mic audio file + public let appAudioURL: URL? // App/system audio file } - + public func finish() throws -> URL { let result = try finishWithAudio() return result.videoURL } - + public func finishWithAudio() throws -> FinishResult { return try assetWriterQueue.sync { let group: DispatchGroup = .init() @@ -328,18 +381,18 @@ public final class BroadcastWriter { } group.wait() try error.map { throw $0 } - - // Finish separate audio writer if enabled + + // Finish separate mic audio writer if enabled var audioURL: URL? = nil if separateAudioFile, let audioWriter = separateAudioWriter { if separateAudioInput.isReadyForMoreMediaData { separateAudioInput.markAsFinished() } - + if audioWriter.status == .writing { let audioGroup = DispatchGroup() audioGroup.enter() - + var audioError: Swift.Error? audioWriter.finishWriting { defer { audioGroup.leave() } @@ -352,14 +405,45 @@ public final class BroadcastWriter { } } audioGroup.wait() - + if audioError == nil { audioURL = audioWriter.outputURL } } } - - return FinishResult(videoURL: assetWriter.outputURL, audioURL: audioURL) + + // Finish separate app audio writer if enabled + var appAudioURL: URL? = nil + if separateAudioFile, let appWriter = appAudioWriter { + if appAudioInput.isReadyForMoreMediaData { + appAudioInput.markAsFinished() + } + + if appWriter.status == .writing { + let appAudioGroup = DispatchGroup() + appAudioGroup.enter() + + var appAudioError: Swift.Error? + appWriter.finishWriting { + defer { appAudioGroup.leave() } + if let e = appWriter.error { + appAudioError = e + return + } + if appWriter.status != .completed { + appAudioError = Error.wrongAssetWriterStatus(appWriter.status) + } + } + appAudioGroup.wait() + + if appAudioError == nil { + appAudioURL = appWriter.outputURL + } + } + } + + return FinishResult( + videoURL: assetWriter.outputURL, audioURL: audioURL, appAudioURL: appAudioURL) } } } @@ -375,7 +459,7 @@ extension BroadcastWriter { assetWriter.startSession(atSourceTime: sourceTime) assetWriterSessionStarted = true } - + fileprivate func startAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { guard !audioAssetWriterSessionStarted, let audioWriter = separateAudioWriter else { return @@ -386,6 +470,16 @@ extension BroadcastWriter { audioAssetWriterSessionStarted = true } + fileprivate func startAppAudioSessionIfNeeded(sampleBuffer: CMSampleBuffer) { + guard !appAudioAssetWriterSessionStarted, let appWriter = appAudioWriter else { + return + } + + let sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + appWriter.startSession(atSourceTime: sourceTime) + appAudioAssetWriterSessionStarted = true + } + fileprivate func captureVideoOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard videoInput.isReadyForMoreMediaData else { debugPrint("videoInput is not ready") @@ -410,25 +504,46 @@ extension BroadcastWriter { } return microphoneInput.append(sampleBuffer) } - + fileprivate func captureSeparateAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { guard separateAudioFile, let audioWriter = separateAudioWriter else { return false } - + // Check if audio writer is still writing guard audioWriter.status == .writing else { debugPrint("separateAudioWriter is not writing, status: \(audioWriter.status.description)") return false } - + // Start session if needed startAudioSessionIfNeeded(sampleBuffer: sampleBuffer) - + guard separateAudioInput.isReadyForMoreMediaData else { debugPrint("separateAudioInput is not ready") return false } return separateAudioInput.append(sampleBuffer) } + + fileprivate func captureAppAudioOutput(_ sampleBuffer: CMSampleBuffer) -> Bool { + guard separateAudioFile, let appWriter = appAudioWriter else { + return false + } + + // Check if app audio writer is still writing + guard appWriter.status == .writing else { + debugPrint("appAudioWriter is not writing, status: \(appWriter.status.description)") + return false + } + + // Start session if needed + startAppAudioSessionIfNeeded(sampleBuffer: sampleBuffer) + + guard appAudioInput.isReadyForMoreMediaData else { + debugPrint("appAudioInput is not ready") + return false + } + return appAudioInput.append(sampleBuffer) + } } diff --git a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift index f8feae7..bbea949 100644 --- a/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift +++ b/lib/typescript/expo-plugin/support/broadcastExtensionFiles/SampleHandler.swift @@ -1,7 +1,7 @@ import AVFoundation +import Darwin import ReplayKit import UserNotifications -import Darwin @_silgen_name("finishBroadcastGracefully") func finishBroadcastGracefully(_ handler: RPBroadcastSampleHandler) @@ -16,14 +16,16 @@ final class SampleHandler: RPBroadcastSampleHandler { // MARK: โ€“ Properties private func appGroupIDFromPlist() -> String? { - guard let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") as? String, + guard + let value = Bundle.main.object(forInfoDictionaryKey: "BroadcastExtensionAppGroupIdentifier") + as? String, !value.isEmpty else { return nil } return value } - + // Store both the CFString and CFNotificationName versions private static let stopNotificationString = "com.nitroscreenrecorder.stopBroadcast" as CFString private static let stopNotificationName = CFNotificationName(stopNotificationString) @@ -35,7 +37,8 @@ final class SampleHandler: RPBroadcastSampleHandler { private var writer: BroadcastWriter? private let fileManager: FileManager = .default private let nodeURL: URL - private let audioNodeURL: URL + private let audioNodeURL: URL // Mic audio + private let appAudioNodeURL: URL // App/system audio private var sawMicBuffers = false private var separateAudioFile: Bool = false @@ -45,16 +48,21 @@ final class SampleHandler: RPBroadcastSampleHandler { nodeURL = fileManager.temporaryDirectory .appendingPathComponent(uuid) .appendingPathExtension(for: .mpeg4Movie) - + audioNodeURL = fileManager.temporaryDirectory - .appendingPathComponent("\(uuid)_audio") + .appendingPathComponent("\(uuid)_mic_audio") + .appendingPathExtension("m4a") + + appAudioNodeURL = fileManager.temporaryDirectory + .appendingPathComponent("\(uuid)_app_audio") .appendingPathExtension("m4a") fileManager.removeFileIfExists(url: nodeURL) fileManager.removeFileIfExists(url: audioNodeURL) + fileManager.removeFileIfExists(url: appAudioNodeURL) super.init() } - + deinit { CFNotificationCenterRemoveObserver( CFNotificationCenterGetDarwinNotifyCenter(), @@ -63,7 +71,7 @@ final class SampleHandler: RPBroadcastSampleHandler { nil ) } - + private func startListeningForStopSignal() { let center = CFNotificationCenterGetDarwinNotifyCenter() @@ -95,7 +103,7 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { finishBroadcastWithError( NSError( - domain: "SampleHandler", + domain: "SampleHandler", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing app group identifier"] ) @@ -117,6 +125,7 @@ final class SampleHandler: RPBroadcastSampleHandler { writer = try .init( outputURL: nodeURL, audioOutputURL: separateAudioFile ? audioNodeURL : nil, + appAudioOutputURL: separateAudioFile ? appAudioNodeURL : nil, screenSize: screen.bounds.size, screenScale: screen.scale, separateAudioFile: separateAudioFile @@ -128,15 +137,20 @@ final class SampleHandler: RPBroadcastSampleHandler { } private func cleanupOldRecordings(in groupID: String) { - guard let docs = fileManager.containerURL( - forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let docs = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } do { let items = try fileManager.contentsOfDirectory(at: docs, includingPropertiesForKeys: nil) - for url in items where url.pathExtension.lowercased() == "mp4" { - try? fileManager.removeItem(at: url) + for url in items { + let ext = url.pathExtension.lowercased() + // Clean up video and audio files from previous recordings + if ext == "mp4" || ext == "m4a" { + try? fileManager.removeItem(at: url) + } } } catch { // Non-critical error, continue with broadcast @@ -149,8 +163,8 @@ final class SampleHandler: RPBroadcastSampleHandler { ) { guard let writer else { return } - if sampleBufferType == .audioMic { - sawMicBuffers = true + if sampleBufferType == .audioMic { + sawMicBuffers = true } do { @@ -160,18 +174,18 @@ final class SampleHandler: RPBroadcastSampleHandler { } } - override func broadcastPaused() { - writer?.pause() + override func broadcastPaused() { + writer?.pause() } - - override func broadcastResumed() { - writer?.resume() + + override func broadcastResumed() { + writer?.resume() } private func stopBroadcastGracefully() { finishBroadcastGracefully(self) } - + override func broadcastFinished() { guard let writer else { return } @@ -188,9 +202,11 @@ final class SampleHandler: RPBroadcastSampleHandler { guard let groupID = hostAppGroupIdentifier else { return } // Get container directory - guard let containerURL = fileManager - .containerURL(forSecurityApplicationGroupIdentifier: groupID)? - .appendingPathComponent("Library/Documents/", isDirectory: true) + guard + let containerURL = + fileManager + .containerURL(forSecurityApplicationGroupIdentifier: groupID)? + .appendingPathComponent("Library/Documents/", isDirectory: true) else { return } // Create directory if needed @@ -208,25 +224,43 @@ final class SampleHandler: RPBroadcastSampleHandler { // File move failed, but we can't error out at this point return } - - // Move audio file to shared container if it exists + + // Move mic audio file to shared container if it exists if let audioURL = result.audioURL { let audioDestination = containerURL.appendingPathComponent(audioURL.lastPathComponent) do { try fileManager.moveItem(at: audioURL, to: audioDestination) - // Store audio file name for retrieval + // Store mic audio file name for retrieval UserDefaults(suiteName: groupID)? .set(audioDestination.lastPathComponent, forKey: "LastBroadcastAudioFileName") } catch { // Audio file move failed, but video is already saved - debugPrint("Failed to move audio file: \(error)") + debugPrint("Failed to move mic audio file: \(error)") } } else { - // Clear audio file name if no separate audio + // Clear mic audio file name if no separate audio UserDefaults(suiteName: groupID)? .removeObject(forKey: "LastBroadcastAudioFileName") } + // Move app audio file to shared container if it exists + if let appAudioURL = result.appAudioURL { + let appAudioDestination = containerURL.appendingPathComponent(appAudioURL.lastPathComponent) + do { + try fileManager.moveItem(at: appAudioURL, to: appAudioDestination) + // Store app audio file name for retrieval + UserDefaults(suiteName: groupID)? + .set(appAudioDestination.lastPathComponent, forKey: "LastBroadcastAppAudioFileName") + } catch { + // App audio file move failed, but video is already saved + debugPrint("Failed to move app audio file: \(error)") + } + } else { + // Clear app audio file name if no separate audio + UserDefaults(suiteName: groupID)? + .removeObject(forKey: "LastBroadcastAppAudioFileName") + } + // Persist microphone state and audio file state UserDefaults(suiteName: groupID)? .set(sawMicBuffers, forKey: "LastBroadcastMicrophoneWasEnabled") @@ -241,4 +275,4 @@ extension FileManager { guard fileExists(atPath: url.path) else { return } try? removeItem(at: url) } -} \ No newline at end of file +} diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts index 2c77d6d..27b2cec 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -231,10 +231,16 @@ export interface AudioRecordingFile { * duration: 30.5, // 30.5 seconds * enabledMicrophone: true, * audioFile: { - * path: '/path/to/recording.m4a', - * name: 'screen_recording_2024_01_15.m4a', + * path: '/path/to/mic_audio.m4a', + * name: 'mic_audio.m4a', * size: 1048576, * duration: 30.5 + * }, + * appAudioFile: { + * path: '/path/to/app_audio.m4a', + * name: 'app_audio.m4a', + * size: 2097152, + * duration: 30.5 * } * }; * ``` @@ -250,8 +256,18 @@ export interface ScreenRecordingFile { duration: number; /** Whether microphone audio was recorded */ enabledMicrophone: boolean; - /** Optional separate audio file (when separateAudioFile option is enabled) */ + /** + * Optional separate microphone audio file (when separateAudioFile option is enabled). + * Contains only the microphone audio track. + */ audioFile?: AudioRecordingFile; + /** + * Optional separate app/system audio file (when separateAudioFile option is enabled). + * Contains only the app/system audio track. + * Note: Only available on iOS. On Android, app audio capture requires Android 10+ + * and is not currently supported. + */ + appAudioFile?: AudioRecordingFile; } /** * Error object returned when recording operations fail. diff --git a/lib/typescript/types.d.ts.map b/lib/typescript/types.d.ts.map index db6f2b5..1bc23b0 100644 --- a/lib/typescript/types.d.ts.map +++ b/lib/typescript/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nitrogen/generated/android/c++/JScreenRecordingFile.hpp b/nitrogen/generated/android/c++/JScreenRecordingFile.hpp index 2179296..e655b78 100644 --- a/nitrogen/generated/android/c++/JScreenRecordingFile.hpp +++ b/nitrogen/generated/android/c++/JScreenRecordingFile.hpp @@ -46,13 +46,16 @@ namespace margelo::nitro::nitroscreenrecorder { jboolean enabledMicrophone = this->getFieldValue(fieldEnabledMicrophone); static const auto fieldAudioFile = clazz->getField("audioFile"); jni::local_ref audioFile = this->getFieldValue(fieldAudioFile); + static const auto fieldAppAudioFile = clazz->getField("appAudioFile"); + jni::local_ref appAudioFile = this->getFieldValue(fieldAppAudioFile); return ScreenRecordingFile( path->toStdString(), name->toStdString(), size, duration, static_cast(enabledMicrophone), - audioFile != nullptr ? std::make_optional(audioFile->toCpp()) : std::nullopt + audioFile != nullptr ? std::make_optional(audioFile->toCpp()) : std::nullopt, + appAudioFile != nullptr ? std::make_optional(appAudioFile->toCpp()) : std::nullopt ); } @@ -62,7 +65,7 @@ namespace margelo::nitro::nitroscreenrecorder { */ [[maybe_unused]] static jni::local_ref fromCpp(const ScreenRecordingFile& value) { - using JSignature = JScreenRecordingFile(jni::alias_ref, jni::alias_ref, double, double, jboolean, jni::alias_ref); + using JSignature = JScreenRecordingFile(jni::alias_ref, jni::alias_ref, double, double, jboolean, jni::alias_ref, jni::alias_ref); static const auto clazz = javaClassStatic(); static const auto create = clazz->getStaticMethod("fromCpp"); return create( @@ -72,7 +75,8 @@ namespace margelo::nitro::nitroscreenrecorder { value.size, value.duration, value.enabledMicrophone, - value.audioFile.has_value() ? JAudioRecordingFile::fromCpp(value.audioFile.value()) : nullptr + value.audioFile.has_value() ? JAudioRecordingFile::fromCpp(value.audioFile.value()) : nullptr, + value.appAudioFile.has_value() ? JAudioRecordingFile::fromCpp(value.appAudioFile.value()) : nullptr ); } }; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt index bb7cbbb..cb97921 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ScreenRecordingFile.kt @@ -34,7 +34,10 @@ data class ScreenRecordingFile( val enabledMicrophone: Boolean, @DoNotStrip @Keep - val audioFile: AudioRecordingFile? + val audioFile: AudioRecordingFile?, + @DoNotStrip + @Keep + val appAudioFile: AudioRecordingFile? ) { /* primary constructor */ @@ -46,8 +49,8 @@ data class ScreenRecordingFile( @Keep @Suppress("unused") @JvmStatic - private fun fromCpp(path: String, name: String, size: Double, duration: Double, enabledMicrophone: Boolean, audioFile: AudioRecordingFile?): ScreenRecordingFile { - return ScreenRecordingFile(path, name, size, duration, enabledMicrophone, audioFile) + private fun fromCpp(path: String, name: String, size: Double, duration: Double, enabledMicrophone: Boolean, audioFile: AudioRecordingFile?, appAudioFile: AudioRecordingFile?): ScreenRecordingFile { + return ScreenRecordingFile(path, name, size, duration, enabledMicrophone, audioFile, appAudioFile) } } } diff --git a/nitrogen/generated/ios/swift/ScreenRecordingFile.swift b/nitrogen/generated/ios/swift/ScreenRecordingFile.swift index 9483464..9ea783e 100644 --- a/nitrogen/generated/ios/swift/ScreenRecordingFile.swift +++ b/nitrogen/generated/ios/swift/ScreenRecordingFile.swift @@ -18,13 +18,19 @@ public extension ScreenRecordingFile { /** * Create a new instance of `ScreenRecordingFile`. */ - init(path: String, name: String, size: Double, duration: Double, enabledMicrophone: Bool, audioFile: AudioRecordingFile?) { + init(path: String, name: String, size: Double, duration: Double, enabledMicrophone: Bool, audioFile: AudioRecordingFile?, appAudioFile: AudioRecordingFile?) { self.init(std.string(path), std.string(name), size, duration, enabledMicrophone, { () -> bridge.std__optional_AudioRecordingFile_ in if let __unwrappedValue = audioFile { return bridge.create_std__optional_AudioRecordingFile_(__unwrappedValue) } else { return .init() } + }(), { () -> bridge.std__optional_AudioRecordingFile_ in + if let __unwrappedValue = appAudioFile { + return bridge.create_std__optional_AudioRecordingFile_(__unwrappedValue) + } else { + return .init() + } }()) } @@ -99,4 +105,21 @@ public extension ScreenRecordingFile { }() } } + + var appAudioFile: AudioRecordingFile? { + @inline(__always) + get { + return self.__appAudioFile.value + } + @inline(__always) + set { + self.__appAudioFile = { () -> bridge.std__optional_AudioRecordingFile_ in + if let __unwrappedValue = newValue { + return bridge.create_std__optional_AudioRecordingFile_(__unwrappedValue) + } else { + return .init() + } + }() + } + } } diff --git a/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp b/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp index 7cd063f..678a49e 100644 --- a/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp +++ b/nitrogen/generated/shared/c++/ScreenRecordingFile.hpp @@ -43,10 +43,11 @@ namespace margelo::nitro::nitroscreenrecorder { double duration SWIFT_PRIVATE; bool enabledMicrophone SWIFT_PRIVATE; std::optional audioFile SWIFT_PRIVATE; + std::optional appAudioFile SWIFT_PRIVATE; public: ScreenRecordingFile() = default; - explicit ScreenRecordingFile(std::string path, std::string name, double size, double duration, bool enabledMicrophone, std::optional audioFile): path(path), name(name), size(size), duration(duration), enabledMicrophone(enabledMicrophone), audioFile(audioFile) {} + explicit ScreenRecordingFile(std::string path, std::string name, double size, double duration, bool enabledMicrophone, std::optional audioFile, std::optional appAudioFile): path(path), name(name), size(size), duration(duration), enabledMicrophone(enabledMicrophone), audioFile(audioFile), appAudioFile(appAudioFile) {} }; } // namespace margelo::nitro::nitroscreenrecorder @@ -64,7 +65,8 @@ namespace margelo::nitro { JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "size")), JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "duration")), JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "enabledMicrophone")), - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "audioFile")) + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "audioFile")), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, "appAudioFile")) ); } static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::ScreenRecordingFile& arg) { @@ -75,6 +77,7 @@ namespace margelo::nitro { obj.setProperty(runtime, "duration", JSIConverter::toJSI(runtime, arg.duration)); obj.setProperty(runtime, "enabledMicrophone", JSIConverter::toJSI(runtime, arg.enabledMicrophone)); obj.setProperty(runtime, "audioFile", JSIConverter>::toJSI(runtime, arg.audioFile)); + obj.setProperty(runtime, "appAudioFile", JSIConverter>::toJSI(runtime, arg.appAudioFile)); return obj; } static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { @@ -91,6 +94,7 @@ namespace margelo::nitro { if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "duration"))) return false; if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "enabledMicrophone"))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "audioFile"))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, "appAudioFile"))) return false; return true; } }; diff --git a/src/types.ts b/src/types.ts index f5534ce..42d610a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -243,10 +243,16 @@ export interface AudioRecordingFile { * duration: 30.5, // 30.5 seconds * enabledMicrophone: true, * audioFile: { - * path: '/path/to/recording.m4a', - * name: 'screen_recording_2024_01_15.m4a', + * path: '/path/to/mic_audio.m4a', + * name: 'mic_audio.m4a', * size: 1048576, * duration: 30.5 + * }, + * appAudioFile: { + * path: '/path/to/app_audio.m4a', + * name: 'app_audio.m4a', + * size: 2097152, + * duration: 30.5 * } * }; * ``` @@ -262,8 +268,18 @@ export interface ScreenRecordingFile { duration: number; /** Whether microphone audio was recorded */ enabledMicrophone: boolean; - /** Optional separate audio file (when separateAudioFile option is enabled) */ + /** + * Optional separate microphone audio file (when separateAudioFile option is enabled). + * Contains only the microphone audio track. + */ audioFile?: AudioRecordingFile; + /** + * Optional separate app/system audio file (when separateAudioFile option is enabled). + * Contains only the app/system audio track. + * Note: Only available on iOS. On Android, app audio capture requires Android 10+ + * and is not currently supported. + */ + appAudioFile?: AudioRecordingFile; } /** From dfc96667401d81b581510b6c84055cbd0071d3fe Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 8 Dec 2025 16:39:46 -0800 Subject: [PATCH 07/32] chore: bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d8ec26..b635167 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.7.1", + "version": "0.8.1", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", From b87152da36979b3af8f59593362528baefe9297f Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 8 Dec 2025 16:56:43 -0800 Subject: [PATCH 08/32] fix: nitrogen binding --- ios/NitroScreenRecorder.swift | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/NitroScreenRecorder.swift b/ios/NitroScreenRecorder.swift index c1c3026..a8d69cf 100644 --- a/ios/NitroScreenRecorder.swift +++ b/ios/NitroScreenRecorder.swift @@ -423,7 +423,8 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { size: attrs[.size] as? Double ?? 0, duration: duration, enabledMicrophone: self.recorder.isMicrophoneEnabled, - audioFile: audioFile + audioFile: audioFile, + appAudioFile: nil // In-app recording doesn't capture app audio separately ) print("โœ… Recording finished and saved to:", outputURL.path) diff --git a/package.json b/package.json index b635167..dd1bdd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.8.1", + "version": "0.8.2", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", From 42643d0ad6b19030a8507b247992b8c0f3c979f9 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 9 Dec 2025 10:54:32 -0800 Subject: [PATCH 09/32] fix: patched enitlements --- expo-plugin/ios/withMainAppEntitlementsFile.ts | 17 +++++++++++++++-- .../ios/withMainAppEntitlementsFile.js | 13 +++++++++++-- .../ios/withMainAppEntitlementsFile.js | 13 +++++++++++-- .../ios/withMainAppEntitlementsFile.js | 13 +++++++++++-- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/expo-plugin/ios/withMainAppEntitlementsFile.ts b/expo-plugin/ios/withMainAppEntitlementsFile.ts index 67a1151..af1f9f5 100644 --- a/expo-plugin/ios/withMainAppEntitlementsFile.ts +++ b/expo-plugin/ios/withMainAppEntitlementsFile.ts @@ -17,6 +17,15 @@ export const withMainAppEntitlementsFile: ConfigPlugin = ( // Check if the entitlements file is already added to the project const files = xcodeProject.hash.project.objects.PBXFileReference; + + // Safety check: ensure files object exists + if (!files) { + ScreenRecorderLog.log( + 'PBXFileReference not found in project. Skipping entitlements file addition.' + ); + return newConfig; + } + const entitlementsFileExists = Object.values(files).some( (file: any) => file && file.path === `"${entitlementsFileName}"` ); @@ -64,15 +73,19 @@ export const withMainAppEntitlementsFile: ConfigPlugin = ( } // If still not found, try to find the group that contains AppDelegate or main source files - if (!mainAppGroupKey) { + if (!mainAppGroupKey && files) { ScreenRecorderLog.log( 'Trying to find main app group by looking for AppDelegate...' ); for (const key in groups) { const group = groups[key]; - if (group && group.children) { + if (group && group.children && Array.isArray(group.children)) { // Check if this group contains typical main app files const hasMainAppFiles = group.children.some((childKey: string) => { + // Safety check: ensure childKey is a valid string + if (!childKey || typeof childKey !== 'string') { + return false; + } const file = files[childKey]; return ( file && diff --git a/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js b/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js index e6da01b..1fda04c 100644 --- a/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js +++ b/lib/commonjs/expo-plugin/ios/withMainAppEntitlementsFile.js @@ -15,6 +15,11 @@ const withMainAppEntitlementsFile = (config) => { const entitlementsPath = `${projectName}/${entitlementsFileName}`; // Check if the entitlements file is already added to the project const files = xcodeProject.hash.project.objects.PBXFileReference; + // Safety check: ensure files object exists + if (!files) { + ScreenRecorderLog_1.ScreenRecorderLog.log('PBXFileReference not found in project. Skipping entitlements file addition.'); + return newConfig; + } const entitlementsFileExists = Object.values(files).some((file) => file && file.path === `"${entitlementsFileName}"`); if (entitlementsFileExists) { ScreenRecorderLog_1.ScreenRecorderLog.log(`${entitlementsFileName} already exists in project. Skipping...`); @@ -51,14 +56,18 @@ const withMainAppEntitlementsFile = (config) => { break; } // If still not found, try to find the group that contains AppDelegate or main source files - if (!mainAppGroupKey) { + if (!mainAppGroupKey && files) { ScreenRecorderLog_1.ScreenRecorderLog.log('Trying to find main app group by looking for AppDelegate...'); for (const key in groups) { const group = groups[key]; - if (group && group.children) { + if (group && group.children && Array.isArray(group.children)) { // Check if this group contains typical main app files const hasMainAppFiles = group.children.some((childKey) => { var _a, _b, _c; + // Safety check: ensure childKey is a valid string + if (!childKey || typeof childKey !== 'string') { + return false; + } const file = files[childKey]; return (file && (((_a = file.path) === null || _a === void 0 ? void 0 : _a.includes('AppDelegate')) || diff --git a/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js b/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js index e6da01b..1fda04c 100644 --- a/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js +++ b/lib/module/expo-plugin/ios/withMainAppEntitlementsFile.js @@ -15,6 +15,11 @@ const withMainAppEntitlementsFile = (config) => { const entitlementsPath = `${projectName}/${entitlementsFileName}`; // Check if the entitlements file is already added to the project const files = xcodeProject.hash.project.objects.PBXFileReference; + // Safety check: ensure files object exists + if (!files) { + ScreenRecorderLog_1.ScreenRecorderLog.log('PBXFileReference not found in project. Skipping entitlements file addition.'); + return newConfig; + } const entitlementsFileExists = Object.values(files).some((file) => file && file.path === `"${entitlementsFileName}"`); if (entitlementsFileExists) { ScreenRecorderLog_1.ScreenRecorderLog.log(`${entitlementsFileName} already exists in project. Skipping...`); @@ -51,14 +56,18 @@ const withMainAppEntitlementsFile = (config) => { break; } // If still not found, try to find the group that contains AppDelegate or main source files - if (!mainAppGroupKey) { + if (!mainAppGroupKey && files) { ScreenRecorderLog_1.ScreenRecorderLog.log('Trying to find main app group by looking for AppDelegate...'); for (const key in groups) { const group = groups[key]; - if (group && group.children) { + if (group && group.children && Array.isArray(group.children)) { // Check if this group contains typical main app files const hasMainAppFiles = group.children.some((childKey) => { var _a, _b, _c; + // Safety check: ensure childKey is a valid string + if (!childKey || typeof childKey !== 'string') { + return false; + } const file = files[childKey]; return (file && (((_a = file.path) === null || _a === void 0 ? void 0 : _a.includes('AppDelegate')) || diff --git a/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js b/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js index e6da01b..1fda04c 100644 --- a/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js +++ b/lib/typescript/expo-plugin/ios/withMainAppEntitlementsFile.js @@ -15,6 +15,11 @@ const withMainAppEntitlementsFile = (config) => { const entitlementsPath = `${projectName}/${entitlementsFileName}`; // Check if the entitlements file is already added to the project const files = xcodeProject.hash.project.objects.PBXFileReference; + // Safety check: ensure files object exists + if (!files) { + ScreenRecorderLog_1.ScreenRecorderLog.log('PBXFileReference not found in project. Skipping entitlements file addition.'); + return newConfig; + } const entitlementsFileExists = Object.values(files).some((file) => file && file.path === `"${entitlementsFileName}"`); if (entitlementsFileExists) { ScreenRecorderLog_1.ScreenRecorderLog.log(`${entitlementsFileName} already exists in project. Skipping...`); @@ -51,14 +56,18 @@ const withMainAppEntitlementsFile = (config) => { break; } // If still not found, try to find the group that contains AppDelegate or main source files - if (!mainAppGroupKey) { + if (!mainAppGroupKey && files) { ScreenRecorderLog_1.ScreenRecorderLog.log('Trying to find main app group by looking for AppDelegate...'); for (const key in groups) { const group = groups[key]; - if (group && group.children) { + if (group && group.children && Array.isArray(group.children)) { // Check if this group contains typical main app files const hasMainAppFiles = group.children.some((childKey) => { var _a, _b, _c; + // Safety check: ensure childKey is a valid string + if (!childKey || typeof childKey !== 'string') { + return false; + } const file = files[childKey]; return (file && (((_a = file.path) === null || _a === void 0 ? void 0 : _a.includes('AppDelegate')) || From 40090d5e975d8b2602b5fb8eea2d7ec0a0d3757a Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 9 Dec 2025 15:03:09 -0800 Subject: [PATCH 10/32] feat: make startglobalrecording a promise --- .../NitroScreenRecorder.kt | 84 +++++++++++-------- ios/NitroScreenRecorder.swift | 79 +++++++++++++---- package.json | 2 +- src/NitroScreenRecorder.nitro.ts | 16 +++- src/functions.ts | 41 +++++++-- src/types.ts | 21 +++-- 6 files changed, 176 insertions(+), 67 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt index c469554..c9bcb16 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt @@ -19,6 +19,7 @@ import com.margelo.nitro.core.* import com.margelo.nitro.nitroscreenrecorder.utils.RecorderUtils import java.io.File import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull import kotlin.coroutines.resume import kotlinx.coroutines.delay @@ -34,7 +35,7 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { private var isServiceBound = false private var lastGlobalRecording: File? = null private var lastGlobalAudioRecording: File? = null - private var globalRecordingErrorCallback: ((RecordingError) -> Unit)? = null + private var globalRecordingStartContinuation: kotlin.coroutines.Continuation? = null private val screenRecordingListeners = mutableListOf Unit>>() @@ -61,6 +62,12 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { "๐Ÿ”” notifyGlobalRecordingEvent called with type: ${event.type}, reason: ${event.reason}" ) instance?.notifyListeners(event) + + // Resolve the start promise when recording begins + if (event.type == RecordingEventType.GLOBAL && event.reason == RecordingEventReason.BEGAN) { + instance?.globalRecordingStartContinuation?.resume(true) + instance?.globalRecordingStartContinuation = null + } } fun notifyGlobalRecordingFinished( @@ -82,7 +89,9 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { TAG, "โŒ notifyGlobalRecordingError called with error: ${error.name} - ${error.message}" ) - instance?.globalRecordingErrorCallback?.invoke(error) + // Resolve the start promise with null (recording failed to start) + instance?.globalRecordingStartContinuation?.resume(null) + instance?.globalRecordingStartContinuation = null } } @@ -301,43 +310,52 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { // --- Global Recording Methods --- - override fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: (RecordingError) -> Unit) { - if (globalRecordingService?.isCurrentlyRecording() == true) { - Log.w(TAG, "โš ๏ธ Global recording already in progress") - return - } - val ctx = NitroModules.applicationContext ?: throw Error("NO_CONTEXT") + override fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, timeoutMs: Double): Promise = + Promise.async { + if (globalRecordingService?.isCurrentlyRecording() == true) { + Log.w(TAG, "โš ๏ธ Global recording already in progress") + throw Error("BROADCAST_ALREADY_ACTIVE: A screen recording session is already in progress.") + } + val ctx = NitroModules.applicationContext ?: throw Error("NO_CONTEXT") - // Store the error callback so it can be used by the service - globalRecordingErrorCallback = onRecordingError + // Cancel any existing pending continuation + globalRecordingStartContinuation?.resume(null) + globalRecordingStartContinuation = null - requestGlobalRecordingPermission().then { (resultCode, resultData) -> - if (!isServiceBound) { - val serviceIntent = Intent(ctx, ScreenRecordingService::class.java) - ctx.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE) - } + try { + val (resultCode, resultData) = requestGlobalRecordingPermission().await() - val startIntent = Intent(ctx, ScreenRecordingService::class.java).apply { - action = ScreenRecordingService.ACTION_START_RECORDING - putExtra(ScreenRecordingService.EXTRA_RESULT_CODE, resultCode) - putExtra(ScreenRecordingService.EXTRA_RESULT_DATA, resultData) - putExtra(ScreenRecordingService.EXTRA_ENABLE_MIC, enableMic) - putExtra(ScreenRecordingService.EXTRA_SEPARATE_AUDIO, separateAudioFile) - } + if (!isServiceBound) { + val serviceIntent = Intent(ctx, ScreenRecordingService::class.java) + ctx.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ctx.startForegroundService(startIntent) - } else { - ctx.startService(startIntent) + val startIntent = Intent(ctx, ScreenRecordingService::class.java).apply { + action = ScreenRecordingService.ACTION_START_RECORDING + putExtra(ScreenRecordingService.EXTRA_RESULT_CODE, resultCode) + putExtra(ScreenRecordingService.EXTRA_RESULT_DATA, resultData) + putExtra(ScreenRecordingService.EXTRA_ENABLE_MIC, enableMic) + putExtra(ScreenRecordingService.EXTRA_SEPARATE_AUDIO, separateAudioFile) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(startIntent) + } else { + ctx.startService(startIntent) + } + + // Wait for recording to start (or timeout) + kotlinx.coroutines.withTimeoutOrNull(timeoutMs.toLong()) { + suspendCancellableCoroutine { cont -> + globalRecordingStartContinuation = cont + } + } + } catch (e: Exception) { + Log.e(TAG, "โŒ Failed to start global recording: ${e.message}") + // User denied permission or other error - return null (cancelled) + null } - }.catch { error -> - val recordingError = RecordingError( - name = "GlobalRecordingStartError", - message = error.message ?: "Failed to start global recording" - ) - onRecordingError(recordingError) // Use the callback parameter directly } - } override fun stopGlobalRecording(settledTimeMs: Double): Promise { return Promise.async { diff --git a/ios/NitroScreenRecorder.swift b/ios/NitroScreenRecorder.swift index a8d69cf..3ef5070 100644 --- a/ios/NitroScreenRecorder.swift +++ b/ios/NitroScreenRecorder.swift @@ -43,6 +43,10 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { private var isBroadcastModalShowing: Bool = false private var appStateObservers: [NSObjectProtocol] = [] + // Promise continuation for startGlobalRecording + private var globalRecordingContinuation: CheckedContinuation? + private var globalRecordingTimeoutTask: Task? + override init() { super.init() registerListener() @@ -150,6 +154,11 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { // Notify all listeners that the modal was dismissed broadcastPickerEventListeners.forEach { $0.callback(.dismissed) } + + // If we have a pending continuation and recording didn't start, resolve with nil (user cancelled) + if !UIScreen.main.isCaptured { + resolveGlobalRecordingPromise(with: nil) + } } @objc private func handleScreenRecordingChange() { @@ -163,6 +172,12 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } else { type = .global isGlobalRecordingActive = true + + // Resolve the promise if we were waiting for global recording to start + if globalRecordingInitiatedByThisPackage { + isBroadcastModalShowing = false // Modal is gone once recording starts + resolveGlobalRecordingPromise(with: true) + } } } else { reason = .ended @@ -546,55 +561,85 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } } + /// Helper to resolve the global recording promise and clean up + private func resolveGlobalRecordingPromise(with result: Bool?) { + globalRecordingTimeoutTask?.cancel() + globalRecordingTimeoutTask = nil + + if let continuation = globalRecordingContinuation { + globalRecordingContinuation = nil + continuation.resume(returning: result) + } + } + func startGlobalRecording( - enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (RecordingError) -> Void - ) - throws - { + enableMic: Bool, separateAudioFile: Bool, timeoutMs: Double + ) throws -> Promise { + // Validate not already recording guard !isGlobalRecordingActive else { - print("โš ๏ธ Attempted to start a global recording, but one is already active.") - let error = RecordingError( + throw RecorderError.error( name: "BROADCAST_ALREADY_ACTIVE", message: "A screen recording session is already in progress." ) - onRecordingError(error) - return } + // Cancel any existing pending promise + resolveGlobalRecordingPromise(with: nil) + // Validate that we can access the app group (needed for global recordings) guard let appGroupId = try? getAppGroupIdentifier() else { - let error = RecordingError( + throw RecorderError.error( name: "APP_GROUP_ACCESS_FAILED", message: "Could not access app group identifier required for global recording. Something is wrong with your entitlements." ) - onRecordingError(error) - return } guard FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: appGroupId) != nil else { - let error = RecordingError( + throw RecorderError.error( name: "APP_GROUP_CONTAINER_FAILED", message: "Could not access app group container required for global recording. Something is wrong with your entitlements." ) - onRecordingError(error) - return } // Store the separateAudioFile preference for the broadcast extension to read self.separateAudioFileEnabled = separateAudioFile UserDefaults(suiteName: appGroupId)?.set(separateAudioFile, forKey: "SeparateAudioFileEnabled") + // Mark that we initiated this recording + globalRecordingInitiatedByThisPackage = true + // Present the broadcast picker presentGlobalBroadcastModal(enableMicrophone: enableMic) - // This is sort of a hack to try and track if the user opened the broadcast modal first - // may not be that reliable, because technically they can open this modal and close it without starting a broadcast - globalRecordingInitiatedByThisPackage = true + return Promise.async { [weak self] in + guard let self = self else { return nil } + + return await withCheckedContinuation { continuation in + self.globalRecordingContinuation = continuation + // Set up timeout + self.globalRecordingTimeoutTask = Task { + do { + try await Task.sleep(nanoseconds: UInt64(timeoutMs * 1_000_000)) + // If we get here, timeout occurred + await MainActor.run { + if self.globalRecordingContinuation != nil { + print("โฑ๏ธ Global recording start timed out after \(timeoutMs)ms") + self.isBroadcastModalShowing = false + self.globalRecordingInitiatedByThisPackage = false + self.resolveGlobalRecordingPromise(with: nil) + } + } + } catch { + // Task was cancelled, which is expected when recording starts or modal dismissed + } + } + } + } } // This is a hack I learned through: // https://mehmetbaykar.com/posts/how-to-gracefully-stop-a-broadcast-upload-extension/ diff --git a/package.json b/package.json index dd1bdd0..b86f6a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.8.2", + "version": "0.9.0", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/src/NitroScreenRecorder.nitro.ts b/src/NitroScreenRecorder.nitro.ts index 15b00a5..d1b807c 100644 --- a/src/NitroScreenRecorder.nitro.ts +++ b/src/NitroScreenRecorder.nitro.ts @@ -6,7 +6,6 @@ import type { ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, - RecordingError, BroadcastPickerPresentationEvent, } from './types'; @@ -66,11 +65,22 @@ export interface NitroScreenRecorder // GLOBAL RECORDING // ============================================================================ + /** + * Starts global screen recording (iOS: broadcast extension, Android: MediaProjection). + * + * @param enableMic - Whether to enable microphone recording + * @param separateAudioFile - Whether to save audio as a separate file + * @param timeoutMs - How long to wait for recording to start (default: 120000ms / 2 minutes) + * @returns Promise that resolves with: + * - `true` if recording started successfully + * - `undefined` if user dismissed/cancelled or timed out + * @throws Error if there's an actual failure (permissions, app group issues, etc.) + */ startGlobalRecording( enableMic: boolean, separateAudioFile: boolean, - onRecordingError: (error: RecordingError) => void - ): void; + timeoutMs: number + ): Promise; stopGlobalRecording( settledTimeMs: number ): Promise; diff --git a/src/functions.ts b/src/functions.ts index 989f4de..635ff65 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -200,32 +200,59 @@ export async function cancelInAppRecording(): Promise { // GLOBAL RECORDING // ============================================================================ +/** Default timeout for waiting for global recording to start (2 minutes) */ +const DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS = 120000; + /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * Requires screen recording permission on iOS. + * + * On iOS, this presents the system broadcast picker modal. The promise resolves + * when the user starts the broadcast, or with `undefined` if they dismiss the modal. + * + * On Android, this requests screen capture permission. The promise resolves + * when recording starts, or with `undefined` if the user denies permission. * * @platform iOS, Android + * @param input Configuration options for the recording session + * @returns Promise that resolves with: + * - `true` if recording started successfully + * - `undefined` if user cancelled/dismissed or timed out + * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * startGlobalRecording(); - * // User can now navigate to other apps while recording continues + * const started = await startGlobalRecording({ + * options: { enableMic: true }, + * timeoutMs: 60000 // 1 minute timeout + * }); + * + * if (started) { + * console.log('Recording started!'); + * // User can now navigate to other apps while recording continues + * } else { + * console.log('User cancelled or timed out'); + * } * ``` */ -export function startGlobalRecording(input: GlobalRecordingInput): void { - // On IOS, the user grants microphone permission via a picker toggle +export async function startGlobalRecording( + input?: GlobalRecordingInput +): Promise { + // On iOS, the user grants microphone permission via a picker toggle // button, so we don't need this check first if ( - input.options?.enableMic && + input?.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted' ) { throw new Error('Microphone permission not granted.'); } + + const timeoutMs = input?.timeoutMs ?? DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS; + return NitroScreenRecorderHybridObject.startGlobalRecording( input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, - input?.onRecordingError + timeoutMs ); } diff --git a/src/types.ts b/src/types.ts index 42d610a..de70855 100644 --- a/src/types.ts +++ b/src/types.ts @@ -192,18 +192,27 @@ export type GlobalRecordingInputOptions = { * options: { * enableMic: true, // Enable microphone audio for the recording * }, - * onRecordingError: (error) => { - * console.error('Global recording failed:', error.message); - * // Handle the error, e.g., display an alert to the user. - * } + * timeoutMs: 120000, // 2 minutes timeout * }; + * + * const started = await startGlobalRecording(globalInput); + * if (started) { + * console.log('Recording started successfully'); + * } else { + * console.log('User cancelled or timed out'); + * } * ``` */ export type GlobalRecordingInput = { /** Optional configuration options for the global recording session. */ options?: GlobalRecordingInputOptions; - /** Callback invoked when the global recording encounters an error during start or execution. */ - onRecordingError: (error: RecordingError) => void; + /** + * How long to wait (in milliseconds) for the recording to start before timing out. + * On iOS, this covers the time the user spends in the broadcast picker modal. + * On Android, this covers the time for the permission dialog and service startup. + * @default 120000 (2 minutes) + */ + timeoutMs?: number; }; /** From e27aa8070f6b1196ca50fef28c484f1f4e1b7b2c Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 9 Dec 2025 15:14:10 -0800 Subject: [PATCH 11/32] chore: run prepare --- lib/commonjs/functions.js | 37 +++++++-- lib/commonjs/functions.js.map | 2 +- lib/module/functions.js | 37 +++++++-- lib/module/functions.js.map | 2 +- lib/typescript/NitroScreenRecorder.nitro.d.ts | 15 +++- .../NitroScreenRecorder.nitro.d.ts.map | 2 +- lib/typescript/functions.d.ts | 27 ++++++- lib/typescript/functions.d.ts.map | 2 +- lib/typescript/types.d.ts | 21 +++-- lib/typescript/types.d.ts.map | 2 +- .../android/c++/JFunc_void_RecordingError.hpp | 77 ------------------ .../c++/JHybridNitroScreenRecorderSpec.cpp | 23 ++++-- .../c++/JHybridNitroScreenRecorderSpec.hpp | 2 +- .../generated/android/c++/JRecordingError.hpp | 61 -------------- .../Func_void_RecordingError.kt | 80 ------------------- .../HybridNitroScreenRecorderSpec.kt | 7 +- .../nitroscreenrecorder/RecordingError.kt | 41 ---------- .../android/nitroscreenrecorderOnLoad.cpp | 2 - .../NitroScreenRecorder-Swift-Cxx-Bridge.cpp | 10 +-- .../NitroScreenRecorder-Swift-Cxx-Bridge.hpp | 63 +++++++++++---- ...NitroScreenRecorder-Swift-Cxx-Umbrella.hpp | 3 - .../HybridNitroScreenRecorderSpecSwift.hpp | 9 +-- ...ft => Func_void_std__optional_bool_.swift} | 29 ++++--- .../swift/HybridNitroScreenRecorderSpec.swift | 2 +- .../HybridNitroScreenRecorderSpec_cxx.swift | 27 ++++--- .../generated/ios/swift/RecordingError.swift | 46 ----------- .../c++/HybridNitroScreenRecorderSpec.hpp | 5 +- .../generated/shared/c++/RecordingError.hpp | 79 ------------------ 28 files changed, 228 insertions(+), 485 deletions(-) delete mode 100644 nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp delete mode 100644 nitrogen/generated/android/c++/JRecordingError.hpp delete mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt delete mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt rename nitrogen/generated/ios/swift/{Func_void_RecordingError.swift => Func_void_std__optional_bool_.swift} (53%) delete mode 100644 nitrogen/generated/ios/swift/RecordingError.swift delete mode 100644 nitrogen/generated/shared/c++/RecordingError.hpp diff --git a/lib/commonjs/functions.js b/lib/commonjs/functions.js index 96b4dfe..be556d5 100644 --- a/lib/commonjs/functions.js +++ b/lib/commonjs/functions.js @@ -184,25 +184,48 @@ async function cancelInAppRecording() { // GLOBAL RECORDING // ============================================================================ +/** Default timeout for waiting for global recording to start (2 minutes) */ +const DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS = 120000; + /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * Requires screen recording permission on iOS. + * + * On iOS, this presents the system broadcast picker modal. The promise resolves + * when the user starts the broadcast, or with `undefined` if they dismiss the modal. + * + * On Android, this requests screen capture permission. The promise resolves + * when recording starts, or with `undefined` if the user denies permission. * * @platform iOS, Android + * @param input Configuration options for the recording session + * @returns Promise that resolves with: + * - `true` if recording started successfully + * - `undefined` if user cancelled/dismissed or timed out + * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * startGlobalRecording(); - * // User can now navigate to other apps while recording continues + * const started = await startGlobalRecording({ + * options: { enableMic: true }, + * timeoutMs: 60000 // 1 minute timeout + * }); + * + * if (started) { + * console.log('Recording started!'); + * // User can now navigate to other apps while recording continues + * } else { + * console.log('User cancelled or timed out'); + * } * ``` */ -function startGlobalRecording(input) { - // On IOS, the user grants microphone permission via a picker toggle +async function startGlobalRecording(input) { + // On iOS, the user grants microphone permission via a picker toggle // button, so we don't need this check first - if (input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { + if (input?.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { throw new Error('Microphone permission not granted.'); } - return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, input?.onRecordingError); + const timeoutMs = input?.timeoutMs ?? DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS; + return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, timeoutMs); } /** diff --git a/lib/commonjs/functions.js.map b/lib/commonjs/functions.js.map index 9fe31c0..3ffc621 100644 --- a/lib/commonjs/functions.js.map +++ b/lib/commonjs/functions.js.map @@ -1 +1 @@ -{"version":3,"names":["_reactNativeNitroModules","require","_reactNative","NitroScreenRecorderHybridObject","NitroModules","createHybridObject","isAndroid","Platform","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","startGlobalRecording","onRecordingError","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} +{"version":3,"names":["_reactNativeNitroModules","require","_reactNative","NitroScreenRecorderHybridObject","NitroModules","createHybridObject","isAndroid","Platform","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS","startGlobalRecording","timeoutMs","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} diff --git a/lib/module/functions.js b/lib/module/functions.js index 04f3768..aeb0505 100644 --- a/lib/module/functions.js +++ b/lib/module/functions.js @@ -168,25 +168,48 @@ export async function cancelInAppRecording() { // GLOBAL RECORDING // ============================================================================ +/** Default timeout for waiting for global recording to start (2 minutes) */ +const DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS = 120000; + /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * Requires screen recording permission on iOS. + * + * On iOS, this presents the system broadcast picker modal. The promise resolves + * when the user starts the broadcast, or with `undefined` if they dismiss the modal. + * + * On Android, this requests screen capture permission. The promise resolves + * when recording starts, or with `undefined` if the user denies permission. * * @platform iOS, Android + * @param input Configuration options for the recording session + * @returns Promise that resolves with: + * - `true` if recording started successfully + * - `undefined` if user cancelled/dismissed or timed out + * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * startGlobalRecording(); - * // User can now navigate to other apps while recording continues + * const started = await startGlobalRecording({ + * options: { enableMic: true }, + * timeoutMs: 60000 // 1 minute timeout + * }); + * + * if (started) { + * console.log('Recording started!'); + * // User can now navigate to other apps while recording continues + * } else { + * console.log('User cancelled or timed out'); + * } * ``` */ -export function startGlobalRecording(input) { - // On IOS, the user grants microphone permission via a picker toggle +export async function startGlobalRecording(input) { + // On iOS, the user grants microphone permission via a picker toggle // button, so we don't need this check first - if (input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { + if (input?.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { throw new Error('Microphone permission not granted.'); } - return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, input?.onRecordingError); + const timeoutMs = input?.timeoutMs ?? DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS; + return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, timeoutMs); } /** diff --git a/lib/module/functions.js.map b/lib/module/functions.js.map index cb13f29..5b937c5 100644 --- a/lib/module/functions.js.map +++ b/lib/module/functions.js.map @@ -1 +1 @@ -{"version":3,"names":["NitroModules","Platform","NitroScreenRecorderHybridObject","createHybridObject","isAndroid","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","startGlobalRecording","onRecordingError","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} +{"version":3,"names":["NitroModules","Platform","NitroScreenRecorderHybridObject","createHybridObject","isAndroid","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS","startGlobalRecording","timeoutMs","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} diff --git a/lib/typescript/NitroScreenRecorder.nitro.d.ts b/lib/typescript/NitroScreenRecorder.nitro.d.ts index bc37797..7cae257 100644 --- a/lib/typescript/NitroScreenRecorder.nitro.d.ts +++ b/lib/typescript/NitroScreenRecorder.nitro.d.ts @@ -1,5 +1,5 @@ import type { HybridObject } from 'react-native-nitro-modules'; -import type { CameraDevice, RecorderCameraStyle, PermissionResponse, ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, RecordingError, BroadcastPickerPresentationEvent } from './types'; +import type { CameraDevice, RecorderCameraStyle, PermissionResponse, ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, BroadcastPickerPresentationEvent } from './types'; /** * ============================================================================ * NOTES WITH NITRO-MODULES @@ -24,7 +24,18 @@ export interface NitroScreenRecorder extends HybridObject<{ startInAppRecording(enableMic: boolean, enableCamera: boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: boolean, onRecordingFinished: (file: ScreenRecordingFile) => void): void; stopInAppRecording(): Promise; cancelInAppRecording(): Promise; - startGlobalRecording(enableMic: boolean, separateAudioFile: boolean, onRecordingError: (error: RecordingError) => void): void; + /** + * Starts global screen recording (iOS: broadcast extension, Android: MediaProjection). + * + * @param enableMic - Whether to enable microphone recording + * @param separateAudioFile - Whether to save audio as a separate file + * @param timeoutMs - How long to wait for recording to start (default: 120000ms / 2 minutes) + * @returns Promise that resolves with: + * - `true` if recording started successfully + * - `undefined` if user dismissed/cancelled or timed out + * @throws Error if there's an actual failure (permissions, app group issues, etc.) + */ + startGlobalRecording(enableMic: boolean, separateAudioFile: boolean, timeoutMs: number): Promise; stopGlobalRecording(settledTimeMs: number): Promise; retrieveLastGlobalRecording(): ScreenRecordingFile | undefined; clearRecordingCache(): void; diff --git a/lib/typescript/NitroScreenRecorder.nitro.d.ts.map b/lib/typescript/NitroScreenRecorder.nitro.d.ts.map index 0c9489d..c3ca33e 100644 --- a/lib/typescript/NitroScreenRecorder.nitro.d.ts.map +++ b/lib/typescript/NitroScreenRecorder.nitro.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"NitroScreenRecorder.nitro.d.ts","sourceRoot":"","sources":["../../src/NitroScreenRecorder.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAEjB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAKzD,yBAAyB,IAAI,gBAAgB,CAAC;IAC9C,6BAA6B,IAAI,gBAAgB,CAAC;IAClD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAM3D,0BAA0B,CACxB,kCAAkC,EAAE,OAAO,EAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAC9C,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,0BAA0B,CACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAMhD,mBAAmB,CACjB,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,OAAO,EACrB,kBAAkB,EAAE,mBAAmB,EACvC,YAAY,EAAE,YAAY,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,GAEvD,IAAI,CAAC;IACR,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC/D,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAMtC,oBAAoB,CAClB,SAAS,EAAE,OAAO,EAClB,iBAAiB,EAAE,OAAO,EAC1B,gBAAgB,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAChD,IAAI,CAAC;IACR,mBAAmB,CACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC5C,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAM/D,mBAAmB,IAAI,IAAI,CAAC;CAC7B"} \ No newline at end of file +{"version":3,"file":"NitroScreenRecorder.nitro.d.ts","sourceRoot":"","sources":["../../src/NitroScreenRecorder.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAEjB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAKzD,yBAAyB,IAAI,gBAAgB,CAAC;IAC9C,6BAA6B,IAAI,gBAAgB,CAAC;IAClD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAM3D,0BAA0B,CACxB,kCAAkC,EAAE,OAAO,EAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAC9C,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,0BAA0B,CACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAMhD,mBAAmB,CACjB,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,OAAO,EACrB,kBAAkB,EAAE,mBAAmB,EACvC,YAAY,EAAE,YAAY,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,GAEvD,IAAI,CAAC;IACR,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC/D,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAMtC;;;;;;;;;;OAUG;IACH,oBAAoB,CAClB,SAAS,EAAE,OAAO,EAClB,iBAAiB,EAAE,OAAO,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAChC,mBAAmB,CACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC5C,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAM/D,mBAAmB,IAAI,IAAI,CAAC;CAC7B"} \ No newline at end of file diff --git a/lib/typescript/functions.d.ts b/lib/typescript/functions.d.ts index 3483257..8091da2 100644 --- a/lib/typescript/functions.d.ts +++ b/lib/typescript/functions.d.ts @@ -105,16 +105,35 @@ export declare function cancelInAppRecording(): Promise; /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * Requires screen recording permission on iOS. + * + * On iOS, this presents the system broadcast picker modal. The promise resolves + * when the user starts the broadcast, or with `undefined` if they dismiss the modal. + * + * On Android, this requests screen capture permission. The promise resolves + * when recording starts, or with `undefined` if the user denies permission. * * @platform iOS, Android + * @param input Configuration options for the recording session + * @returns Promise that resolves with: + * - `true` if recording started successfully + * - `undefined` if user cancelled/dismissed or timed out + * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * startGlobalRecording(); - * // User can now navigate to other apps while recording continues + * const started = await startGlobalRecording({ + * options: { enableMic: true }, + * timeoutMs: 60000 // 1 minute timeout + * }); + * + * if (started) { + * console.log('Recording started!'); + * // User can now navigate to other apps while recording continues + * } else { + * console.log('User cancelled or timed out'); + * } * ``` */ -export declare function startGlobalRecording(input: GlobalRecordingInput): void; +export declare function startGlobalRecording(input?: GlobalRecordingInput): Promise; /** * Stops the current global screen recording and saves the video. * The recorded file can be retrieved using retrieveLastGlobalRecording(). diff --git a/lib/typescript/functions.d.ts.map b/lib/typescript/functions.d.ts.map index 0294cb9..46a6447 100644 --- a/lib/typescript/functions.d.ts.map +++ b/lib/typescript/functions.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../src/functions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAYjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,gBAAgB,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,IAAI,gBAAgB,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE/E;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CACjD,mBAAmB,GAAG,SAAS,CAChC,CAMA;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAetE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAe3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAE7E;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,kCAA0C,GAC3C,EAAE;IACD,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,kCAAkC,EAAE,OAAO,CAAC;CAC7C,GAAG,MAAM,IAAI,CASb;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,IAAI,CAWZ;AAMD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"} \ No newline at end of file +{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../src/functions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAYjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,gBAAgB,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,IAAI,gBAAgB,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE/E;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CACjD,mBAAmB,GAAG,SAAS,CAChC,CAMA;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1D;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,CAAC,EAAE,oBAAoB,GAC3B,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAkB9B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAe3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAE7E;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,kCAA0C,GAC3C,EAAE;IACD,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,kCAAkC,EAAE,OAAO,CAAC;CAC7C,GAAG,MAAM,IAAI,CASb;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,IAAI,CAWZ;AAMD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"} \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts index 27b2cec..4b739d9 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -182,18 +182,27 @@ export type GlobalRecordingInputOptions = { * options: { * enableMic: true, // Enable microphone audio for the recording * }, - * onRecordingError: (error) => { - * console.error('Global recording failed:', error.message); - * // Handle the error, e.g., display an alert to the user. - * } + * timeoutMs: 120000, // 2 minutes timeout * }; + * + * const started = await startGlobalRecording(globalInput); + * if (started) { + * console.log('Recording started successfully'); + * } else { + * console.log('User cancelled or timed out'); + * } * ``` */ export type GlobalRecordingInput = { /** Optional configuration options for the global recording session. */ options?: GlobalRecordingInputOptions; - /** Callback invoked when the global recording encounters an error during start or execution. */ - onRecordingError: (error: RecordingError) => void; + /** + * How long to wait (in milliseconds) for the recording to start before timing out. + * On iOS, this covers the time the user spends in the broadcast picker modal. + * On Android, this covers the time for the permission dialog and service startup. + * @default 120000 (2 minutes) + */ + timeoutMs?: number; }; /** * Represents a separate audio file recorded alongside the video. diff --git a/lib/typescript/types.d.ts.map b/lib/typescript/types.d.ts.map index 1bc23b0..eee5af1 100644 --- a/lib/typescript/types.d.ts.map +++ b/lib/typescript/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp b/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp deleted file mode 100644 index dce2ade..0000000 --- a/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp +++ /dev/null @@ -1,77 +0,0 @@ -/// -/// JFunc_void_RecordingError.hpp -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright ยฉ 2025 Marc Rousavy @ Margelo -/// - -#pragma once - -#include -#include - -#include "RecordingError.hpp" -#include -#include "JRecordingError.hpp" -#include - -namespace margelo::nitro::nitroscreenrecorder { - - using namespace facebook; - - /** - * Represents the Java/Kotlin callback `(error: RecordingError) -> Unit`. - * This can be passed around between C++ and Java/Kotlin. - */ - struct JFunc_void_RecordingError: public jni::JavaClass { - public: - static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError;"; - - public: - /** - * Invokes the function this `JFunc_void_RecordingError` instance holds through JNI. - */ - void invoke(const RecordingError& error) const { - static const auto method = javaClassStatic()->getMethod /* error */)>("invoke"); - method(self(), JRecordingError::fromCpp(error)); - } - }; - - /** - * An implementation of Func_void_RecordingError that is backed by a C++ implementation (using `std::function<...>`) - */ - struct JFunc_void_RecordingError_cxx final: public jni::HybridClass { - public: - static jni::local_ref fromCpp(const std::function& func) { - return JFunc_void_RecordingError_cxx::newObjectCxxArgs(func); - } - - public: - /** - * Invokes the C++ `std::function<...>` this `JFunc_void_RecordingError_cxx` instance holds. - */ - void invoke_cxx(jni::alias_ref error) { - _func(error->toCpp()); - } - - public: - [[nodiscard]] - inline const std::function& getFunction() const { - return _func; - } - - public: - static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError_cxx;"; - static void registerNatives() { - registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_RecordingError_cxx::invoke_cxx)}); - } - - private: - explicit JFunc_void_RecordingError_cxx(const std::function& func): _func(func) { } - - private: - friend HybridBase; - std::function _func; - }; - -} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp index a1cb1e8..65f22dd 100644 --- a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp @@ -27,8 +27,6 @@ namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresen namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } // Forward declaration of `CameraDevice` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } -// Forward declaration of `RecordingError` to properly resolve imports. -namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "PermissionStatus.hpp" #include "JPermissionStatus.hpp" @@ -58,9 +56,6 @@ namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "CameraDevice.hpp" #include "JCameraDevice.hpp" #include "JFunc_void_ScreenRecordingFile.hpp" -#include "RecordingError.hpp" -#include "JFunc_void_RecordingError.hpp" -#include "JRecordingError.hpp" namespace margelo::nitro::nitroscreenrecorder { @@ -189,9 +184,21 @@ namespace margelo::nitro::nitroscreenrecorder { return __promise; }(); } - void JHybridNitroScreenRecorderSpec::startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) { - static const auto method = javaClassStatic()->getMethod /* onRecordingError */)>("startGlobalRecording_cxx"); - method(_javaPart, enableMic, separateAudioFile, JFunc_void_RecordingError_cxx::fromCpp(onRecordingError)); + std::shared_ptr>> JHybridNitroScreenRecorderSpec::startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) { + static const auto method = javaClassStatic()->getMethod(jboolean /* enableMic */, jboolean /* separateAudioFile */, double /* timeoutMs */)>("startGlobalRecording"); + auto __result = method(_javaPart, enableMic, separateAudioFile, timeoutMs); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result != nullptr ? std::make_optional(static_cast(__result->value())) : std::nullopt); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); } std::shared_ptr>> JHybridNitroScreenRecorderSpec::stopGlobalRecording(double settledTimeMs) { static const auto method = javaClassStatic()->getMethod(double /* settledTimeMs */)>("stopGlobalRecording"); diff --git a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp index ab5c30f..e3dbe76 100644 --- a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp @@ -65,7 +65,7 @@ namespace margelo::nitro::nitroscreenrecorder { void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) override; std::shared_ptr>> stopInAppRecording() override; std::shared_ptr> cancelInAppRecording() override; - void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) override; + std::shared_ptr>> startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) override; std::shared_ptr>> stopGlobalRecording(double settledTimeMs) override; std::optional retrieveLastGlobalRecording() override; void clearRecordingCache() override; diff --git a/nitrogen/generated/android/c++/JRecordingError.hpp b/nitrogen/generated/android/c++/JRecordingError.hpp deleted file mode 100644 index ecbb2f7..0000000 --- a/nitrogen/generated/android/c++/JRecordingError.hpp +++ /dev/null @@ -1,61 +0,0 @@ -/// -/// JRecordingError.hpp -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright ยฉ 2025 Marc Rousavy @ Margelo -/// - -#pragma once - -#include -#include "RecordingError.hpp" - -#include - -namespace margelo::nitro::nitroscreenrecorder { - - using namespace facebook; - - /** - * The C++ JNI bridge between the C++ struct "RecordingError" and the the Kotlin data class "RecordingError". - */ - struct JRecordingError final: public jni::JavaClass { - public: - static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/RecordingError;"; - - public: - /** - * Convert this Java/Kotlin-based struct to the C++ struct RecordingError by copying all values to C++. - */ - [[maybe_unused]] - [[nodiscard]] - RecordingError toCpp() const { - static const auto clazz = javaClassStatic(); - static const auto fieldName = clazz->getField("name"); - jni::local_ref name = this->getFieldValue(fieldName); - static const auto fieldMessage = clazz->getField("message"); - jni::local_ref message = this->getFieldValue(fieldMessage); - return RecordingError( - name->toStdString(), - message->toStdString() - ); - } - - public: - /** - * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. - */ - [[maybe_unused]] - static jni::local_ref fromCpp(const RecordingError& value) { - using JSignature = JRecordingError(jni::alias_ref, jni::alias_ref); - static const auto clazz = javaClassStatic(); - static const auto create = clazz->getStaticMethod("fromCpp"); - return create( - clazz, - jni::make_jstring(value.name), - jni::make_jstring(value.message) - ); - } - }; - -} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt deleted file mode 100644 index 2256914..0000000 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt +++ /dev/null @@ -1,80 +0,0 @@ -/// -/// Func_void_RecordingError.kt -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright ยฉ 2025 Marc Rousavy @ Margelo -/// - -package com.margelo.nitro.nitroscreenrecorder - -import androidx.annotation.Keep -import com.facebook.jni.HybridData -import com.facebook.proguard.annotations.DoNotStrip -import dalvik.annotation.optimization.FastNative - - -/** - * Represents the JavaScript callback `(error: struct) => void`. - * This can be either implemented in C++ (in which case it might be a callback coming from JS), - * or in Kotlin/Java (in which case it is a native callback). - */ -@DoNotStrip -@Keep -@Suppress("ClassName", "RedundantUnitReturnType") -fun interface Func_void_RecordingError: (RecordingError) -> Unit { - /** - * Call the given JS callback. - * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. - */ - @DoNotStrip - @Keep - override fun invoke(error: RecordingError): Unit -} - -/** - * Represents the JavaScript callback `(error: struct) => void`. - * This is implemented in C++, via a `std::function<...>`. - * The callback might be coming from JS. - */ -@DoNotStrip -@Keep -@Suppress( - "KotlinJniMissingFunction", "unused", - "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", - "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", -) -class Func_void_RecordingError_cxx: Func_void_RecordingError { - @DoNotStrip - @Keep - private val mHybridData: HybridData - - @DoNotStrip - @Keep - private constructor(hybridData: HybridData) { - mHybridData = hybridData - } - - @DoNotStrip - @Keep - override fun invoke(error: RecordingError): Unit - = invoke_cxx(error) - - @FastNative - private external fun invoke_cxx(error: RecordingError): Unit -} - -/** - * Represents the JavaScript callback `(error: struct) => void`. - * This is implemented in Java/Kotlin, via a `(RecordingError) -> Unit`. - * The callback is always coming from native. - */ -@DoNotStrip -@Keep -@Suppress("ClassName", "RedundantUnitReturnType", "unused") -class Func_void_RecordingError_java(private val function: (RecordingError) -> Unit): Func_void_RecordingError { - @DoNotStrip - @Keep - override fun invoke(error: RecordingError): Unit { - return this.function(error) - } -} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt index 5bbd299..6e4376e 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt @@ -105,14 +105,9 @@ abstract class HybridNitroScreenRecorderSpec: HybridObject() { @Keep abstract fun cancelInAppRecording(): Promise - abstract fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: (error: RecordingError) -> Unit): Unit - @DoNotStrip @Keep - private fun startGlobalRecording_cxx(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: Func_void_RecordingError): Unit { - val __result = startGlobalRecording(enableMic, separateAudioFile, onRecordingError) - return __result - } + abstract fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, timeoutMs: Double): Promise @DoNotStrip @Keep diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt deleted file mode 100644 index 7def6fa..0000000 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt +++ /dev/null @@ -1,41 +0,0 @@ -/// -/// RecordingError.kt -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright ยฉ 2025 Marc Rousavy @ Margelo -/// - -package com.margelo.nitro.nitroscreenrecorder - -import androidx.annotation.Keep -import com.facebook.proguard.annotations.DoNotStrip - - -/** - * Represents the JavaScript object/struct "RecordingError". - */ -@DoNotStrip -@Keep -data class RecordingError( - @DoNotStrip - @Keep - val name: String, - @DoNotStrip - @Keep - val message: String -) { - /* primary constructor */ - - private companion object { - /** - * Constructor called from C++ - */ - @DoNotStrip - @Keep - @Suppress("unused") - @JvmStatic - private fun fromCpp(name: String, message: String): RecordingError { - return RecordingError(name, message) - } - } -} diff --git a/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp b/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp index a7bd57b..2666610 100644 --- a/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp +++ b/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp @@ -19,7 +19,6 @@ #include "JFunc_void_ScreenRecordingEvent.hpp" #include "JFunc_void_BroadcastPickerPresentationEvent.hpp" #include "JFunc_void_ScreenRecordingFile.hpp" -#include "JFunc_void_RecordingError.hpp" #include namespace margelo::nitro::nitroscreenrecorder { @@ -35,7 +34,6 @@ int initialize(JavaVM* vm) { margelo::nitro::nitroscreenrecorder::JFunc_void_ScreenRecordingEvent_cxx::registerNatives(); margelo::nitro::nitroscreenrecorder::JFunc_void_BroadcastPickerPresentationEvent_cxx::registerNatives(); margelo::nitro::nitroscreenrecorder::JFunc_void_ScreenRecordingFile_cxx::registerNatives(); - margelo::nitro::nitroscreenrecorder::JFunc_void_RecordingError_cxx::registerNatives(); // Register Nitro Hybrid Objects HybridObjectRegistry::registerHybridObjectConstructor( diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp index 99d48e8..eeff26c 100644 --- a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp @@ -69,11 +69,11 @@ namespace margelo::nitro::nitroscreenrecorder::bridge::swift { }; } - // pragma MARK: std::function - Func_void_RecordingError create_Func_void_RecordingError(void* NON_NULL swiftClosureWrapper) noexcept { - auto swiftClosure = NitroScreenRecorder::Func_void_RecordingError::fromUnsafe(swiftClosureWrapper); - return [swiftClosure = std::move(swiftClosure)](const RecordingError& error) mutable -> void { - swiftClosure.call(error); + // pragma MARK: std::function /* result */)> + Func_void_std__optional_bool_ create_Func_void_std__optional_bool_(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_std__optional_bool_::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](std::optional result) mutable -> void { + swiftClosure.call(result); }; } diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp index fc95ffb..5c2622a 100644 --- a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp @@ -18,8 +18,6 @@ namespace margelo::nitro::nitroscreenrecorder { class HybridNitroScreenRecorderS namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } // Forward declaration of `PermissionStatus` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } -// Forward declaration of `RecordingError` to properly resolve imports. -namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } // Forward declaration of `RecordingEventReason` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } // Forward declaration of `RecordingEventType` to properly resolve imports. @@ -39,7 +37,6 @@ namespace NitroScreenRecorder { class HybridNitroScreenRecorderSpec_cxx; } #include "HybridNitroScreenRecorderSpec.hpp" #include "PermissionResponse.hpp" #include "PermissionStatus.hpp" -#include "RecordingError.hpp" #include "RecordingEventReason.hpp" #include "RecordingEventType.hpp" #include "ScreenRecordingEvent.hpp" @@ -294,26 +291,53 @@ namespace margelo::nitro::nitroscreenrecorder::bridge::swift { return Func_void_Wrapper(std::move(value)); } - // pragma MARK: std::function + // pragma MARK: std::optional /** - * Specialized version of `std::function`. + * Specialized version of `std::optional`. */ - using Func_void_RecordingError = std::function; + using std__optional_bool_ = std::optional; + inline std::optional create_std__optional_bool_(const bool& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_bool_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline bool get_std__optional_bool_(const std::optional& optional) noexcept { + return *optional; + } + + // pragma MARK: std::shared_ptr>> + /** + * Specialized version of `std::shared_ptr>>`. + */ + using std__shared_ptr_Promise_std__optional_bool___ = std::shared_ptr>>; + inline std::shared_ptr>> create_std__shared_ptr_Promise_std__optional_bool___() noexcept { + return Promise>::create(); + } + inline PromiseHolder> wrap_std__shared_ptr_Promise_std__optional_bool___(std::shared_ptr>> promise) noexcept { + return PromiseHolder>(std::move(promise)); + } + + // pragma MARK: std::function /* result */)> /** - * Wrapper class for a `std::function`, this can be used from Swift. + * Specialized version of `std::function)>`. */ - class Func_void_RecordingError_Wrapper final { + using Func_void_std__optional_bool_ = std::function /* result */)>; + /** + * Wrapper class for a `std::function / * result * /)>`, this can be used from Swift. + */ + class Func_void_std__optional_bool__Wrapper final { public: - explicit Func_void_RecordingError_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} - inline void call(RecordingError error) const noexcept { - _function->operator()(error); + explicit Func_void_std__optional_bool__Wrapper(std::function /* result */)>&& func): _function(std::make_unique /* result */)>>(std::move(func))) {} + inline void call(std::optional result) const noexcept { + _function->operator()(result); } private: - std::unique_ptr> _function; + std::unique_ptr /* result */)>> _function; } SWIFT_NONCOPYABLE; - Func_void_RecordingError create_Func_void_RecordingError(void* NON_NULL swiftClosureWrapper) noexcept; - inline Func_void_RecordingError_Wrapper wrap_Func_void_RecordingError(Func_void_RecordingError value) noexcept { - return Func_void_RecordingError_Wrapper(std::move(value)); + Func_void_std__optional_bool_ create_Func_void_std__optional_bool_(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_std__optional_bool__Wrapper wrap_Func_void_std__optional_bool_(Func_void_std__optional_bool_ value) noexcept { + return Func_void_std__optional_bool__Wrapper(std::move(value)); } // pragma MARK: std::shared_ptr @@ -382,6 +406,15 @@ namespace margelo::nitro::nitroscreenrecorder::bridge::swift { return Result>>::withError(error); } + // pragma MARK: Result>>> + using Result_std__shared_ptr_Promise_std__optional_bool____ = Result>>>; + inline Result_std__shared_ptr_Promise_std__optional_bool____ create_Result_std__shared_ptr_Promise_std__optional_bool____(const std::shared_ptr>>& value) noexcept { + return Result>>>::withValue(value); + } + inline Result_std__shared_ptr_Promise_std__optional_bool____ create_Result_std__shared_ptr_Promise_std__optional_bool____(const std::exception_ptr& error) noexcept { + return Result>>>::withError(error); + } + // pragma MARK: Result> using Result_std__optional_ScreenRecordingFile__ = Result>; inline Result_std__optional_ScreenRecordingFile__ create_Result_std__optional_ScreenRecordingFile__(const std::optional& value) noexcept { diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp index 4b6bef5..f21d364 100644 --- a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp @@ -22,8 +22,6 @@ namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } // Forward declaration of `RecorderCameraStyle` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } -// Forward declaration of `RecordingError` to properly resolve imports. -namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } // Forward declaration of `RecordingEventReason` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } // Forward declaration of `RecordingEventType` to properly resolve imports. @@ -41,7 +39,6 @@ namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } #include "PermissionResponse.hpp" #include "PermissionStatus.hpp" #include "RecorderCameraStyle.hpp" -#include "RecordingError.hpp" #include "RecordingEventReason.hpp" #include "RecordingEventType.hpp" #include "ScreenRecordingEvent.hpp" diff --git a/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp index fa7d5d7..087d3d7 100644 --- a/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp @@ -32,8 +32,6 @@ namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } // Forward declaration of `AudioRecordingFile` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } -// Forward declaration of `RecordingError` to properly resolve imports. -namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "PermissionStatus.hpp" #include "PermissionResponse.hpp" @@ -49,7 +47,6 @@ namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "ScreenRecordingFile.hpp" #include #include "AudioRecordingFile.hpp" -#include "RecordingError.hpp" #include "NitroScreenRecorder-Swift-Cxx-Umbrella.hpp" @@ -177,11 +174,13 @@ namespace margelo::nitro::nitroscreenrecorder { auto __value = std::move(__result.value()); return __value; } - inline void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) override { - auto __result = _swiftPart.startGlobalRecording(std::forward(enableMic), std::forward(separateAudioFile), onRecordingError); + inline std::shared_ptr>> startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) override { + auto __result = _swiftPart.startGlobalRecording(std::forward(enableMic), std::forward(separateAudioFile), std::forward(timeoutMs)); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); } + auto __value = std::move(__result.value()); + return __value; } inline std::shared_ptr>> stopGlobalRecording(double settledTimeMs) override { auto __result = _swiftPart.stopGlobalRecording(std::forward(settledTimeMs)); diff --git a/nitrogen/generated/ios/swift/Func_void_RecordingError.swift b/nitrogen/generated/ios/swift/Func_void_std__optional_bool_.swift similarity index 53% rename from nitrogen/generated/ios/swift/Func_void_RecordingError.swift rename to nitrogen/generated/ios/swift/Func_void_std__optional_bool_.swift index 4d11201..beb4f00 100644 --- a/nitrogen/generated/ios/swift/Func_void_RecordingError.swift +++ b/nitrogen/generated/ios/swift/Func_void_std__optional_bool_.swift @@ -1,5 +1,5 @@ /// -/// Func_void_RecordingError.swift +/// Func_void_std__optional_bool_.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro /// Copyright ยฉ 2025 Marc Rousavy @ Margelo @@ -9,21 +9,28 @@ import NitroModules /** - * Wraps a Swift `(_ error: RecordingError) -> Void` as a class. + * Wraps a Swift `(_ value: Bool?) -> Void` as a class. * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. */ -public final class Func_void_RecordingError { +public final class Func_void_std__optional_bool_ { public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift - private let closure: (_ error: RecordingError) -> Void + private let closure: (_ value: Bool?) -> Void - public init(_ closure: @escaping (_ error: RecordingError) -> Void) { + public init(_ closure: @escaping (_ value: Bool?) -> Void) { self.closure = closure } @inline(__always) - public func call(error: RecordingError) -> Void { - self.closure(error) + public func call(value: bridge.std__optional_bool_) -> Void { + self.closure({ () -> Bool? in + if bridge.has_value_std__optional_bool_(value) { + let __unwrapped = bridge.get_std__optional_bool_(value) + return __unwrapped + } else { + return nil + } + }()) } /** @@ -36,12 +43,12 @@ public final class Func_void_RecordingError { } /** - * Casts an unsafe pointer to a `Func_void_RecordingError`. - * The pointer has to be a retained opaque `Unmanaged`. + * Casts an unsafe pointer to a `Func_void_std__optional_bool_`. + * The pointer has to be a retained opaque `Unmanaged`. * This removes one strong reference from the object! */ @inline(__always) - public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_RecordingError { - return Unmanaged.fromOpaque(pointer).takeRetainedValue() + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__optional_bool_ { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() } } diff --git a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift index e0947f0..608832f 100644 --- a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift +++ b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift @@ -26,7 +26,7 @@ public protocol HybridNitroScreenRecorderSpec_protocol: HybridObject { func startInAppRecording(enableMic: Bool, enableCamera: Bool, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: Bool, onRecordingFinished: @escaping (_ file: ScreenRecordingFile) -> Void) throws -> Void func stopInAppRecording() throws -> Promise func cancelInAppRecording() throws -> Promise - func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (_ error: RecordingError) -> Void) throws -> Void + func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, timeoutMs: Double) throws -> Promise func stopGlobalRecording(settledTimeMs: Double) throws -> Promise func retrieveLastGlobalRecording() throws -> ScreenRecordingFile? func clearRecordingCache() throws -> Void diff --git a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift index 50e2a0f..cd79d83 100644 --- a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift @@ -297,18 +297,27 @@ open class HybridNitroScreenRecorderSpec_cxx { } @inline(__always) - public final func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: bridge.Func_void_RecordingError) -> bridge.Result_void_ { + public final func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, timeoutMs: Double) -> bridge.Result_std__shared_ptr_Promise_std__optional_bool____ { do { - try self.__implementation.startGlobalRecording(enableMic: enableMic, separateAudioFile: separateAudioFile, onRecordingError: { () -> (RecordingError) -> Void in - let __wrappedFunction = bridge.wrap_Func_void_RecordingError(onRecordingError) - return { (__error: RecordingError) -> Void in - __wrappedFunction.call(__error) - } - }()) - return bridge.create_Result_void_() + let __result = try self.__implementation.startGlobalRecording(enableMic: enableMic, separateAudioFile: separateAudioFile, timeoutMs: timeoutMs) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__optional_bool___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__optional_bool___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__optional_bool___(__promise) + __result + .then({ __result in __promiseHolder.resolve({ () -> bridge.std__optional_bool_ in + if let __unwrappedValue = __result { + return bridge.create_std__optional_bool_(__unwrappedValue) + } else { + return .init() + } + }()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__optional_bool____(__resultCpp) } catch (let __error) { let __exceptionPtr = __error.toCpp() - return bridge.create_Result_void_(__exceptionPtr) + return bridge.create_Result_std__shared_ptr_Promise_std__optional_bool____(__exceptionPtr) } } diff --git a/nitrogen/generated/ios/swift/RecordingError.swift b/nitrogen/generated/ios/swift/RecordingError.swift deleted file mode 100644 index d46c945..0000000 --- a/nitrogen/generated/ios/swift/RecordingError.swift +++ /dev/null @@ -1,46 +0,0 @@ -/// -/// RecordingError.swift -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright ยฉ 2025 Marc Rousavy @ Margelo -/// - -import NitroModules - -/** - * Represents an instance of `RecordingError`, backed by a C++ struct. - */ -public typealias RecordingError = margelo.nitro.nitroscreenrecorder.RecordingError - -public extension RecordingError { - private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift - - /** - * Create a new instance of `RecordingError`. - */ - init(name: String, message: String) { - self.init(std.string(name), std.string(message)) - } - - var name: String { - @inline(__always) - get { - return String(self.__name) - } - @inline(__always) - set { - self.__name = std.string(newValue) - } - } - - var message: String { - @inline(__always) - get { - return String(self.__message) - } - @inline(__always) - set { - self.__message = std.string(newValue) - } - } -} diff --git a/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp index d3a2b5e..c1d1c4e 100644 --- a/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp @@ -27,8 +27,6 @@ namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } // Forward declaration of `ScreenRecordingFile` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } -// Forward declaration of `RecordingError` to properly resolve imports. -namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "PermissionStatus.hpp" #include "PermissionResponse.hpp" @@ -40,7 +38,6 @@ namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "CameraDevice.hpp" #include "ScreenRecordingFile.hpp" #include -#include "RecordingError.hpp" namespace margelo::nitro::nitroscreenrecorder { @@ -84,7 +81,7 @@ namespace margelo::nitro::nitroscreenrecorder { virtual void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) = 0; virtual std::shared_ptr>> stopInAppRecording() = 0; virtual std::shared_ptr> cancelInAppRecording() = 0; - virtual void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) = 0; + virtual std::shared_ptr>> startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) = 0; virtual std::shared_ptr>> stopGlobalRecording(double settledTimeMs) = 0; virtual std::optional retrieveLastGlobalRecording() = 0; virtual void clearRecordingCache() = 0; diff --git a/nitrogen/generated/shared/c++/RecordingError.hpp b/nitrogen/generated/shared/c++/RecordingError.hpp deleted file mode 100644 index f499c5c..0000000 --- a/nitrogen/generated/shared/c++/RecordingError.hpp +++ /dev/null @@ -1,79 +0,0 @@ -/// -/// RecordingError.hpp -/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. -/// https://github.com/mrousavy/nitro -/// Copyright ยฉ 2025 Marc Rousavy @ Margelo -/// - -#pragma once - -#if __has_include() -#include -#else -#error NitroModules cannot be found! Are you sure you installed NitroModules properly? -#endif -#if __has_include() -#include -#else -#error NitroModules cannot be found! Are you sure you installed NitroModules properly? -#endif -#if __has_include() -#include -#else -#error NitroModules cannot be found! Are you sure you installed NitroModules properly? -#endif - - - -#include - -namespace margelo::nitro::nitroscreenrecorder { - - /** - * A struct which can be represented as a JavaScript object (RecordingError). - */ - struct RecordingError { - public: - std::string name SWIFT_PRIVATE; - std::string message SWIFT_PRIVATE; - - public: - RecordingError() = default; - explicit RecordingError(std::string name, std::string message): name(name), message(message) {} - }; - -} // namespace margelo::nitro::nitroscreenrecorder - -namespace margelo::nitro { - - // C++ RecordingError <> JS RecordingError (object) - template <> - struct JSIConverter final { - static inline margelo::nitro::nitroscreenrecorder::RecordingError fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { - jsi::Object obj = arg.asObject(runtime); - return margelo::nitro::nitroscreenrecorder::RecordingError( - JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "name")), - JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "message")) - ); - } - static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::RecordingError& arg) { - jsi::Object obj(runtime); - obj.setProperty(runtime, "name", JSIConverter::toJSI(runtime, arg.name)); - obj.setProperty(runtime, "message", JSIConverter::toJSI(runtime, arg.message)); - return obj; - } - static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { - if (!value.isObject()) { - return false; - } - jsi::Object obj = value.getObject(runtime); - if (!nitro::isPlainObject(runtime, obj)) { - return false; - } - if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "name"))) return false; - if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "message"))) return false; - return true; - } - }; - -} // namespace margelo::nitro From a7247efe336213adbccf2de631be824e28a74ce4 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 9 Dec 2025 15:14:57 -0800 Subject: [PATCH 12/32] chore: bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b86f6a1..5fa8d26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.9.0", + "version": "0.9.1", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", From 561c560694ed06c62ed71905211ce35d48bd68f8 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 9 Dec 2025 15:36:09 -0800 Subject: [PATCH 13/32] fix: mmaybe didnt work --- .../NitroScreenRecorder.kt | 15 +++--- ios/NitroScreenRecorder.swift | 54 +++++++++++++------ package.json | 2 +- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt index c9bcb16..b879e70 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt @@ -338,16 +338,19 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { putExtra(ScreenRecordingService.EXTRA_SEPARATE_AUDIO, separateAudioFile) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ctx.startForegroundService(startIntent) - } else { - ctx.startService(startIntent) - } - // Wait for recording to start (or timeout) + // IMPORTANT: Set up continuation BEFORE starting the service to avoid race condition kotlinx.coroutines.withTimeoutOrNull(timeoutMs.toLong()) { suspendCancellableCoroutine { cont -> + // Set up continuation first globalRecordingStartContinuation = cont + + // NOW start the service (after continuation is ready) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(startIntent) + } else { + ctx.startService(startIntent) + } } } } catch (e: Exception) { diff --git a/ios/NitroScreenRecorder.swift b/ios/NitroScreenRecorder.swift index 3ef5070..37a736e 100644 --- a/ios/NitroScreenRecorder.swift +++ b/ios/NitroScreenRecorder.swift @@ -174,6 +174,9 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { isGlobalRecordingActive = true // Resolve the promise if we were waiting for global recording to start + print( + "๐ŸŽฌ Global recording detected - initiated by us: \(globalRecordingInitiatedByThisPackage), continuation exists: \(globalRecordingContinuation != nil)" + ) if globalRecordingInitiatedByThisPackage { isBroadcastModalShowing = false // Modal is gone once recording starts resolveGlobalRecordingPromise(with: true) @@ -563,12 +566,18 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { /// Helper to resolve the global recording promise and clean up private func resolveGlobalRecordingPromise(with result: Bool?) { + print( + "๐Ÿ”„ resolveGlobalRecordingPromise called with: \(String(describing: result)), continuation exists: \(globalRecordingContinuation != nil)" + ) globalRecordingTimeoutTask?.cancel() globalRecordingTimeoutTask = nil if let continuation = globalRecordingContinuation { globalRecordingContinuation = nil + print("โœ… Resuming continuation with: \(String(describing: result))") continuation.resume(returning: result) + } else { + print("โš ๏ธ No continuation to resume!") } } @@ -612,31 +621,42 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { // Mark that we initiated this recording globalRecordingInitiatedByThisPackage = true - // Present the broadcast picker - presentGlobalBroadcastModal(enableMicrophone: enableMic) + // Capture enableMic for use in async context + let enableMicCapture = enableMic + let timeoutMsCapture = timeoutMs return Promise.async { [weak self] in guard let self = self else { return nil } return await withCheckedContinuation { continuation in - self.globalRecordingContinuation = continuation - - // Set up timeout - self.globalRecordingTimeoutTask = Task { - do { - try await Task.sleep(nanoseconds: UInt64(timeoutMs * 1_000_000)) - // If we get here, timeout occurred - await MainActor.run { - if self.globalRecordingContinuation != nil { - print("โฑ๏ธ Global recording start timed out after \(timeoutMs)ms") - self.isBroadcastModalShowing = false - self.globalRecordingInitiatedByThisPackage = false - self.resolveGlobalRecordingPromise(with: nil) + // Dispatch to main thread to ensure thread safety with handleScreenRecordingChange + // and to properly present the modal + DispatchQueue.main.async { + // IMPORTANT: Set up continuation FIRST, before presenting the modal + // This prevents race condition where recording starts before continuation is ready + print("๐Ÿ“ Setting up globalRecordingContinuation on main thread") + self.globalRecordingContinuation = continuation + + // Set up timeout + self.globalRecordingTimeoutTask = Task { + do { + try await Task.sleep(nanoseconds: UInt64(timeoutMsCapture * 1_000_000)) + // If we get here, timeout occurred + await MainActor.run { + if self.globalRecordingContinuation != nil { + print("โฑ๏ธ Global recording start timed out after \(timeoutMsCapture)ms") + self.isBroadcastModalShowing = false + self.globalRecordingInitiatedByThisPackage = false + self.resolveGlobalRecordingPromise(with: nil) + } } + } catch { + // Task was cancelled, which is expected when recording starts or modal dismissed } - } catch { - // Task was cancelled, which is expected when recording starts or modal dismissed } + + // NOW present the broadcast picker (after continuation is set up) + self.presentGlobalBroadcastModal(enableMicrophone: enableMicCapture) } } } diff --git a/package.json b/package.json index 5fa8d26..59002d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.9.1", + "version": "0.9.2", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", From 7e72ed8132614bc2d7e16f797fe3a3d12b0cbb4f Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 9 Dec 2025 15:59:03 -0800 Subject: [PATCH 14/32] fix: revert --- .../NitroScreenRecorder.kt | 87 ++++++--------- ios/NitroScreenRecorder.swift | 103 ++++-------------- lib/commonjs/functions.js | 37 ++----- lib/commonjs/functions.js.map | 2 +- lib/module/functions.js | 37 ++----- lib/module/functions.js.map | 2 +- lib/typescript/NitroScreenRecorder.nitro.d.ts | 15 +-- .../NitroScreenRecorder.nitro.d.ts.map | 2 +- lib/typescript/functions.d.ts | 27 +---- lib/typescript/functions.d.ts.map | 2 +- lib/typescript/types.d.ts | 21 +--- lib/typescript/types.d.ts.map | 2 +- .../android/c++/JFunc_void_RecordingError.hpp | 77 +++++++++++++ .../c++/JHybridNitroScreenRecorderSpec.cpp | 23 ++-- .../c++/JHybridNitroScreenRecorderSpec.hpp | 2 +- .../generated/android/c++/JRecordingError.hpp | 61 +++++++++++ .../Func_void_RecordingError.kt | 80 ++++++++++++++ .../HybridNitroScreenRecorderSpec.kt | 7 +- .../nitroscreenrecorder/RecordingError.kt | 41 +++++++ .../android/nitroscreenrecorderOnLoad.cpp | 2 + .../NitroScreenRecorder-Swift-Cxx-Bridge.cpp | 10 +- .../NitroScreenRecorder-Swift-Cxx-Bridge.hpp | 63 +++-------- ...NitroScreenRecorder-Swift-Cxx-Umbrella.hpp | 3 + .../HybridNitroScreenRecorderSpecSwift.hpp | 9 +- ..._.swift => Func_void_RecordingError.swift} | 29 ++--- .../swift/HybridNitroScreenRecorderSpec.swift | 2 +- .../HybridNitroScreenRecorderSpec_cxx.swift | 27 ++--- .../generated/ios/swift/RecordingError.swift | 46 ++++++++ .../c++/HybridNitroScreenRecorderSpec.hpp | 5 +- .../generated/shared/c++/RecordingError.hpp | 79 ++++++++++++++ package.json | 2 +- src/NitroScreenRecorder.nitro.ts | 16 +-- src/functions.ts | 41 ++----- src/types.ts | 21 +--- 34 files changed, 554 insertions(+), 429 deletions(-) create mode 100644 nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp create mode 100644 nitrogen/generated/android/c++/JRecordingError.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt rename nitrogen/generated/ios/swift/{Func_void_std__optional_bool_.swift => Func_void_RecordingError.swift} (53%) create mode 100644 nitrogen/generated/ios/swift/RecordingError.swift create mode 100644 nitrogen/generated/shared/c++/RecordingError.hpp diff --git a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt index b879e70..c469554 100644 --- a/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt +++ b/android/src/main/java/com/margelo/nitro/nitroscreenrecorder/NitroScreenRecorder.kt @@ -19,7 +19,6 @@ import com.margelo.nitro.core.* import com.margelo.nitro.nitroscreenrecorder.utils.RecorderUtils import java.io.File import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull import kotlin.coroutines.resume import kotlinx.coroutines.delay @@ -35,7 +34,7 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { private var isServiceBound = false private var lastGlobalRecording: File? = null private var lastGlobalAudioRecording: File? = null - private var globalRecordingStartContinuation: kotlin.coroutines.Continuation? = null + private var globalRecordingErrorCallback: ((RecordingError) -> Unit)? = null private val screenRecordingListeners = mutableListOf Unit>>() @@ -62,12 +61,6 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { "๐Ÿ”” notifyGlobalRecordingEvent called with type: ${event.type}, reason: ${event.reason}" ) instance?.notifyListeners(event) - - // Resolve the start promise when recording begins - if (event.type == RecordingEventType.GLOBAL && event.reason == RecordingEventReason.BEGAN) { - instance?.globalRecordingStartContinuation?.resume(true) - instance?.globalRecordingStartContinuation = null - } } fun notifyGlobalRecordingFinished( @@ -89,9 +82,7 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { TAG, "โŒ notifyGlobalRecordingError called with error: ${error.name} - ${error.message}" ) - // Resolve the start promise with null (recording failed to start) - instance?.globalRecordingStartContinuation?.resume(null) - instance?.globalRecordingStartContinuation = null + instance?.globalRecordingErrorCallback?.invoke(error) } } @@ -310,55 +301,43 @@ class NitroScreenRecorder : HybridNitroScreenRecorderSpec() { // --- Global Recording Methods --- - override fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, timeoutMs: Double): Promise = - Promise.async { - if (globalRecordingService?.isCurrentlyRecording() == true) { - Log.w(TAG, "โš ๏ธ Global recording already in progress") - throw Error("BROADCAST_ALREADY_ACTIVE: A screen recording session is already in progress.") - } - val ctx = NitroModules.applicationContext ?: throw Error("NO_CONTEXT") - - // Cancel any existing pending continuation - globalRecordingStartContinuation?.resume(null) - globalRecordingStartContinuation = null + override fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: (RecordingError) -> Unit) { + if (globalRecordingService?.isCurrentlyRecording() == true) { + Log.w(TAG, "โš ๏ธ Global recording already in progress") + return + } + val ctx = NitroModules.applicationContext ?: throw Error("NO_CONTEXT") - try { - val (resultCode, resultData) = requestGlobalRecordingPermission().await() + // Store the error callback so it can be used by the service + globalRecordingErrorCallback = onRecordingError - if (!isServiceBound) { - val serviceIntent = Intent(ctx, ScreenRecordingService::class.java) - ctx.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE) - } + requestGlobalRecordingPermission().then { (resultCode, resultData) -> + if (!isServiceBound) { + val serviceIntent = Intent(ctx, ScreenRecordingService::class.java) + ctx.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE) + } - val startIntent = Intent(ctx, ScreenRecordingService::class.java).apply { - action = ScreenRecordingService.ACTION_START_RECORDING - putExtra(ScreenRecordingService.EXTRA_RESULT_CODE, resultCode) - putExtra(ScreenRecordingService.EXTRA_RESULT_DATA, resultData) - putExtra(ScreenRecordingService.EXTRA_ENABLE_MIC, enableMic) - putExtra(ScreenRecordingService.EXTRA_SEPARATE_AUDIO, separateAudioFile) - } + val startIntent = Intent(ctx, ScreenRecordingService::class.java).apply { + action = ScreenRecordingService.ACTION_START_RECORDING + putExtra(ScreenRecordingService.EXTRA_RESULT_CODE, resultCode) + putExtra(ScreenRecordingService.EXTRA_RESULT_DATA, resultData) + putExtra(ScreenRecordingService.EXTRA_ENABLE_MIC, enableMic) + putExtra(ScreenRecordingService.EXTRA_SEPARATE_AUDIO, separateAudioFile) + } - // Wait for recording to start (or timeout) - // IMPORTANT: Set up continuation BEFORE starting the service to avoid race condition - kotlinx.coroutines.withTimeoutOrNull(timeoutMs.toLong()) { - suspendCancellableCoroutine { cont -> - // Set up continuation first - globalRecordingStartContinuation = cont - - // NOW start the service (after continuation is ready) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ctx.startForegroundService(startIntent) - } else { - ctx.startService(startIntent) - } - } - } - } catch (e: Exception) { - Log.e(TAG, "โŒ Failed to start global recording: ${e.message}") - // User denied permission or other error - return null (cancelled) - null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ctx.startForegroundService(startIntent) + } else { + ctx.startService(startIntent) } + }.catch { error -> + val recordingError = RecordingError( + name = "GlobalRecordingStartError", + message = error.message ?: "Failed to start global recording" + ) + onRecordingError(recordingError) // Use the callback parameter directly } + } override fun stopGlobalRecording(settledTimeMs: Double): Promise { return Promise.async { diff --git a/ios/NitroScreenRecorder.swift b/ios/NitroScreenRecorder.swift index 37a736e..a8d69cf 100644 --- a/ios/NitroScreenRecorder.swift +++ b/ios/NitroScreenRecorder.swift @@ -43,10 +43,6 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { private var isBroadcastModalShowing: Bool = false private var appStateObservers: [NSObjectProtocol] = [] - // Promise continuation for startGlobalRecording - private var globalRecordingContinuation: CheckedContinuation? - private var globalRecordingTimeoutTask: Task? - override init() { super.init() registerListener() @@ -154,11 +150,6 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { // Notify all listeners that the modal was dismissed broadcastPickerEventListeners.forEach { $0.callback(.dismissed) } - - // If we have a pending continuation and recording didn't start, resolve with nil (user cancelled) - if !UIScreen.main.isCaptured { - resolveGlobalRecordingPromise(with: nil) - } } @objc private func handleScreenRecordingChange() { @@ -172,15 +163,6 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } else { type = .global isGlobalRecordingActive = true - - // Resolve the promise if we were waiting for global recording to start - print( - "๐ŸŽฌ Global recording detected - initiated by us: \(globalRecordingInitiatedByThisPackage), continuation exists: \(globalRecordingContinuation != nil)" - ) - if globalRecordingInitiatedByThisPackage { - isBroadcastModalShowing = false // Modal is gone once recording starts - resolveGlobalRecordingPromise(with: true) - } } } else { reason = .ended @@ -564,102 +546,55 @@ class NitroScreenRecorder: HybridNitroScreenRecorderSpec { } } - /// Helper to resolve the global recording promise and clean up - private func resolveGlobalRecordingPromise(with result: Bool?) { - print( - "๐Ÿ”„ resolveGlobalRecordingPromise called with: \(String(describing: result)), continuation exists: \(globalRecordingContinuation != nil)" - ) - globalRecordingTimeoutTask?.cancel() - globalRecordingTimeoutTask = nil - - if let continuation = globalRecordingContinuation { - globalRecordingContinuation = nil - print("โœ… Resuming continuation with: \(String(describing: result))") - continuation.resume(returning: result) - } else { - print("โš ๏ธ No continuation to resume!") - } - } - func startGlobalRecording( - enableMic: Bool, separateAudioFile: Bool, timeoutMs: Double - ) throws -> Promise { - // Validate not already recording + enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (RecordingError) -> Void + ) + throws + { guard !isGlobalRecordingActive else { - throw RecorderError.error( + print("โš ๏ธ Attempted to start a global recording, but one is already active.") + let error = RecordingError( name: "BROADCAST_ALREADY_ACTIVE", message: "A screen recording session is already in progress." ) + onRecordingError(error) + return } - // Cancel any existing pending promise - resolveGlobalRecordingPromise(with: nil) - // Validate that we can access the app group (needed for global recordings) guard let appGroupId = try? getAppGroupIdentifier() else { - throw RecorderError.error( + let error = RecordingError( name: "APP_GROUP_ACCESS_FAILED", message: "Could not access app group identifier required for global recording. Something is wrong with your entitlements." ) + onRecordingError(error) + return } guard FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: appGroupId) != nil else { - throw RecorderError.error( + let error = RecordingError( name: "APP_GROUP_CONTAINER_FAILED", message: "Could not access app group container required for global recording. Something is wrong with your entitlements." ) + onRecordingError(error) + return } // Store the separateAudioFile preference for the broadcast extension to read self.separateAudioFileEnabled = separateAudioFile UserDefaults(suiteName: appGroupId)?.set(separateAudioFile, forKey: "SeparateAudioFileEnabled") - // Mark that we initiated this recording - globalRecordingInitiatedByThisPackage = true - - // Capture enableMic for use in async context - let enableMicCapture = enableMic - let timeoutMsCapture = timeoutMs - - return Promise.async { [weak self] in - guard let self = self else { return nil } + // Present the broadcast picker + presentGlobalBroadcastModal(enableMicrophone: enableMic) - return await withCheckedContinuation { continuation in - // Dispatch to main thread to ensure thread safety with handleScreenRecordingChange - // and to properly present the modal - DispatchQueue.main.async { - // IMPORTANT: Set up continuation FIRST, before presenting the modal - // This prevents race condition where recording starts before continuation is ready - print("๐Ÿ“ Setting up globalRecordingContinuation on main thread") - self.globalRecordingContinuation = continuation - - // Set up timeout - self.globalRecordingTimeoutTask = Task { - do { - try await Task.sleep(nanoseconds: UInt64(timeoutMsCapture * 1_000_000)) - // If we get here, timeout occurred - await MainActor.run { - if self.globalRecordingContinuation != nil { - print("โฑ๏ธ Global recording start timed out after \(timeoutMsCapture)ms") - self.isBroadcastModalShowing = false - self.globalRecordingInitiatedByThisPackage = false - self.resolveGlobalRecordingPromise(with: nil) - } - } - } catch { - // Task was cancelled, which is expected when recording starts or modal dismissed - } - } + // This is sort of a hack to try and track if the user opened the broadcast modal first + // may not be that reliable, because technically they can open this modal and close it without starting a broadcast + globalRecordingInitiatedByThisPackage = true - // NOW present the broadcast picker (after continuation is set up) - self.presentGlobalBroadcastModal(enableMicrophone: enableMicCapture) - } - } - } } // This is a hack I learned through: // https://mehmetbaykar.com/posts/how-to-gracefully-stop-a-broadcast-upload-extension/ diff --git a/lib/commonjs/functions.js b/lib/commonjs/functions.js index be556d5..96b4dfe 100644 --- a/lib/commonjs/functions.js +++ b/lib/commonjs/functions.js @@ -184,48 +184,25 @@ async function cancelInAppRecording() { // GLOBAL RECORDING // ============================================================================ -/** Default timeout for waiting for global recording to start (2 minutes) */ -const DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS = 120000; - /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * - * On iOS, this presents the system broadcast picker modal. The promise resolves - * when the user starts the broadcast, or with `undefined` if they dismiss the modal. - * - * On Android, this requests screen capture permission. The promise resolves - * when recording starts, or with `undefined` if the user denies permission. + * Requires screen recording permission on iOS. * * @platform iOS, Android - * @param input Configuration options for the recording session - * @returns Promise that resolves with: - * - `true` if recording started successfully - * - `undefined` if user cancelled/dismissed or timed out - * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * const started = await startGlobalRecording({ - * options: { enableMic: true }, - * timeoutMs: 60000 // 1 minute timeout - * }); - * - * if (started) { - * console.log('Recording started!'); - * // User can now navigate to other apps while recording continues - * } else { - * console.log('User cancelled or timed out'); - * } + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues * ``` */ -async function startGlobalRecording(input) { - // On iOS, the user grants microphone permission via a picker toggle +function startGlobalRecording(input) { + // On IOS, the user grants microphone permission via a picker toggle // button, so we don't need this check first - if (input?.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { + if (input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { throw new Error('Microphone permission not granted.'); } - const timeoutMs = input?.timeoutMs ?? DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS; - return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, timeoutMs); + return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, input?.onRecordingError); } /** diff --git a/lib/commonjs/functions.js.map b/lib/commonjs/functions.js.map index 3ffc621..9fe31c0 100644 --- a/lib/commonjs/functions.js.map +++ b/lib/commonjs/functions.js.map @@ -1 +1 @@ -{"version":3,"names":["_reactNativeNitroModules","require","_reactNative","NitroScreenRecorderHybridObject","NitroModules","createHybridObject","isAndroid","Platform","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS","startGlobalRecording","timeoutMs","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} +{"version":3,"names":["_reactNativeNitroModules","require","_reactNative","NitroScreenRecorderHybridObject","NitroModules","createHybridObject","isAndroid","Platform","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","startGlobalRecording","onRecordingError","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} diff --git a/lib/module/functions.js b/lib/module/functions.js index aeb0505..04f3768 100644 --- a/lib/module/functions.js +++ b/lib/module/functions.js @@ -168,48 +168,25 @@ export async function cancelInAppRecording() { // GLOBAL RECORDING // ============================================================================ -/** Default timeout for waiting for global recording to start (2 minutes) */ -const DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS = 120000; - /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * - * On iOS, this presents the system broadcast picker modal. The promise resolves - * when the user starts the broadcast, or with `undefined` if they dismiss the modal. - * - * On Android, this requests screen capture permission. The promise resolves - * when recording starts, or with `undefined` if the user denies permission. + * Requires screen recording permission on iOS. * * @platform iOS, Android - * @param input Configuration options for the recording session - * @returns Promise that resolves with: - * - `true` if recording started successfully - * - `undefined` if user cancelled/dismissed or timed out - * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * const started = await startGlobalRecording({ - * options: { enableMic: true }, - * timeoutMs: 60000 // 1 minute timeout - * }); - * - * if (started) { - * console.log('Recording started!'); - * // User can now navigate to other apps while recording continues - * } else { - * console.log('User cancelled or timed out'); - * } + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues * ``` */ -export async function startGlobalRecording(input) { - // On iOS, the user grants microphone permission via a picker toggle +export function startGlobalRecording(input) { + // On IOS, the user grants microphone permission via a picker toggle // button, so we don't need this check first - if (input?.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { + if (input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted') { throw new Error('Microphone permission not granted.'); } - const timeoutMs = input?.timeoutMs ?? DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS; - return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, timeoutMs); + return NitroScreenRecorderHybridObject.startGlobalRecording(input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, input?.onRecordingError); } /** diff --git a/lib/module/functions.js.map b/lib/module/functions.js.map index 5b937c5..cb13f29 100644 --- a/lib/module/functions.js.map +++ b/lib/module/functions.js.map @@ -1 +1 @@ -{"version":3,"names":["NitroModules","Platform","NitroScreenRecorderHybridObject","createHybridObject","isAndroid","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS","startGlobalRecording","timeoutMs","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} +{"version":3,"names":["NitroModules","Platform","NitroScreenRecorderHybridObject","createHybridObject","isAndroid","OS","getCameraPermissionStatus","getMicrophonePermissionStatus","requestCameraPermission","requestMicrophonePermission","startInAppRecording","input","console","warn","options","enableMic","Error","enableCamera","cameraPreviewStyle","cameraDevice","separateAudioFile","onRecordingFinished","stopInAppRecording","cancelInAppRecording","startGlobalRecording","onRecordingError","stopGlobalRecording","settledTimeMs","retrieveLastGlobalRecording","addScreenRecordingListener","listener","ignoreRecordingsInitiatedElsewhere","listenerId","removeScreenRecordingListener","addBroadcastPickerListener","removeBroadcastPickerListener","clearCache","clearRecordingCache"],"sourceRoot":"../../src","sources":["functions.ts"],"mappings":"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","ignoreList":[]} diff --git a/lib/typescript/NitroScreenRecorder.nitro.d.ts b/lib/typescript/NitroScreenRecorder.nitro.d.ts index 7cae257..bc37797 100644 --- a/lib/typescript/NitroScreenRecorder.nitro.d.ts +++ b/lib/typescript/NitroScreenRecorder.nitro.d.ts @@ -1,5 +1,5 @@ import type { HybridObject } from 'react-native-nitro-modules'; -import type { CameraDevice, RecorderCameraStyle, PermissionResponse, ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, BroadcastPickerPresentationEvent } from './types'; +import type { CameraDevice, RecorderCameraStyle, PermissionResponse, ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, RecordingError, BroadcastPickerPresentationEvent } from './types'; /** * ============================================================================ * NOTES WITH NITRO-MODULES @@ -24,18 +24,7 @@ export interface NitroScreenRecorder extends HybridObject<{ startInAppRecording(enableMic: boolean, enableCamera: boolean, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: boolean, onRecordingFinished: (file: ScreenRecordingFile) => void): void; stopInAppRecording(): Promise; cancelInAppRecording(): Promise; - /** - * Starts global screen recording (iOS: broadcast extension, Android: MediaProjection). - * - * @param enableMic - Whether to enable microphone recording - * @param separateAudioFile - Whether to save audio as a separate file - * @param timeoutMs - How long to wait for recording to start (default: 120000ms / 2 minutes) - * @returns Promise that resolves with: - * - `true` if recording started successfully - * - `undefined` if user dismissed/cancelled or timed out - * @throws Error if there's an actual failure (permissions, app group issues, etc.) - */ - startGlobalRecording(enableMic: boolean, separateAudioFile: boolean, timeoutMs: number): Promise; + startGlobalRecording(enableMic: boolean, separateAudioFile: boolean, onRecordingError: (error: RecordingError) => void): void; stopGlobalRecording(settledTimeMs: number): Promise; retrieveLastGlobalRecording(): ScreenRecordingFile | undefined; clearRecordingCache(): void; diff --git a/lib/typescript/NitroScreenRecorder.nitro.d.ts.map b/lib/typescript/NitroScreenRecorder.nitro.d.ts.map index c3ca33e..0c9489d 100644 --- a/lib/typescript/NitroScreenRecorder.nitro.d.ts.map +++ b/lib/typescript/NitroScreenRecorder.nitro.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"NitroScreenRecorder.nitro.d.ts","sourceRoot":"","sources":["../../src/NitroScreenRecorder.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAEjB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAKzD,yBAAyB,IAAI,gBAAgB,CAAC;IAC9C,6BAA6B,IAAI,gBAAgB,CAAC;IAClD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAM3D,0BAA0B,CACxB,kCAAkC,EAAE,OAAO,EAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAC9C,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,0BAA0B,CACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAMhD,mBAAmB,CACjB,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,OAAO,EACrB,kBAAkB,EAAE,mBAAmB,EACvC,YAAY,EAAE,YAAY,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,GAEvD,IAAI,CAAC;IACR,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC/D,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAMtC;;;;;;;;;;OAUG;IACH,oBAAoB,CAClB,SAAS,EAAE,OAAO,EAClB,iBAAiB,EAAE,OAAO,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAChC,mBAAmB,CACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC5C,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAM/D,mBAAmB,IAAI,IAAI,CAAC;CAC7B"} \ No newline at end of file +{"version":3,"file":"NitroScreenRecorder.nitro.d.ts","sourceRoot":"","sources":["../../src/NitroScreenRecorder.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAEjB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAKzD,yBAAyB,IAAI,gBAAgB,CAAC;IAC9C,6BAA6B,IAAI,gBAAgB,CAAC;IAClD,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACvD,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAM3D,0BAA0B,CACxB,kCAAkC,EAAE,OAAO,EAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAC9C,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhD,0BAA0B,CACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,CAAC;IACV,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAMhD,mBAAmB,CACjB,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,OAAO,EACrB,kBAAkB,EAAE,mBAAmB,EACvC,YAAY,EAAE,YAAY,EAC1B,iBAAiB,EAAE,OAAO,EAC1B,mBAAmB,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,IAAI,GAEvD,IAAI,CAAC;IACR,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC/D,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAMtC,oBAAoB,CAClB,SAAS,EAAE,OAAO,EAClB,iBAAiB,EAAE,OAAO,EAC1B,gBAAgB,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAChD,IAAI,CAAC;IACR,mBAAmB,CACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC5C,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAAC;IAM/D,mBAAmB,IAAI,IAAI,CAAC;CAC7B"} \ No newline at end of file diff --git a/lib/typescript/functions.d.ts b/lib/typescript/functions.d.ts index 8091da2..3483257 100644 --- a/lib/typescript/functions.d.ts +++ b/lib/typescript/functions.d.ts @@ -105,35 +105,16 @@ export declare function cancelInAppRecording(): Promise; /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * - * On iOS, this presents the system broadcast picker modal. The promise resolves - * when the user starts the broadcast, or with `undefined` if they dismiss the modal. - * - * On Android, this requests screen capture permission. The promise resolves - * when recording starts, or with `undefined` if the user denies permission. + * Requires screen recording permission on iOS. * * @platform iOS, Android - * @param input Configuration options for the recording session - * @returns Promise that resolves with: - * - `true` if recording started successfully - * - `undefined` if user cancelled/dismissed or timed out - * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * const started = await startGlobalRecording({ - * options: { enableMic: true }, - * timeoutMs: 60000 // 1 minute timeout - * }); - * - * if (started) { - * console.log('Recording started!'); - * // User can now navigate to other apps while recording continues - * } else { - * console.log('User cancelled or timed out'); - * } + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues * ``` */ -export declare function startGlobalRecording(input?: GlobalRecordingInput): Promise; +export declare function startGlobalRecording(input: GlobalRecordingInput): void; /** * Stops the current global screen recording and saves the video. * The recorded file can be retrieved using retrieveLastGlobalRecording(). diff --git a/lib/typescript/functions.d.ts.map b/lib/typescript/functions.d.ts.map index 46a6447..0294cb9 100644 --- a/lib/typescript/functions.d.ts.map +++ b/lib/typescript/functions.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../src/functions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAYjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,gBAAgB,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,IAAI,gBAAgB,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE/E;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CACjD,mBAAmB,GAAG,SAAS,CAChC,CAMA;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1D;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,CAAC,EAAE,oBAAoB,GAC3B,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAkB9B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAe3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAE7E;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,kCAA0C,GAC3C,EAAE;IACD,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,kCAAkC,EAAE,OAAO,CAAC;CAC7C,GAAG,MAAM,IAAI,CASb;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,IAAI,CAWZ;AAMD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"} \ No newline at end of file +{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../src/functions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,gCAAgC,EACjC,MAAM,SAAS,CAAC;AAYjB;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,IAAI,gBAAgB,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,IAAI,gBAAgB,CAEhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE3E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAE/E;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CACjD,mBAAmB,GAAG,SAAS,CAChC,CAMA;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAetE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAe3C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,IAAI,mBAAmB,GAAG,SAAS,CAE7E;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,kCAA0C,GAC3C,EAAE;IACD,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,kCAAkC,EAAE,OAAO,CAAC;CAC7C,GAAG,MAAM,IAAI,CASb;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,gCAAgC,KAAK,IAAI,GAC1D,MAAM,IAAI,CAWZ;AAMD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"} \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts index 4b739d9..27b2cec 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -182,27 +182,18 @@ export type GlobalRecordingInputOptions = { * options: { * enableMic: true, // Enable microphone audio for the recording * }, - * timeoutMs: 120000, // 2 minutes timeout + * onRecordingError: (error) => { + * console.error('Global recording failed:', error.message); + * // Handle the error, e.g., display an alert to the user. + * } * }; - * - * const started = await startGlobalRecording(globalInput); - * if (started) { - * console.log('Recording started successfully'); - * } else { - * console.log('User cancelled or timed out'); - * } * ``` */ export type GlobalRecordingInput = { /** Optional configuration options for the global recording session. */ options?: GlobalRecordingInputOptions; - /** - * How long to wait (in milliseconds) for the recording to start before timing out. - * On iOS, this covers the time the user spends in the broadcast picker modal. - * On Android, this covers the time for the permission dialog and service startup. - * @default 120000 (2 minutes) - */ - timeoutMs?: number; + /** Callback invoked when the global recording encounters an error during start or execution. */ + onRecordingError: (error: RecordingError) => void; }; /** * Represents a separate audio file recorded alongside the video. diff --git a/lib/typescript/types.d.ts.map b/lib/typescript/types.d.ts.map index eee5af1..1bc23b0 100644 --- a/lib/typescript/types.d.ts.map +++ b/lib/typescript/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp b/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp new file mode 100644 index 0000000..dce2ade --- /dev/null +++ b/nitrogen/generated/android/c++/JFunc_void_RecordingError.hpp @@ -0,0 +1,77 @@ +/// +/// JFunc_void_RecordingError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include + +#include "RecordingError.hpp" +#include +#include "JRecordingError.hpp" +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * Represents the Java/Kotlin callback `(error: RecordingError) -> Unit`. + * This can be passed around between C++ and Java/Kotlin. + */ + struct JFunc_void_RecordingError: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError;"; + + public: + /** + * Invokes the function this `JFunc_void_RecordingError` instance holds through JNI. + */ + void invoke(const RecordingError& error) const { + static const auto method = javaClassStatic()->getMethod /* error */)>("invoke"); + method(self(), JRecordingError::fromCpp(error)); + } + }; + + /** + * An implementation of Func_void_RecordingError that is backed by a C++ implementation (using `std::function<...>`) + */ + struct JFunc_void_RecordingError_cxx final: public jni::HybridClass { + public: + static jni::local_ref fromCpp(const std::function& func) { + return JFunc_void_RecordingError_cxx::newObjectCxxArgs(func); + } + + public: + /** + * Invokes the C++ `std::function<...>` this `JFunc_void_RecordingError_cxx` instance holds. + */ + void invoke_cxx(jni::alias_ref error) { + _func(error->toCpp()); + } + + public: + [[nodiscard]] + inline const std::function& getFunction() const { + return _func; + } + + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError_cxx;"; + static void registerNatives() { + registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_RecordingError_cxx::invoke_cxx)}); + } + + private: + explicit JFunc_void_RecordingError_cxx(const std::function& func): _func(func) { } + + private: + friend HybridBase; + std::function _func; + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp index 65f22dd..a1cb1e8 100644 --- a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.cpp @@ -27,6 +27,8 @@ namespace margelo::nitro::nitroscreenrecorder { enum class BroadcastPickerPresen namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } // Forward declaration of `CameraDevice` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "PermissionStatus.hpp" #include "JPermissionStatus.hpp" @@ -56,6 +58,9 @@ namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } #include "CameraDevice.hpp" #include "JCameraDevice.hpp" #include "JFunc_void_ScreenRecordingFile.hpp" +#include "RecordingError.hpp" +#include "JFunc_void_RecordingError.hpp" +#include "JRecordingError.hpp" namespace margelo::nitro::nitroscreenrecorder { @@ -184,21 +189,9 @@ namespace margelo::nitro::nitroscreenrecorder { return __promise; }(); } - std::shared_ptr>> JHybridNitroScreenRecorderSpec::startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) { - static const auto method = javaClassStatic()->getMethod(jboolean /* enableMic */, jboolean /* separateAudioFile */, double /* timeoutMs */)>("startGlobalRecording"); - auto __result = method(_javaPart, enableMic, separateAudioFile, timeoutMs); - return [&]() { - auto __promise = Promise>::create(); - __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { - auto __result = jni::static_ref_cast(__boxedResult); - __promise->resolve(__result != nullptr ? std::make_optional(static_cast(__result->value())) : std::nullopt); - }); - __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { - jni::JniException __jniError(__throwable); - __promise->reject(std::make_exception_ptr(__jniError)); - }); - return __promise; - }(); + void JHybridNitroScreenRecorderSpec::startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) { + static const auto method = javaClassStatic()->getMethod /* onRecordingError */)>("startGlobalRecording_cxx"); + method(_javaPart, enableMic, separateAudioFile, JFunc_void_RecordingError_cxx::fromCpp(onRecordingError)); } std::shared_ptr>> JHybridNitroScreenRecorderSpec::stopGlobalRecording(double settledTimeMs) { static const auto method = javaClassStatic()->getMethod(double /* settledTimeMs */)>("stopGlobalRecording"); diff --git a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp index e3dbe76..ab5c30f 100644 --- a/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridNitroScreenRecorderSpec.hpp @@ -65,7 +65,7 @@ namespace margelo::nitro::nitroscreenrecorder { void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) override; std::shared_ptr>> stopInAppRecording() override; std::shared_ptr> cancelInAppRecording() override; - std::shared_ptr>> startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) override; + void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) override; std::shared_ptr>> stopGlobalRecording(double settledTimeMs) override; std::optional retrieveLastGlobalRecording() override; void clearRecordingCache() override; diff --git a/nitrogen/generated/android/c++/JRecordingError.hpp b/nitrogen/generated/android/c++/JRecordingError.hpp new file mode 100644 index 0000000..ecbb2f7 --- /dev/null +++ b/nitrogen/generated/android/c++/JRecordingError.hpp @@ -0,0 +1,61 @@ +/// +/// JRecordingError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "RecordingError.hpp" + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ struct "RecordingError" and the the Kotlin data class "RecordingError". + */ + struct JRecordingError final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitroscreenrecorder/RecordingError;"; + + public: + /** + * Convert this Java/Kotlin-based struct to the C++ struct RecordingError by copying all values to C++. + */ + [[maybe_unused]] + [[nodiscard]] + RecordingError toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldName = clazz->getField("name"); + jni::local_ref name = this->getFieldValue(fieldName); + static const auto fieldMessage = clazz->getField("message"); + jni::local_ref message = this->getFieldValue(fieldMessage); + return RecordingError( + name->toStdString(), + message->toStdString() + ); + } + + public: + /** + * Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java. + */ + [[maybe_unused]] + static jni::local_ref fromCpp(const RecordingError& value) { + using JSignature = JRecordingError(jni::alias_ref, jni::alias_ref); + static const auto clazz = javaClassStatic(); + static const auto create = clazz->getStaticMethod("fromCpp"); + return create( + clazz, + jni::make_jstring(value.name), + jni::make_jstring(value.message) + ); + } + }; + +} // namespace margelo::nitro::nitroscreenrecorder diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt new file mode 100644 index 0000000..2256914 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/Func_void_RecordingError.kt @@ -0,0 +1,80 @@ +/// +/// Func_void_RecordingError.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.jni.HybridData +import com.facebook.proguard.annotations.DoNotStrip +import dalvik.annotation.optimization.FastNative + + +/** + * Represents the JavaScript callback `(error: struct) => void`. + * This can be either implemented in C++ (in which case it might be a callback coming from JS), + * or in Kotlin/Java (in which case it is a native callback). + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType") +fun interface Func_void_RecordingError: (RecordingError) -> Unit { + /** + * Call the given JS callback. + * @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted. + */ + @DoNotStrip + @Keep + override fun invoke(error: RecordingError): Unit +} + +/** + * Represents the JavaScript callback `(error: struct) => void`. + * This is implemented in C++, via a `std::function<...>`. + * The callback might be coming from JS. + */ +@DoNotStrip +@Keep +@Suppress( + "KotlinJniMissingFunction", "unused", + "RedundantSuppression", "RedundantUnitReturnType", "FunctionName", + "ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName", +) +class Func_void_RecordingError_cxx: Func_void_RecordingError { + @DoNotStrip + @Keep + private val mHybridData: HybridData + + @DoNotStrip + @Keep + private constructor(hybridData: HybridData) { + mHybridData = hybridData + } + + @DoNotStrip + @Keep + override fun invoke(error: RecordingError): Unit + = invoke_cxx(error) + + @FastNative + private external fun invoke_cxx(error: RecordingError): Unit +} + +/** + * Represents the JavaScript callback `(error: struct) => void`. + * This is implemented in Java/Kotlin, via a `(RecordingError) -> Unit`. + * The callback is always coming from native. + */ +@DoNotStrip +@Keep +@Suppress("ClassName", "RedundantUnitReturnType", "unused") +class Func_void_RecordingError_java(private val function: (RecordingError) -> Unit): Func_void_RecordingError { + @DoNotStrip + @Keep + override fun invoke(error: RecordingError): Unit { + return this.function(error) + } +} diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt index 6e4376e..5bbd299 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/HybridNitroScreenRecorderSpec.kt @@ -105,9 +105,14 @@ abstract class HybridNitroScreenRecorderSpec: HybridObject() { @Keep abstract fun cancelInAppRecording(): Promise + abstract fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: (error: RecordingError) -> Unit): Unit + @DoNotStrip @Keep - abstract fun startGlobalRecording(enableMic: Boolean, separateAudioFile: Boolean, timeoutMs: Double): Promise + private fun startGlobalRecording_cxx(enableMic: Boolean, separateAudioFile: Boolean, onRecordingError: Func_void_RecordingError): Unit { + val __result = startGlobalRecording(enableMic, separateAudioFile, onRecordingError) + return __result + } @DoNotStrip @Keep diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt new file mode 100644 index 0000000..7def6fa --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/RecordingError.kt @@ -0,0 +1,41 @@ +/// +/// RecordingError.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.nitroscreenrecorder + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + + +/** + * Represents the JavaScript object/struct "RecordingError". + */ +@DoNotStrip +@Keep +data class RecordingError( + @DoNotStrip + @Keep + val name: String, + @DoNotStrip + @Keep + val message: String +) { + /* primary constructor */ + + private companion object { + /** + * Constructor called from C++ + */ + @DoNotStrip + @Keep + @Suppress("unused") + @JvmStatic + private fun fromCpp(name: String, message: String): RecordingError { + return RecordingError(name, message) + } + } +} diff --git a/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp b/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp index 2666610..a7bd57b 100644 --- a/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp +++ b/nitrogen/generated/android/nitroscreenrecorderOnLoad.cpp @@ -19,6 +19,7 @@ #include "JFunc_void_ScreenRecordingEvent.hpp" #include "JFunc_void_BroadcastPickerPresentationEvent.hpp" #include "JFunc_void_ScreenRecordingFile.hpp" +#include "JFunc_void_RecordingError.hpp" #include namespace margelo::nitro::nitroscreenrecorder { @@ -34,6 +35,7 @@ int initialize(JavaVM* vm) { margelo::nitro::nitroscreenrecorder::JFunc_void_ScreenRecordingEvent_cxx::registerNatives(); margelo::nitro::nitroscreenrecorder::JFunc_void_BroadcastPickerPresentationEvent_cxx::registerNatives(); margelo::nitro::nitroscreenrecorder::JFunc_void_ScreenRecordingFile_cxx::registerNatives(); + margelo::nitro::nitroscreenrecorder::JFunc_void_RecordingError_cxx::registerNatives(); // Register Nitro Hybrid Objects HybridObjectRegistry::registerHybridObjectConstructor( diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp index eeff26c..99d48e8 100644 --- a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.cpp @@ -69,11 +69,11 @@ namespace margelo::nitro::nitroscreenrecorder::bridge::swift { }; } - // pragma MARK: std::function /* result */)> - Func_void_std__optional_bool_ create_Func_void_std__optional_bool_(void* NON_NULL swiftClosureWrapper) noexcept { - auto swiftClosure = NitroScreenRecorder::Func_void_std__optional_bool_::fromUnsafe(swiftClosureWrapper); - return [swiftClosure = std::move(swiftClosure)](std::optional result) mutable -> void { - swiftClosure.call(result); + // pragma MARK: std::function + Func_void_RecordingError create_Func_void_RecordingError(void* NON_NULL swiftClosureWrapper) noexcept { + auto swiftClosure = NitroScreenRecorder::Func_void_RecordingError::fromUnsafe(swiftClosureWrapper); + return [swiftClosure = std::move(swiftClosure)](const RecordingError& error) mutable -> void { + swiftClosure.call(error); }; } diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp index 5c2622a..fc95ffb 100644 --- a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Bridge.hpp @@ -18,6 +18,8 @@ namespace margelo::nitro::nitroscreenrecorder { class HybridNitroScreenRecorderS namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } // Forward declaration of `PermissionStatus` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } // Forward declaration of `RecordingEventReason` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } // Forward declaration of `RecordingEventType` to properly resolve imports. @@ -37,6 +39,7 @@ namespace NitroScreenRecorder { class HybridNitroScreenRecorderSpec_cxx; } #include "HybridNitroScreenRecorderSpec.hpp" #include "PermissionResponse.hpp" #include "PermissionStatus.hpp" +#include "RecordingError.hpp" #include "RecordingEventReason.hpp" #include "RecordingEventType.hpp" #include "ScreenRecordingEvent.hpp" @@ -291,53 +294,26 @@ namespace margelo::nitro::nitroscreenrecorder::bridge::swift { return Func_void_Wrapper(std::move(value)); } - // pragma MARK: std::optional + // pragma MARK: std::function /** - * Specialized version of `std::optional`. + * Specialized version of `std::function`. */ - using std__optional_bool_ = std::optional; - inline std::optional create_std__optional_bool_(const bool& value) noexcept { - return std::optional(value); - } - inline bool has_value_std__optional_bool_(const std::optional& optional) noexcept { - return optional.has_value(); - } - inline bool get_std__optional_bool_(const std::optional& optional) noexcept { - return *optional; - } - - // pragma MARK: std::shared_ptr>> - /** - * Specialized version of `std::shared_ptr>>`. - */ - using std__shared_ptr_Promise_std__optional_bool___ = std::shared_ptr>>; - inline std::shared_ptr>> create_std__shared_ptr_Promise_std__optional_bool___() noexcept { - return Promise>::create(); - } - inline PromiseHolder> wrap_std__shared_ptr_Promise_std__optional_bool___(std::shared_ptr>> promise) noexcept { - return PromiseHolder>(std::move(promise)); - } - - // pragma MARK: std::function /* result */)> + using Func_void_RecordingError = std::function; /** - * Specialized version of `std::function)>`. + * Wrapper class for a `std::function`, this can be used from Swift. */ - using Func_void_std__optional_bool_ = std::function /* result */)>; - /** - * Wrapper class for a `std::function / * result * /)>`, this can be used from Swift. - */ - class Func_void_std__optional_bool__Wrapper final { + class Func_void_RecordingError_Wrapper final { public: - explicit Func_void_std__optional_bool__Wrapper(std::function /* result */)>&& func): _function(std::make_unique /* result */)>>(std::move(func))) {} - inline void call(std::optional result) const noexcept { - _function->operator()(result); + explicit Func_void_RecordingError_Wrapper(std::function&& func): _function(std::make_unique>(std::move(func))) {} + inline void call(RecordingError error) const noexcept { + _function->operator()(error); } private: - std::unique_ptr /* result */)>> _function; + std::unique_ptr> _function; } SWIFT_NONCOPYABLE; - Func_void_std__optional_bool_ create_Func_void_std__optional_bool_(void* NON_NULL swiftClosureWrapper) noexcept; - inline Func_void_std__optional_bool__Wrapper wrap_Func_void_std__optional_bool_(Func_void_std__optional_bool_ value) noexcept { - return Func_void_std__optional_bool__Wrapper(std::move(value)); + Func_void_RecordingError create_Func_void_RecordingError(void* NON_NULL swiftClosureWrapper) noexcept; + inline Func_void_RecordingError_Wrapper wrap_Func_void_RecordingError(Func_void_RecordingError value) noexcept { + return Func_void_RecordingError_Wrapper(std::move(value)); } // pragma MARK: std::shared_ptr @@ -406,15 +382,6 @@ namespace margelo::nitro::nitroscreenrecorder::bridge::swift { return Result>>::withError(error); } - // pragma MARK: Result>>> - using Result_std__shared_ptr_Promise_std__optional_bool____ = Result>>>; - inline Result_std__shared_ptr_Promise_std__optional_bool____ create_Result_std__shared_ptr_Promise_std__optional_bool____(const std::shared_ptr>>& value) noexcept { - return Result>>>::withValue(value); - } - inline Result_std__shared_ptr_Promise_std__optional_bool____ create_Result_std__shared_ptr_Promise_std__optional_bool____(const std::exception_ptr& error) noexcept { - return Result>>>::withError(error); - } - // pragma MARK: Result> using Result_std__optional_ScreenRecordingFile__ = Result>; inline Result_std__optional_ScreenRecordingFile__ create_Result_std__optional_ScreenRecordingFile__(const std::optional& value) noexcept { diff --git a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp index f21d364..4b6bef5 100644 --- a/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/NitroScreenRecorder-Swift-Cxx-Umbrella.hpp @@ -22,6 +22,8 @@ namespace margelo::nitro::nitroscreenrecorder { struct PermissionResponse; } namespace margelo::nitro::nitroscreenrecorder { enum class PermissionStatus; } // Forward declaration of `RecorderCameraStyle` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } // Forward declaration of `RecordingEventReason` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { enum class RecordingEventReason; } // Forward declaration of `RecordingEventType` to properly resolve imports. @@ -39,6 +41,7 @@ namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } #include "PermissionResponse.hpp" #include "PermissionStatus.hpp" #include "RecorderCameraStyle.hpp" +#include "RecordingError.hpp" #include "RecordingEventReason.hpp" #include "RecordingEventType.hpp" #include "ScreenRecordingEvent.hpp" diff --git a/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp index 087d3d7..fa7d5d7 100644 --- a/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridNitroScreenRecorderSpecSwift.hpp @@ -32,6 +32,8 @@ namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } // Forward declaration of `AudioRecordingFile` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "PermissionStatus.hpp" #include "PermissionResponse.hpp" @@ -47,6 +49,7 @@ namespace margelo::nitro::nitroscreenrecorder { struct AudioRecordingFile; } #include "ScreenRecordingFile.hpp" #include #include "AudioRecordingFile.hpp" +#include "RecordingError.hpp" #include "NitroScreenRecorder-Swift-Cxx-Umbrella.hpp" @@ -174,13 +177,11 @@ namespace margelo::nitro::nitroscreenrecorder { auto __value = std::move(__result.value()); return __value; } - inline std::shared_ptr>> startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) override { - auto __result = _swiftPart.startGlobalRecording(std::forward(enableMic), std::forward(separateAudioFile), std::forward(timeoutMs)); + inline void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) override { + auto __result = _swiftPart.startGlobalRecording(std::forward(enableMic), std::forward(separateAudioFile), onRecordingError); if (__result.hasError()) [[unlikely]] { std::rethrow_exception(__result.error()); } - auto __value = std::move(__result.value()); - return __value; } inline std::shared_ptr>> stopGlobalRecording(double settledTimeMs) override { auto __result = _swiftPart.stopGlobalRecording(std::forward(settledTimeMs)); diff --git a/nitrogen/generated/ios/swift/Func_void_std__optional_bool_.swift b/nitrogen/generated/ios/swift/Func_void_RecordingError.swift similarity index 53% rename from nitrogen/generated/ios/swift/Func_void_std__optional_bool_.swift rename to nitrogen/generated/ios/swift/Func_void_RecordingError.swift index beb4f00..4d11201 100644 --- a/nitrogen/generated/ios/swift/Func_void_std__optional_bool_.swift +++ b/nitrogen/generated/ios/swift/Func_void_RecordingError.swift @@ -1,5 +1,5 @@ /// -/// Func_void_std__optional_bool_.swift +/// Func_void_RecordingError.swift /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. /// https://github.com/mrousavy/nitro /// Copyright ยฉ 2025 Marc Rousavy @ Margelo @@ -9,28 +9,21 @@ import NitroModules /** - * Wraps a Swift `(_ value: Bool?) -> Void` as a class. + * Wraps a Swift `(_ error: RecordingError) -> Void` as a class. * This class can be used from C++, e.g. to wrap the Swift closure as a `std::function`. */ -public final class Func_void_std__optional_bool_ { +public final class Func_void_RecordingError { public typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift - private let closure: (_ value: Bool?) -> Void + private let closure: (_ error: RecordingError) -> Void - public init(_ closure: @escaping (_ value: Bool?) -> Void) { + public init(_ closure: @escaping (_ error: RecordingError) -> Void) { self.closure = closure } @inline(__always) - public func call(value: bridge.std__optional_bool_) -> Void { - self.closure({ () -> Bool? in - if bridge.has_value_std__optional_bool_(value) { - let __unwrapped = bridge.get_std__optional_bool_(value) - return __unwrapped - } else { - return nil - } - }()) + public func call(error: RecordingError) -> Void { + self.closure(error) } /** @@ -43,12 +36,12 @@ public final class Func_void_std__optional_bool_ { } /** - * Casts an unsafe pointer to a `Func_void_std__optional_bool_`. - * The pointer has to be a retained opaque `Unmanaged`. + * Casts an unsafe pointer to a `Func_void_RecordingError`. + * The pointer has to be a retained opaque `Unmanaged`. * This removes one strong reference from the object! */ @inline(__always) - public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_std__optional_bool_ { - return Unmanaged.fromOpaque(pointer).takeRetainedValue() + public static func fromUnsafe(_ pointer: UnsafeMutableRawPointer) -> Func_void_RecordingError { + return Unmanaged.fromOpaque(pointer).takeRetainedValue() } } diff --git a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift index 608832f..e0947f0 100644 --- a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift +++ b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec.swift @@ -26,7 +26,7 @@ public protocol HybridNitroScreenRecorderSpec_protocol: HybridObject { func startInAppRecording(enableMic: Bool, enableCamera: Bool, cameraPreviewStyle: RecorderCameraStyle, cameraDevice: CameraDevice, separateAudioFile: Bool, onRecordingFinished: @escaping (_ file: ScreenRecordingFile) -> Void) throws -> Void func stopInAppRecording() throws -> Promise func cancelInAppRecording() throws -> Promise - func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, timeoutMs: Double) throws -> Promise + func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: @escaping (_ error: RecordingError) -> Void) throws -> Void func stopGlobalRecording(settledTimeMs: Double) throws -> Promise func retrieveLastGlobalRecording() throws -> ScreenRecordingFile? func clearRecordingCache() throws -> Void diff --git a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift index cd79d83..50e2a0f 100644 --- a/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridNitroScreenRecorderSpec_cxx.swift @@ -297,27 +297,18 @@ open class HybridNitroScreenRecorderSpec_cxx { } @inline(__always) - public final func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, timeoutMs: Double) -> bridge.Result_std__shared_ptr_Promise_std__optional_bool____ { + public final func startGlobalRecording(enableMic: Bool, separateAudioFile: Bool, onRecordingError: bridge.Func_void_RecordingError) -> bridge.Result_void_ { do { - let __result = try self.__implementation.startGlobalRecording(enableMic: enableMic, separateAudioFile: separateAudioFile, timeoutMs: timeoutMs) - let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__optional_bool___ in - let __promise = bridge.create_std__shared_ptr_Promise_std__optional_bool___() - let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__optional_bool___(__promise) - __result - .then({ __result in __promiseHolder.resolve({ () -> bridge.std__optional_bool_ in - if let __unwrappedValue = __result { - return bridge.create_std__optional_bool_(__unwrappedValue) - } else { - return .init() - } - }()) }) - .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) - return __promise - }() - return bridge.create_Result_std__shared_ptr_Promise_std__optional_bool____(__resultCpp) + try self.__implementation.startGlobalRecording(enableMic: enableMic, separateAudioFile: separateAudioFile, onRecordingError: { () -> (RecordingError) -> Void in + let __wrappedFunction = bridge.wrap_Func_void_RecordingError(onRecordingError) + return { (__error: RecordingError) -> Void in + __wrappedFunction.call(__error) + } + }()) + return bridge.create_Result_void_() } catch (let __error) { let __exceptionPtr = __error.toCpp() - return bridge.create_Result_std__shared_ptr_Promise_std__optional_bool____(__exceptionPtr) + return bridge.create_Result_void_(__exceptionPtr) } } diff --git a/nitrogen/generated/ios/swift/RecordingError.swift b/nitrogen/generated/ios/swift/RecordingError.swift new file mode 100644 index 0000000..d46c945 --- /dev/null +++ b/nitrogen/generated/ios/swift/RecordingError.swift @@ -0,0 +1,46 @@ +/// +/// RecordingError.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +import NitroModules + +/** + * Represents an instance of `RecordingError`, backed by a C++ struct. + */ +public typealias RecordingError = margelo.nitro.nitroscreenrecorder.RecordingError + +public extension RecordingError { + private typealias bridge = margelo.nitro.nitroscreenrecorder.bridge.swift + + /** + * Create a new instance of `RecordingError`. + */ + init(name: String, message: String) { + self.init(std.string(name), std.string(message)) + } + + var name: String { + @inline(__always) + get { + return String(self.__name) + } + @inline(__always) + set { + self.__name = std.string(newValue) + } + } + + var message: String { + @inline(__always) + get { + return String(self.__message) + } + @inline(__always) + set { + self.__message = std.string(newValue) + } + } +} diff --git a/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp index c1d1c4e..d3a2b5e 100644 --- a/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridNitroScreenRecorderSpec.hpp @@ -27,6 +27,8 @@ namespace margelo::nitro::nitroscreenrecorder { struct RecorderCameraStyle; } namespace margelo::nitro::nitroscreenrecorder { enum class CameraDevice; } // Forward declaration of `ScreenRecordingFile` to properly resolve imports. namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } +// Forward declaration of `RecordingError` to properly resolve imports. +namespace margelo::nitro::nitroscreenrecorder { struct RecordingError; } #include "PermissionStatus.hpp" #include "PermissionResponse.hpp" @@ -38,6 +40,7 @@ namespace margelo::nitro::nitroscreenrecorder { struct ScreenRecordingFile; } #include "CameraDevice.hpp" #include "ScreenRecordingFile.hpp" #include +#include "RecordingError.hpp" namespace margelo::nitro::nitroscreenrecorder { @@ -81,7 +84,7 @@ namespace margelo::nitro::nitroscreenrecorder { virtual void startInAppRecording(bool enableMic, bool enableCamera, const RecorderCameraStyle& cameraPreviewStyle, CameraDevice cameraDevice, bool separateAudioFile, const std::function& onRecordingFinished) = 0; virtual std::shared_ptr>> stopInAppRecording() = 0; virtual std::shared_ptr> cancelInAppRecording() = 0; - virtual std::shared_ptr>> startGlobalRecording(bool enableMic, bool separateAudioFile, double timeoutMs) = 0; + virtual void startGlobalRecording(bool enableMic, bool separateAudioFile, const std::function& onRecordingError) = 0; virtual std::shared_ptr>> stopGlobalRecording(double settledTimeMs) = 0; virtual std::optional retrieveLastGlobalRecording() = 0; virtual void clearRecordingCache() = 0; diff --git a/nitrogen/generated/shared/c++/RecordingError.hpp b/nitrogen/generated/shared/c++/RecordingError.hpp new file mode 100644 index 0000000..f499c5c --- /dev/null +++ b/nitrogen/generated/shared/c++/RecordingError.hpp @@ -0,0 +1,79 @@ +/// +/// RecordingError.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright ยฉ 2025 Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +namespace margelo::nitro::nitroscreenrecorder { + + /** + * A struct which can be represented as a JavaScript object (RecordingError). + */ + struct RecordingError { + public: + std::string name SWIFT_PRIVATE; + std::string message SWIFT_PRIVATE; + + public: + RecordingError() = default; + explicit RecordingError(std::string name, std::string message): name(name), message(message) {} + }; + +} // namespace margelo::nitro::nitroscreenrecorder + +namespace margelo::nitro { + + // C++ RecordingError <> JS RecordingError (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::nitroscreenrecorder::RecordingError fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::nitroscreenrecorder::RecordingError( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "name")), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, "message")) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroscreenrecorder::RecordingError& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, "name", JSIConverter::toJSI(runtime, arg.name)); + obj.setProperty(runtime, "message", JSIConverter::toJSI(runtime, arg.message)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "name"))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, "message"))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/package.json b/package.json index 59002d8..7e3382d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-nitro-screen-recorder", - "version": "0.9.2", + "version": "0.9.3", "description": "A library to capture screen recordings with react-native powered by NitroModules.", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/src/NitroScreenRecorder.nitro.ts b/src/NitroScreenRecorder.nitro.ts index d1b807c..15b00a5 100644 --- a/src/NitroScreenRecorder.nitro.ts +++ b/src/NitroScreenRecorder.nitro.ts @@ -6,6 +6,7 @@ import type { ScreenRecordingFile, ScreenRecordingEvent, PermissionStatus, + RecordingError, BroadcastPickerPresentationEvent, } from './types'; @@ -65,22 +66,11 @@ export interface NitroScreenRecorder // GLOBAL RECORDING // ============================================================================ - /** - * Starts global screen recording (iOS: broadcast extension, Android: MediaProjection). - * - * @param enableMic - Whether to enable microphone recording - * @param separateAudioFile - Whether to save audio as a separate file - * @param timeoutMs - How long to wait for recording to start (default: 120000ms / 2 minutes) - * @returns Promise that resolves with: - * - `true` if recording started successfully - * - `undefined` if user dismissed/cancelled or timed out - * @throws Error if there's an actual failure (permissions, app group issues, etc.) - */ startGlobalRecording( enableMic: boolean, separateAudioFile: boolean, - timeoutMs: number - ): Promise; + onRecordingError: (error: RecordingError) => void + ): void; stopGlobalRecording( settledTimeMs: number ): Promise; diff --git a/src/functions.ts b/src/functions.ts index 635ff65..989f4de 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -200,59 +200,32 @@ export async function cancelInAppRecording(): Promise { // GLOBAL RECORDING // ============================================================================ -/** Default timeout for waiting for global recording to start (2 minutes) */ -const DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS = 120000; - /** * Starts global screen recording that captures the entire device screen. * Records system-wide content, including other apps and system UI. - * - * On iOS, this presents the system broadcast picker modal. The promise resolves - * when the user starts the broadcast, or with `undefined` if they dismiss the modal. - * - * On Android, this requests screen capture permission. The promise resolves - * when recording starts, or with `undefined` if the user denies permission. + * Requires screen recording permission on iOS. * * @platform iOS, Android - * @param input Configuration options for the recording session - * @returns Promise that resolves with: - * - `true` if recording started successfully - * - `undefined` if user cancelled/dismissed or timed out - * @throws Error if there's an actual failure (permissions on Android, app group issues on iOS, etc.) * @example * ```typescript - * const started = await startGlobalRecording({ - * options: { enableMic: true }, - * timeoutMs: 60000 // 1 minute timeout - * }); - * - * if (started) { - * console.log('Recording started!'); - * // User can now navigate to other apps while recording continues - * } else { - * console.log('User cancelled or timed out'); - * } + * startGlobalRecording(); + * // User can now navigate to other apps while recording continues * ``` */ -export async function startGlobalRecording( - input?: GlobalRecordingInput -): Promise { - // On iOS, the user grants microphone permission via a picker toggle +export function startGlobalRecording(input: GlobalRecordingInput): void { + // On IOS, the user grants microphone permission via a picker toggle // button, so we don't need this check first if ( - input?.options?.enableMic && + input.options?.enableMic && isAndroid && getMicrophonePermissionStatus() !== 'granted' ) { throw new Error('Microphone permission not granted.'); } - - const timeoutMs = input?.timeoutMs ?? DEFAULT_GLOBAL_RECORDING_TIMEOUT_MS; - return NitroScreenRecorderHybridObject.startGlobalRecording( input?.options?.enableMic ?? false, input?.options?.separateAudioFile ?? false, - timeoutMs + input?.onRecordingError ); } diff --git a/src/types.ts b/src/types.ts index de70855..42d610a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -192,27 +192,18 @@ export type GlobalRecordingInputOptions = { * options: { * enableMic: true, // Enable microphone audio for the recording * }, - * timeoutMs: 120000, // 2 minutes timeout + * onRecordingError: (error) => { + * console.error('Global recording failed:', error.message); + * // Handle the error, e.g., display an alert to the user. + * } * }; - * - * const started = await startGlobalRecording(globalInput); - * if (started) { - * console.log('Recording started successfully'); - * } else { - * console.log('User cancelled or timed out'); - * } * ``` */ export type GlobalRecordingInput = { /** Optional configuration options for the global recording session. */ options?: GlobalRecordingInputOptions; - /** - * How long to wait (in milliseconds) for the recording to start before timing out. - * On iOS, this covers the time the user spends in the broadcast picker modal. - * On Android, this covers the time for the permission dialog and service startup. - * @default 120000 (2 minutes) - */ - timeoutMs?: number; + /** Callback invoked when the global recording encounters an error during start or execution. */ + onRecordingError: (error: RecordingError) => void; }; /** From d23dbc3b8c8c2a166ba59fbc208e48569b833d27 Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 6 Jan 2026 18:41:51 +0100 Subject: [PATCH 15/32] feat: add live chunking API for global recordings - Add markChunkStart(), finalizeChunk(), flushChunk() for segment capture - Add getExtensionStatus() with isAlive, isMicActive, isCapturing, chunkStartedAt - Implement writer-swapping for seamless chunk capture without stopping - Improve heartbeat reliability with faster updates and explicit sync - Bump to v1.0.0 --- example/app.json | 4 +- example/src/App.tsx | 670 ++++++++++++------ .../SampleHandler.swift | 334 ++++++++- ios/NitroScreenRecorder.swift | 97 +++ .../SampleHandler.swift | 334 ++++++++- lib/commonjs/functions.js | 129 ++++ lib/commonjs/functions.js.map | 2 +- .../SampleHandler.swift | 334 ++++++++- lib/module/functions.js | 125 ++++ lib/module/functions.js.map | 2 +- lib/typescript/NitroScreenRecorder.nitro.d.ts | 5 +- .../NitroScreenRecorder.nitro.d.ts.map | 2 +- .../SampleHandler.swift | 334 ++++++++- lib/typescript/functions.d.ts | 84 ++- lib/typescript/functions.d.ts.map | 2 +- lib/typescript/types.d.ts | 27 + lib/typescript/types.d.ts.map | 2 +- .../android/c++/JAudioRecordingFile.hpp | 2 +- .../c++/JBroadcastPickerPresentationEvent.hpp | 2 +- .../generated/android/c++/JCameraDevice.hpp | 2 +- .../android/c++/JExtensionStatus.hpp | 73 ++ ..._void_BroadcastPickerPresentationEvent.hpp | 2 +- .../android/c++/JFunc_void_RecordingError.hpp | 2 +- .../c++/JFunc_void_ScreenRecordingEvent.hpp | 2 +- .../c++/JFunc_void_ScreenRecordingFile.hpp | 2 +- .../c++/JHybridNitroScreenRecorderSpec.cpp | 31 +- .../c++/JHybridNitroScreenRecorderSpec.hpp | 5 +- .../android/c++/JPermissionResponse.hpp | 2 +- .../android/c++/JPermissionStatus.hpp | 2 +- .../android/c++/JRecorderCameraStyle.hpp | 2 +- .../generated/android/c++/JRecordingError.hpp | 2 +- .../android/c++/JRecordingEventReason.hpp | 2 +- .../android/c++/JRecordingEventType.hpp | 2 +- .../android/c++/JScreenRecordingEvent.hpp | 2 +- .../android/c++/JScreenRecordingFile.hpp | 2 +- .../nitroscreenrecorder/AudioRecordingFile.kt | 2 +- .../BroadcastPickerPresentationEvent.kt | 2 +- .../nitro/nitroscreenrecorder/CameraDevice.kt | 2 +- .../nitroscreenrecorder/ExtensionStatus.kt | 50 ++ ...c_void_BroadcastPickerPresentationEvent.kt | 2 +- .../Func_void_RecordingError.kt | 2 +- .../Func_void_ScreenRecordingEvent.kt | 2 +- .../Func_void_ScreenRecordingFile.kt | 2 +- .../HybridNitroScreenRecorderSpec.kt | 14 +- .../nitroscreenrecorder/PermissionResponse.kt | 2 +- .../nitroscreenrecorder/PermissionStatus.kt | 2 +- .../RecorderCameraStyle.kt | 2 +- .../nitroscreenrecorder/RecordingError.kt | 2 +- .../RecordingEventReason.kt | 2 +- .../nitroscreenrecorder/RecordingEventType.kt | 2 +- .../ScreenRecordingEvent.kt | 2 +- .../ScreenRecordingFile.kt | 2 +- .../nitroscreenrecorderOnLoad.kt | 2 +- .../nitroscreenrecorder+autolinking.cmake | 2 +- .../nitroscreenrecorder+autolinking.gradle | 2 +- .../android/nitroscreenrecorderOnLoad.cpp | 2 +- .../android/nitroscreenrecorderOnLoad.hpp | 2 +- .../ios/NitroScreenRecorder+autolinking.rb | 2 +- .../NitroScreenRecorder-Swift-Cxx-Bridge.cpp | 2 +- .../NitroScreenRecorder-Swift-Cxx-Bridge.hpp | 14 +- ...NitroScreenRecorder-Swift-Cxx-Umbrella.hpp | 5 +- .../ios/NitroScreenRecorderAutolinking.mm | 2 +- .../ios/NitroScreenRecorderAutolinking.swift | 2 +- .../HybridNitroScreenRecorderSpecSwift.cpp | 2 +- .../HybridNitroScreenRecorderSpecSwift.hpp | 27 +- .../ios/swift/AudioRecordingFile.swift | 2 +- .../BroadcastPickerPresentationEvent.swift | 2 +- .../generated/ios/swift/CameraDevice.swift | 2 +- .../generated/ios/swift/ExtensionStatus.swift | 79 +++ nitrogen/generated/ios/swift/Func_void.swift | 2 +- ...oid_BroadcastPickerPresentationEvent.swift | 2 +- .../swift/Func_void_PermissionResponse.swift | 2 +- .../ios/swift/Func_void_RecordingError.swift | 2 +- .../Func_void_ScreenRecordingEvent.swift | 2 +- .../swift/Func_void_ScreenRecordingFile.swift | 2 +- .../swift/Func_void_std__exception_ptr.swift | 2 +- ...d_std__optional_ScreenRecordingFile_.swift | 2 +- .../swift/HybridNitroScreenRecorderSpec.swift | 5 +- .../HybridNitroScreenRecorderSpec_cxx.swift | 50 +- .../ios/swift/PermissionResponse.swift | 2 +- .../ios/swift/PermissionStatus.swift | 2 +- .../ios/swift/RecorderCameraStyle.swift | 2 +- .../generated/ios/swift/RecordingError.swift | 2 +- .../ios/swift/RecordingEventReason.swift | 2 +- .../ios/swift/RecordingEventType.swift | 2 +- .../ios/swift/ScreenRecordingEvent.swift | 2 +- .../ios/swift/ScreenRecordingFile.swift | 2 +- .../shared/c++/AudioRecordingFile.hpp | 2 +- .../c++/BroadcastPickerPresentationEvent.hpp | 2 +- .../generated/shared/c++/CameraDevice.hpp | 2 +- .../generated/shared/c++/ExtensionStatus.hpp | 91 +++ .../c++/HybridNitroScreenRecorderSpec.cpp | 5 +- .../c++/HybridNitroScreenRecorderSpec.hpp | 8 +- .../shared/c++/PermissionResponse.hpp | 2 +- .../generated/shared/c++/PermissionStatus.hpp | 2 +- .../shared/c++/RecorderCameraStyle.hpp | 2 +- .../generated/shared/c++/RecordingError.hpp | 2 +- .../shared/c++/RecordingEventReason.hpp | 2 +- .../shared/c++/RecordingEventType.hpp | 2 +- .../shared/c++/ScreenRecordingEvent.hpp | 2 +- .../shared/c++/ScreenRecordingFile.hpp | 2 +- package.json | 6 +- src/NitroScreenRecorder.nitro.ts | 11 + src/functions.ts | 134 ++++ src/types.ts | 28 + 105 files changed, 2850 insertions(+), 413 deletions(-) create mode 100644 nitrogen/generated/android/c++/JExtensionStatus.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/nitroscreenrecorder/ExtensionStatus.kt create mode 100644 nitrogen/generated/ios/swift/ExtensionStatus.swift create mode 100644 nitrogen/generated/shared/c++/ExtensionStatus.hpp diff --git a/example/app.json b/example/app.json index 6fa8457..c60d710 100644 --- a/example/app.json +++ b/example/app.json @@ -21,11 +21,11 @@ "**/*" ], "ios": { - "bundleIdentifier": "nitroscreenrecorderexample.example", + "bundleIdentifier": "com.listenlabs.screenrecorderexample", "infoPlist": { "NSMicrophoneUsageDescription": "ScreenRecorder needs access to your Microphone." }, - "appleTeamId": "32U9AXHQSK" + "appleTeamId": "96YBPK674P" }, "android": { "package": "nitroscreenrecorderexample.example", diff --git a/example/src/App.tsx b/example/src/App.tsx index 1672c25..e8d4198 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,146 +1,219 @@ import { View, StyleSheet, - Button, Text, ScrollView, Platform, + TouchableOpacity, + Alert, } from 'react-native'; import * as ScreenRecorder from '../../'; import { useVideoPlayer, VideoView } from 'expo-video'; -import { useState } from 'react'; -import { getVideoInfoAsync } from 'expo-video-metadata'; +import { useState, useCallback, useEffect } from 'react'; + +type Chunk = { + id: number; + file: ScreenRecorder.ScreenRecordingFile; + timestamp: Date; +}; export default function App() { + // In-app recording state const [inAppRecording, setInAppRecording] = useState< ScreenRecorder.ScreenRecordingFile | undefined >(); + // Global recording state const [globalRecording, setGlobalRecording] = useState< ScreenRecorder.ScreenRecordingFile | undefined >(); + // Chunking state + const [chunks, setChunks] = useState([]); + const [chunkCounter, setChunkCounter] = useState(0); + const [isChunkingActive, setIsChunkingActive] = useState(false); + const [selectedChunk, setSelectedChunk] = useState(); + + // Extension status + const [extensionStatus, setExtensionStatus] = + useState({ + isAlive: false, + isMicActive: false, + isCapturing: false, + chunkStartedAt: 0, + lastHeartbeat: 0, + }); + const { isRecording } = ScreenRecorder.useGlobalRecording({ onRecordingStarted: () => { - console.log('Recording started'); + console.log('๐ŸŽฌ Recording started'); }, onRecordingFinished: () => { - console.log('Recording ended'); + console.log('๐Ÿ›‘ Recording ended'); + setIsChunkingActive(false); }, onBroadcastModalShown: () => { - console.log('Modal showing'); + console.log('๐Ÿ“ฑ Modal showing'); }, onBroadcastModalDismissed: () => { - console.log('Modal dismissed'); + console.log('๐Ÿ“ฑ Modal dismissed'); }, }); - // @ts-ignore - const inAppPlayer = useVideoPlayer(inAppRecording?.path); - // @ts-ignore - const globalPlayer = useVideoPlayer(globalRecording?.path); - // Permission Functions - const getCameraPermissionStatus = () => { - console.log('CAMERA STATUS:', ScreenRecorder.getCameraPermissionStatus()); - }; + // Video players + const inAppPlayer = useVideoPlayer(inAppRecording?.path ?? null); + const globalPlayer = useVideoPlayer(globalRecording?.path ?? null); + const chunkPlayer = useVideoPlayer(selectedChunk?.file.path ?? null); - const getMicrophonePermissionStatus = () => { - console.log('MIC STATUS:', ScreenRecorder.getMicrophonePermissionStatus()); - }; + // Poll extension status while recording + useEffect(() => { + if (!isRecording) { + setExtensionStatus({ + isAlive: false, + isMicActive: false, + isCapturing: false, + chunkStartedAt: 0, + lastHeartbeat: 0, + }); + return; + } - const requestCameraPermission = () => { - ScreenRecorder.requestCameraPermission().then((status) => { - console.log('Received Camera Status:', JSON.stringify(status, null, 2)); - }); - }; + const interval = setInterval(() => { + const status = ScreenRecorder.getExtensionStatus(); + setExtensionStatus(status); + }, 500); - const requestMicrophonePermission = () => { - ScreenRecorder.requestMicrophonePermission().then((status) => { - console.log('Received Mic Status:', JSON.stringify(status, null, 2)); - }); - }; + return () => clearInterval(interval); + }, [isRecording]); - // Recording Options - const options: ScreenRecorder.InAppRecordingOptions = { - enableMic: true, - enableCamera: true, - cameraPreviewStyle: { - width: 150, - height: 200, - top: 30, - left: 20, - borderRadius: 10, - }, - cameraDevice: 'back', + // Permission Functions + const requestPermissions = async () => { + const mic = await ScreenRecorder.requestMicrophonePermission(); + console.log('Mic permission:', mic.status); + if (Platform.OS === 'ios') { + const cam = await ScreenRecorder.requestCameraPermission(); + console.log('Camera permission:', cam.status); + } + Alert.alert('Permissions Requested', 'Check console for status'); }; // In-App Recording Functions const handleStartInAppRecording = async () => { try { await ScreenRecorder.startInAppRecording({ - options, + options: { + enableMic: true, + enableCamera: false, + }, onRecordingFinished(file) { - console.log( - 'In-app recording finished:', - JSON.stringify(file, null, 2) - ); + console.log('โœ… In-app recording finished:', file.name); setInAppRecording(file); }, }); } catch (error) { - console.error('โŒ Error starting recording:', error); + console.error('โŒ Error starting in-app recording:', error); + Alert.alert('Error', String(error)); } }; - const handleStopInAppRecording = () => { - ScreenRecorder.stopInAppRecording(); - }; - - const handleCancelInAppRecording = () => { - ScreenRecorder.cancelInAppRecording(); - console.log('In-app recording cancelled'); + const handleStopInAppRecording = async () => { + const file = await ScreenRecorder.stopInAppRecording(); + if (file) { + setInAppRecording(file); + } }; // Global Recording Functions const handleStartGlobalRecording = () => { + // Reset chunking state when starting new recording + setChunks([]); + setChunkCounter(0); + setIsChunkingActive(false); + setSelectedChunk(undefined); + ScreenRecorder.startGlobalRecording({ options: { enableMic: true, }, onRecordingError: (error) => { - console.log('Global recording error', error); + console.error('โŒ Global recording error:', error); + Alert.alert('Recording Error', error.message); }, }); }; - const checkFileAccessibility = async ( - file: ScreenRecorder.ScreenRecordingFile | undefined - ) => { + const handleStopGlobalRecording = async () => { + const file = await ScreenRecorder.stopGlobalRecording(); if (file) { - try { - console.log('Checking file accessibility for path', file.path); - const data = await getVideoInfoAsync(file.path); - console.log(JSON.stringify(data, null, 2)); - } catch (error) { - console.error('Error', error); - } + setGlobalRecording(file); + console.log('โœ… Global recording stopped:', file.name); } + setIsChunkingActive(false); }; - const handleStopGlobalRecording = async () => { - const newFile = await ScreenRecorder.stopGlobalRecording(); - setGlobalRecording(newFile); - await checkFileAccessibility(newFile); + // Chunking Functions + const handleMarkChunkStart = useCallback(() => { + if (!isRecording) { + Alert.alert('Not Recording', 'Start a global recording first'); + return; + } + ScreenRecorder.markChunkStart(); + setIsChunkingActive(true); + console.log('๐Ÿ“ Chunk start marked'); + Alert.alert('Chunk Started', 'Recording content from this point...'); + }, [isRecording]); + + const handleFinalizeChunk = useCallback(async () => { + if (!isRecording) { + Alert.alert('Not Recording', 'Start a global recording first'); + return; + } + if (!isChunkingActive) { + Alert.alert('No Active Chunk', 'Call markChunkStart() first'); + return; + } + + console.log('๐Ÿ“ฆ Finalizing chunk...'); + const file = await ScreenRecorder.finalizeChunk({ settledTimeMs: 1000 }); + + if (file) { + const newChunk: Chunk = { + id: chunkCounter + 1, + file, + timestamp: new Date(), + }; + setChunks((prev) => [...prev, newChunk]); + setChunkCounter((prev) => prev + 1); + setSelectedChunk(newChunk); + console.log(`โœ… Chunk ${newChunk.id} finalized:`, file.name); + Alert.alert( + 'Chunk Finalized', + `Chunk ${newChunk.id} saved (${(file.size / 1024).toFixed(1)} KB, ${file.duration.toFixed(1)}s)` + ); + } else { + console.log('โš ๏ธ No chunk file returned'); + Alert.alert('Error', 'Failed to get chunk file'); + } + }, [isRecording, isChunkingActive, chunkCounter]); + + const handleClearChunks = () => { + setChunks([]); + setChunkCounter(0); + setSelectedChunk(undefined); + ScreenRecorder.clearCache(); + console.log('๐Ÿ—‘๏ธ Chunks cleared'); }; - const handleGetGlobalRecordingFile = async () => { - const newFile = ScreenRecorder.retrieveLastGlobalRecording(); - setGlobalRecording(newFile); - await checkFileAccessibility(newFile); + const formatDuration = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; }; - const handleClearRecordingCache = () => { - ScreenRecorder.clearCache(); + const formatSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; }; return ( @@ -148,125 +221,201 @@ export default function App() { style={styles.container} contentContainerStyle={styles.contentContainer} > + {/* Header */} + + Screen Recorder Demo + + {isRecording ? '๐Ÿ”ด Recording Active' : 'โšช Not Recording'} + + + {/* Permissions Section */} - Permissions - {Platform.OS === 'ios' && ( - <> - Camera - - -