@capgo/capacitor-screen-recorder 8.3.17 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,27 @@
1
1
  # Capacitor-screen-recorder
2
- <a href="https://capgo.app/"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-screen-recorder" alt="Capgo - Instant updates for Capacitor" /></a>
2
+
3
+ Record the device screen from your Capacitor app, with optional audio, for bug reports, tutorials and demos.
4
+
5
+ <a href="https://capgo.app/?ref=plugin_screen_recorder"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-screen-recorder" alt="Capgo - Instant updates for Capacitor" /></a>
3
6
 
4
7
  <div align="center">
5
- <h2><a href="https://capgo.app/?ref=plugin_screen_recorder"> ➡️ Get Instant updates for your App with Capgo</a></h2>
6
- <h2><a href="https://capgo.app/consulting/?ref=plugin_screen_recorder"> Missing a feature? We’ll build the plugin for you 💪</a></h2>
8
+ <p><b>Capgo</b>: push fixes to your Capacitor users in minutes, build signed iOS and Android apps without a Mac, and roll back in one click.</p>
9
+ <h2><a href="https://capgo.app/register/?ref=plugin_screen_recorder">➡️ Get started for free</a></h2>
10
+ <p>14-day unlimited free trial. No credit card required</p>
11
+ <p><a href="https://capgo.app/consulting/?ref=plugin_screen_recorder">Missing a feature? We'll build the plugin for you 💪</a></p>
7
12
  </div>
8
- Record device's screen
13
+
14
+ <p align="center">
15
+ <img src="https://raw.githubusercontent.com/Cap-go/capacitor-screen-recorder/main/assets/github-social-preview.png" alt="@capgo/capacitor-screen-recorder for Capacitor apps" width="300" />
16
+ </p>
17
+
18
+ ## Key features
19
+
20
+ - **Start**: `start()` begins recording after the system consent prompt.
21
+ - **Stop**: `stop()` ends the recording.
22
+ - **Audio**: set `recordAudio` to capture sound with the video.
23
+ - **Native capture**: ReplayKit on iOS, saved to the Photos library, and MediaProjection on Android.
24
+ - **Platforms**: iOS and Android. Not available on web.
9
25
 
10
26
  ## Documentation
11
27
 
@@ -103,6 +119,8 @@ No configuration required for this plugin.
103
119
 
104
120
  * [`start(...)`](#start)
105
121
  * [`stop()`](#stop)
122
+ * [`addListener('onStopped', ...)`](#addlisteneronstopped-)
123
+ * [`removeAllListeners()`](#removealllisteners)
106
124
  * [`getPluginVersion()`](#getpluginversion)
107
125
  * [Interfaces](#interfaces)
108
126
  * [Type Aliases](#type-aliases)
@@ -154,6 +172,39 @@ recording. On Android, the video is saved directly to the gallery.
154
172
  --------------------
155
173
 
156
174
 
175
+ ### addListener('onStopped', ...)
176
+
177
+ ```typescript
178
+ addListener(eventName: 'onStopped', listenerFunc: (event: ScreenRecorderStoppedEvent) => void) => Promise<PluginListenerHandle>
179
+ ```
180
+
181
+ Listen for recordings that ended without {@link stop} being called.
182
+
183
+ | Param | Type | Description |
184
+ | ------------------ | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
185
+ | **`eventName`** | <code>'onStopped'</code> | - `onStopped` |
186
+ | **`listenerFunc`** | <code>(event: <a href="#screenrecorderstoppedevent">ScreenRecorderStoppedEvent</a>) =&gt; void</code> | - Called with a `ScreenRecorderStoppedEvent` (`url` and optional `error`) when the recording ended outside `stop()`. |
187
+
188
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
189
+
190
+ **Since:** 8.4.0
191
+
192
+ --------------------
193
+
194
+
195
+ ### removeAllListeners()
196
+
197
+ ```typescript
198
+ removeAllListeners() => Promise<void>
199
+ ```
200
+
201
+ Remove all listeners registered with {@link addListener}.
202
+
203
+ **Since:** 8.4.0
204
+
205
+ --------------------
206
+
207
+
157
208
  ### getPluginVersion()
158
209
 
159
210
  ```typescript
@@ -182,6 +233,28 @@ Options for {@link ScreenRecorderPlugin.start}.
182
233
  | **`format`** | <code><a href="#screenrecordervideoformat">ScreenRecorderVideoFormat</a> \| 'video/mp4' \| 'video/quicktime'</code> | Video container format for the saved recording. Accepts `mp4`, `mov`, or MIME types `video/mp4` and `video/quicktime`. iOS supports both `mp4` and `mov`. Android records MPEG-4 (`.mp4`) regardless of this value. | <code>'mp4'</code> | 8.3.0 |
183
234
 
184
235
 
236
+ #### PluginListenerHandle
237
+
238
+ | Prop | Type |
239
+ | ------------ | ----------------------------------------- |
240
+ | **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |
241
+
242
+
243
+ #### ScreenRecorderStoppedEvent
244
+
245
+ Payload for the `onStopped` event.
246
+
247
+ Fired when a recording ends without {@link ScreenRecorderPlugin.stop} being
248
+ called: the user stopped capture from the system UI (Android "Stop sharing"),
249
+ the recorder hit a limit (max duration or max file size), or the system
250
+ interrupted capture (iOS, for example an incoming call).
251
+
252
+ | Prop | Type | Description | Since |
253
+ | ----------- | ------------------- | ------------------------------------------------------------------------------------------------------------------ | ----- |
254
+ | **`url`** | <code>string</code> | Local URI of the finished recording (`file://` on Android). Empty string when the file path could not be resolved. | 8.4.0 |
255
+ | **`error`** | <code>string</code> | Error message when the recording ended with a failure. | 8.4.0 |
256
+
257
+
185
258
  ### Type Aliases
186
259
 
187
260
 
@@ -38,6 +38,13 @@ class CapgoScrCast private constructor(
38
38
  private var serviceBinder: CapgoRecorderService? = null
39
39
  private var outputFile: File? = null
40
40
  private var startListener: StartListener? = null
41
+ private var stopRequested = false
42
+ private var sessionGeneration = 0L
43
+
44
+ /**
45
+ * Invoked when a recording ends without {@link #stopRecording()} having been called.
46
+ */
47
+ var externalStopListener: ExternalStopListener? = null
41
48
  private var receiverRegistered = false
42
49
 
43
50
  private val metrics by lazy {
@@ -68,11 +75,30 @@ class CapgoScrCast private constructor(
68
75
  override fun onReceive(context: Context?, intent: Intent?) {
69
76
  when (intent?.action) {
70
77
  STATE_RECORDING -> {
78
+ val broadcastGeneration =
79
+ intent?.getLongExtra(EXTRA_SESSION_GENERATION, -1L) ?: -1L
80
+ if (broadcastGeneration != sessionGeneration) {
81
+ return
82
+ }
83
+ if (stopRequested) {
84
+ startListener?.onFailed(
85
+ IllegalStateException("Recording stopped before it started"),
86
+ )
87
+ startListener = null
88
+ broadcaster.sendBroadcast(Intent(Action.Stop.name))
89
+ return
90
+ }
71
91
  startListener?.onStarted()
72
92
  startListener = null
73
93
  }
74
94
  STATE_IDLE -> {
95
+ val broadcastGeneration =
96
+ intent?.getLongExtra(EXTRA_SESSION_GENERATION, -1L) ?: -1L
97
+ if (broadcastGeneration != sessionGeneration) {
98
+ return
99
+ }
75
100
  val startPending = startListener != null
101
+ val sessionActive = recordingSession != null
76
102
  val error = intent.getSerializableExtra(EXTRA_ERROR) as? Throwable
77
103
  if (error != null) {
78
104
  startListener?.onFailed(error)
@@ -81,7 +107,11 @@ class CapgoScrCast private constructor(
81
107
  IllegalStateException("Recording stopped before it started"),
82
108
  )
83
109
  }
84
- cleanupSession(deleteEmptyOutput = startPending || error != null)
110
+ val savedPath = cleanupSession(deleteEmptyOutput = startPending || error != null)
111
+ if (!startPending && sessionActive && !stopRequested) {
112
+ externalStopListener?.onExternalStop(savedPath, error?.message)
113
+ }
114
+ stopRequested = false
85
115
  }
86
116
  }
87
117
  }
@@ -90,6 +120,10 @@ class CapgoScrCast private constructor(
90
120
  private val requestRuntimePermissions = activity.registerForActivityResult(
91
121
  ActivityResultContracts.RequestMultiplePermissions(),
92
122
  ) { _ ->
123
+ if (startListener == null || stopRequested) {
124
+ abandonCanceledPendingStart()
125
+ return@registerForActivityResult
126
+ }
93
127
  if (requiredRuntimePermissions().all { isPermissionGranted(it) }) {
94
128
  startProjection.launch(Unit)
95
129
  } else {
@@ -98,6 +132,10 @@ class CapgoScrCast private constructor(
98
132
  }
99
133
 
100
134
  private val startProjection = activity.registerForActivityResult(CapgoRecordScreen()) { result ->
135
+ if (startListener == null || stopRequested) {
136
+ abandonCanceledPendingStart()
137
+ return@registerForActivityResult
138
+ }
101
139
  if (result.resultCode != Activity.RESULT_OK) {
102
140
  notifyStartFailed(IllegalStateException("Screen capture permission denied"))
103
141
  return@registerForActivityResult
@@ -155,9 +193,23 @@ class CapgoScrCast private constructor(
155
193
  }
156
194
 
157
195
  fun stopRecording() {
196
+ stopRequested = true
197
+ if (recordingSession == null && startListener != null) {
198
+ abandonCanceledPendingStart()
199
+ return
200
+ }
158
201
  broadcaster.sendBroadcast(Intent(Action.Stop.name))
159
202
  }
160
203
 
204
+ private fun abandonCanceledPendingStart() {
205
+ if (startListener == null) {
206
+ stopRequested = false
207
+ return
208
+ }
209
+ notifyStartFailed(IllegalStateException("Recording stopped before it started"))
210
+ stopRequested = false
211
+ }
212
+
161
213
  private fun resolveVideoSize(options: Options): Options {
162
214
  return VideoSizeResolver.applyTo(options, metrics.widthPixels, metrics.heightPixels)
163
215
  }
@@ -168,6 +220,11 @@ class CapgoScrCast private constructor(
168
220
  }
169
221
 
170
222
  private fun startService(result: ActivityResult, file: File) {
223
+ if (startListener == null || stopRequested) {
224
+ abandonCanceledPendingStart()
225
+ return
226
+ }
227
+ sessionGeneration += 1
171
228
  outputFile = file
172
229
  val session = Intent(activity, CapgoRecorderService::class.java).apply {
173
230
  putExtra("code", result.resultCode)
@@ -177,6 +234,7 @@ class CapgoScrCast private constructor(
177
234
  putExtra("dpi", dpi)
178
235
  putExtra("rotation", activity.windowManager.defaultDisplay.rotation)
179
236
  putExtra("recordAudio", recordAudio)
237
+ putExtra(EXTRA_SESSION_GENERATION, sessionGeneration)
180
238
  }
181
239
  recordingSession = session
182
240
 
@@ -201,6 +259,7 @@ class CapgoScrCast private constructor(
201
259
  unregisterRecordingReceiver()
202
260
  recordingSession = null
203
261
  outputFile = null
262
+ stopRequested = false
204
263
  notifyStartFailed(error)
205
264
  }
206
265
 
@@ -216,7 +275,7 @@ class CapgoScrCast private constructor(
216
275
  receiverRegistered = false
217
276
  }
218
277
 
219
- private fun cleanupSession(deleteEmptyOutput: Boolean = false) {
278
+ private fun cleanupSession(deleteEmptyOutput: Boolean = false): String? {
220
279
  startListener = null
221
280
  unregisterRecordingReceiver()
222
281
 
@@ -229,17 +288,24 @@ class CapgoScrCast private constructor(
229
288
  recordingSession?.let { activity.stopService(it) }
230
289
  recordingSession = null
231
290
 
232
- outputFile?.let { file ->
291
+ val savedPath = outputFile?.absolutePath
292
+ val deliverablePath = savedPath?.let { path ->
293
+ val file = File(path)
233
294
  if (file.isFile && file.length() > 0L) {
234
- MediaScannerConnection.scanFile(activity, arrayOf(file.absolutePath), null) { path, uri ->
235
- Log.i("CapgoScreenRecorder", "Saved recording: $path uri=$uri")
295
+ MediaScannerConnection.scanFile(activity, arrayOf(path), null) { scanPath, uri ->
296
+ Log.i("CapgoScreenRecorder", "Saved recording: $scanPath uri=$uri")
297
+ }
298
+ path
299
+ } else {
300
+ if (deleteEmptyOutput && file.exists()) {
301
+ runCatching { file.delete() }
236
302
  }
237
- } else if (deleteEmptyOutput && file.exists()) {
238
- runCatching { file.delete() }
303
+ null
239
304
  }
240
305
  }
241
306
  outputFile = null
242
307
  CapgoRecordingCoordinator.release()
308
+ return deliverablePath
243
309
  }
244
310
 
245
311
  interface StartListener {
@@ -247,7 +313,13 @@ class CapgoScrCast private constructor(
247
313
  fun onFailed(error: Throwable)
248
314
  }
249
315
 
316
+ fun interface ExternalStopListener {
317
+ fun onExternalStop(path: String?, error: String?)
318
+ }
319
+
250
320
  companion object {
321
+ const val EXTRA_SESSION_GENERATION = "capgoSessionGeneration"
322
+
251
323
  @JvmStatic
252
324
  fun use(activity: ComponentActivity, recordAudio: Boolean): CapgoScrCast {
253
325
  return CapgoScrCast(activity, recordAudio)
@@ -1,20 +1,23 @@
1
1
  package ee.forgr.plugin.screenrecorder;
2
2
 
3
+ import android.net.Uri;
3
4
  import com.getcapacitor.JSObject;
4
5
  import com.getcapacitor.Plugin;
5
6
  import com.getcapacitor.PluginCall;
6
7
  import com.getcapacitor.PluginMethod;
7
8
  import com.getcapacitor.annotation.CapacitorPlugin;
8
9
  import dev.bmcreations.scrcast.config.Options;
10
+ import java.io.File;
9
11
 
10
12
  @CapacitorPlugin(name = "ScreenRecorder")
11
13
  public class ScreenRecorderPlugin extends Plugin {
12
14
 
13
- private final String pluginVersion = "8.3.17";
15
+ private final String pluginVersion = "8.4.0";
14
16
 
15
17
  private CapgoScrCast videoRecorder;
16
18
  private CapgoScrCast audioRecorder;
17
- private boolean recordingWithAudio = false;
19
+ private CapgoScrCast pendingRecorder = null;
20
+ private CapgoScrCast activeRecorder = null;
18
21
 
19
22
  @Override
20
23
  public void load() {
@@ -23,17 +26,35 @@ public class ScreenRecorderPlugin extends Plugin {
23
26
  final Options options = new Options();
24
27
  videoRecorder.updateOptions(options);
25
28
  audioRecorder.updateOptions(options);
29
+
30
+ final CapgoScrCast.ExternalStopListener externalStopListener = new CapgoScrCast.ExternalStopListener() {
31
+ @Override
32
+ public void onExternalStop(final String path, final String error) {
33
+ if (activeRecorder == null) {
34
+ return;
35
+ }
36
+ activeRecorder = null;
37
+ final JSObject ret = new JSObject();
38
+ ret.put("url", path != null ? Uri.fromFile(new File(path)).toString() : "");
39
+ if (error != null) {
40
+ ret.put("error", error);
41
+ }
42
+ notifyListeners("onStopped", ret);
43
+ }
44
+ };
45
+ videoRecorder.setExternalStopListener(externalStopListener);
46
+ audioRecorder.setExternalStopListener(externalStopListener);
26
47
  }
27
48
 
28
49
  @PluginMethod
29
50
  public void start(final PluginCall call) {
30
51
  boolean keptAlive = false;
52
+ CapgoScrCast startRecorder = null;
31
53
  try {
32
54
  final boolean recordAudio = call.getBoolean("recordAudio", false);
33
55
  final String format = call.getString("format");
34
- recordingWithAudio = recordAudio;
35
-
36
- final CapgoScrCast recorder = recordAudio ? audioRecorder : videoRecorder;
56
+ startRecorder = recordAudio ? audioRecorder : videoRecorder;
57
+ final CapgoScrCast recorder = startRecorder;
37
58
  final Options configuredOptions = VideoFormatResolver.INSTANCE.applyTo(recorder.getOptions(), format);
38
59
  recorder.updateOptions(configuredOptions);
39
60
  recorder.updateVideoFormat(format);
@@ -44,13 +65,22 @@ public class ScreenRecorderPlugin extends Plugin {
44
65
  new CapgoScrCast.StartListener() {
45
66
  @Override
46
67
  public void onStarted() {
68
+ if (pendingRecorder == recorder) {
69
+ pendingRecorder = null;
70
+ }
71
+ activeRecorder = recorder;
47
72
  call.resolve();
48
73
  call.release(bridge);
49
74
  }
50
75
 
51
76
  @Override
52
77
  public void onFailed(final Throwable error) {
53
- recordingWithAudio = false;
78
+ if (pendingRecorder == recorder) {
79
+ pendingRecorder = null;
80
+ }
81
+ if (activeRecorder == recorder) {
82
+ activeRecorder = null;
83
+ }
54
84
  final Exception exception = error instanceof Exception ? (Exception) error : new Exception(error);
55
85
  call.reject("Could not start screen recording", exception);
56
86
  call.release(bridge);
@@ -58,12 +88,15 @@ public class ScreenRecorderPlugin extends Plugin {
58
88
  }
59
89
  );
60
90
  if (!started) {
61
- recordingWithAudio = false;
62
91
  call.reject("Could not start screen recording", new IllegalStateException("A screen recording is already in progress"));
63
92
  call.release(bridge);
93
+ } else {
94
+ pendingRecorder = recorder;
64
95
  }
65
96
  } catch (final Exception e) {
66
- recordingWithAudio = false;
97
+ if (startRecorder != null && pendingRecorder == startRecorder) {
98
+ pendingRecorder = null;
99
+ }
67
100
  call.reject("Could not start screen recording", e);
68
101
  if (keptAlive) {
69
102
  call.release(bridge);
@@ -74,12 +107,12 @@ public class ScreenRecorderPlugin extends Plugin {
74
107
  @PluginMethod
75
108
  public void stop(PluginCall call) {
76
109
  try {
77
- if (recordingWithAudio) {
78
- audioRecorder.stopRecording();
79
- } else {
80
- videoRecorder.stopRecording();
110
+ final CapgoScrCast recorder = activeRecorder != null ? activeRecorder : pendingRecorder;
111
+ if (recorder != null) {
112
+ recorder.stopRecording();
81
113
  }
82
- recordingWithAudio = false;
114
+ pendingRecorder = null;
115
+ activeRecorder = null;
83
116
  call.resolve();
84
117
  } catch (final Exception e) {
85
118
  call.reject("Could not stop screen recording", e);
@@ -23,6 +23,7 @@ import dev.bmcreations.scrcast.internal.recorder.*
23
23
  import dev.bmcreations.scrcast.internal.recorder.service.orientations
24
24
  import dev.bmcreations.scrcast.recorder.*
25
25
  import dev.bmcreations.scrcast.recorder.notification.NotificationProvider
26
+ import ee.forgr.plugin.screenrecorder.CapgoScrCast
26
27
  import ee.forgr.plugin.screenrecorder.MediaRecorderPrepare
27
28
  import kotlinx.coroutines.Dispatchers
28
29
  import kotlinx.coroutines.GlobalScope
@@ -66,10 +67,13 @@ class CapgoRecorderService : Service() {
66
67
  }
67
68
  }
68
69
 
70
+ private var sessionGeneration: Long = 0L
71
+
69
72
  private var state: RecordingState = RecordingState.Idle()
70
73
  set(value) {
71
74
  field = value
72
75
  broadcaster.sendBroadcast(Intent(value.stateString()).apply {
76
+ putExtra(CapgoScrCast.EXTRA_SESSION_GENERATION, sessionGeneration)
73
77
  if (value is RecordingState.Delay) {
74
78
  putExtra(EXTRA_DELAY_REMAINING, value.remainingSeconds)
75
79
  } else if (value is RecordingState.Idle) {
@@ -112,7 +116,8 @@ class CapgoRecorderService : Service() {
112
116
  }
113
117
 
114
118
  private var mediaRecorder: MediaRecorder? = null
115
-
119
+ private var recordingStarted = false
120
+ private var prepareInProgress = false
116
121
 
117
122
  private fun createMediaRecorder(): MediaRecorder {
118
123
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -238,6 +243,7 @@ class CapgoRecorderService : Service() {
238
243
 
239
244
  private fun recordInternal(code: Int, data: Intent) {
240
245
  GlobalScope.launch(Dispatchers.Main) {
246
+ prepareInProgress = true
241
247
  startForeground(
242
248
  notificationProvider.getNotificationId(),
243
249
  notificationProvider.get(state)
@@ -259,44 +265,73 @@ class CapgoRecorderService : Service() {
259
265
 
260
266
  mediaProjection?.registerCallback(mediaProjectionCallback, Handler())
261
267
  if (!createRecorder()) {
268
+ prepareInProgress = false
262
269
  stopRecording(IOException("MediaRecorder prepare failed"))
263
270
  return@launch
264
271
  }
265
272
  virtualDisplay // touch
266
273
  try {
267
274
  mediaRecorder?.start()
275
+ recordingStarted = true
268
276
  state = RecordingState.Recording
269
277
  notificationProvider.update(state)
270
278
  } catch (e: Exception) {
271
279
  stopRecording(e)
280
+ } finally {
281
+ prepareInProgress = false
272
282
  }
273
283
  }
274
284
  }
275
285
 
276
286
  private fun stopRecording(error: Throwable? = null) {
287
+ finalizeRecorder()
277
288
  mediaProjection?.stop()
278
-
289
+ mediaProjection = null
279
290
  state = RecordingState.Idle(error)
280
291
  stopForeground(true)
281
292
  }
282
293
 
283
- private fun cleanupProjection() {
294
+ private fun finalizeRecorder() {
284
295
  mediaProjection?.unregisterCallback(mediaProjectionCallback)
285
- mediaProjection = null
286
-
287
296
  _virtualDisplay?.release()
288
297
  _virtualDisplay = null
289
-
290
298
  runCatching {
291
299
  mediaRecorder?.stop()
292
300
  }
293
301
  releaseRecorder()
302
+ recordingStarted = false
303
+ prepareInProgress = false
304
+ }
305
+
306
+ private fun cleanupProjection() {
307
+ finalizeRecorder()
308
+ mediaProjection = null
294
309
  }
295
310
 
296
311
  private inner class MediaProjectionCallback : MediaProjection.Callback() {
297
312
  override fun onStop() {
298
313
  Log.d("scrcast", "projection on stop")
314
+ val hadStarted = recordingStarted
315
+ val preparing = prepareInProgress
299
316
  cleanupProjection()
317
+ when {
318
+ state is RecordingState.Idle && (state as RecordingState.Idle).error != null -> {
319
+ // stopRecording() already published Idle with an error.
320
+ }
321
+ state is RecordingState.Idle && hadStarted -> {
322
+ // stopRecording() already published Idle for an active recording.
323
+ }
324
+ !hadStarted || preparing -> {
325
+ state = RecordingState.Idle(
326
+ IllegalStateException("Recording stopped before it started"),
327
+ )
328
+ stopForeground(true)
329
+ }
330
+ else -> {
331
+ state = RecordingState.Idle()
332
+ stopForeground(true)
333
+ }
334
+ }
300
335
  }
301
336
  }
302
337
 
@@ -307,6 +342,7 @@ class CapgoRecorderService : Service() {
307
342
  dpi = it.getFloatExtra("dpi", 0f)
308
343
  outputFile = it.getStringExtra("outputFile") ?: ""
309
344
  recordAudio = it.getBooleanExtra("recordAudio", false)
345
+ sessionGeneration = it.getLongExtra(CapgoScrCast.EXTRA_SESSION_GENERATION, 0L)
310
346
 
311
347
  startRecording(
312
348
  code = it.getIntExtra("code", -1),
package/dist/docs.json CHANGED
@@ -84,6 +84,58 @@
84
84
  "complexTypes": [],
85
85
  "slug": "stop"
86
86
  },
87
+ {
88
+ "name": "addListener",
89
+ "signature": "(eventName: 'onStopped', listenerFunc: (event: ScreenRecorderStoppedEvent) => void) => Promise<PluginListenerHandle>",
90
+ "parameters": [
91
+ {
92
+ "name": "eventName",
93
+ "docs": "- `onStopped`",
94
+ "type": "'onStopped'"
95
+ },
96
+ {
97
+ "name": "listenerFunc",
98
+ "docs": "- Called with a `ScreenRecorderStoppedEvent`\n(`url` and optional `error`) when the recording ended outside `stop()`.",
99
+ "type": "(event: ScreenRecorderStoppedEvent) => void"
100
+ }
101
+ ],
102
+ "returns": "Promise<PluginListenerHandle>",
103
+ "tags": [
104
+ {
105
+ "name": "param",
106
+ "text": "eventName - `onStopped`"
107
+ },
108
+ {
109
+ "name": "param",
110
+ "text": "listenerFunc - Called with a `ScreenRecorderStoppedEvent`\n(`url` and optional `error`) when the recording ended outside `stop()`."
111
+ },
112
+ {
113
+ "name": "since",
114
+ "text": "8.4.0"
115
+ }
116
+ ],
117
+ "docs": "Listen for recordings that ended without {@link stop} being called.",
118
+ "complexTypes": [
119
+ "PluginListenerHandle",
120
+ "ScreenRecorderStoppedEvent"
121
+ ],
122
+ "slug": "addlisteneronstopped-"
123
+ },
124
+ {
125
+ "name": "removeAllListeners",
126
+ "signature": "() => Promise<void>",
127
+ "parameters": [],
128
+ "returns": "Promise<void>",
129
+ "tags": [
130
+ {
131
+ "name": "since",
132
+ "text": "8.4.0"
133
+ }
134
+ ],
135
+ "docs": "Remove all listeners registered with {@link addListener}.",
136
+ "complexTypes": [],
137
+ "slug": "removealllisteners"
138
+ },
87
139
  {
88
140
  "name": "getPluginVersion",
89
141
  "signature": "() => Promise<{ version: string; }>",
@@ -166,6 +218,60 @@
166
218
  "type": "ScreenRecorderVideoFormat | 'video/mp4' | 'video/quicktime' | undefined"
167
219
  }
168
220
  ]
221
+ },
222
+ {
223
+ "name": "PluginListenerHandle",
224
+ "slug": "pluginlistenerhandle",
225
+ "docs": "",
226
+ "tags": [],
227
+ "methods": [],
228
+ "properties": [
229
+ {
230
+ "name": "remove",
231
+ "tags": [],
232
+ "docs": "",
233
+ "complexTypes": [],
234
+ "type": "() => Promise<void>"
235
+ }
236
+ ]
237
+ },
238
+ {
239
+ "name": "ScreenRecorderStoppedEvent",
240
+ "slug": "screenrecorderstoppedevent",
241
+ "docs": "Payload for the `onStopped` event.\n\nFired when a recording ends without {@link ScreenRecorderPlugin.stop} being\ncalled: the user stopped capture from the system UI (Android \"Stop sharing\"),\nthe recorder hit a limit (max duration or max file size), or the system\ninterrupted capture (iOS, for example an incoming call).",
242
+ "tags": [
243
+ {
244
+ "text": "8.4.0",
245
+ "name": "since"
246
+ }
247
+ ],
248
+ "methods": [],
249
+ "properties": [
250
+ {
251
+ "name": "url",
252
+ "tags": [
253
+ {
254
+ "text": "8.4.0",
255
+ "name": "since"
256
+ }
257
+ ],
258
+ "docs": "Local URI of the finished recording (`file://` on Android).\nEmpty string when the file path could not be resolved.",
259
+ "complexTypes": [],
260
+ "type": "string"
261
+ },
262
+ {
263
+ "name": "error",
264
+ "tags": [
265
+ {
266
+ "text": "8.4.0",
267
+ "name": "since"
268
+ }
269
+ ],
270
+ "docs": "Error message when the recording ended with a failure.",
271
+ "complexTypes": [],
272
+ "type": "string | undefined"
273
+ }
274
+ ]
169
275
  }
170
276
  ],
171
277
  "enums": [],
@@ -1,3 +1,29 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core';
2
+ /**
3
+ * Payload for the `onStopped` event.
4
+ *
5
+ * Fired when a recording ends without {@link ScreenRecorderPlugin.stop} being
6
+ * called: the user stopped capture from the system UI (Android "Stop sharing"),
7
+ * the recorder hit a limit (max duration or max file size), or the system
8
+ * interrupted capture (iOS, for example an incoming call).
9
+ *
10
+ * @since 8.4.0
11
+ */
12
+ export interface ScreenRecorderStoppedEvent {
13
+ /**
14
+ * Local URI of the finished recording (`file://` on Android).
15
+ * Empty string when the file path could not be resolved.
16
+ *
17
+ * @since 8.4.0
18
+ */
19
+ url: string;
20
+ /**
21
+ * Error message when the recording ended with a failure.
22
+ *
23
+ * @since 8.4.0
24
+ */
25
+ error?: string;
26
+ }
1
27
  /**
2
28
  * Supported video container formats for screen recordings.
3
29
  *
@@ -80,6 +106,21 @@ export interface ScreenRecorderPlugin {
80
106
  * ```
81
107
  */
82
108
  stop(): Promise<void>;
109
+ /**
110
+ * Listen for recordings that ended without {@link stop} being called.
111
+ *
112
+ * @param eventName - `onStopped`
113
+ * @param listenerFunc - Called with a `ScreenRecorderStoppedEvent`
114
+ * (`url` and optional `error`) when the recording ended outside `stop()`.
115
+ * @since 8.4.0
116
+ */
117
+ addListener(eventName: 'onStopped', listenerFunc: (event: ScreenRecorderStoppedEvent) => void): Promise<PluginListenerHandle>;
118
+ /**
119
+ * Remove all listeners registered with {@link addListener}.
120
+ *
121
+ * @since 8.4.0
122
+ */
123
+ removeAllListeners(): Promise<void>;
83
124
  /**
84
125
  * Get the native Capacitor plugin version.
85
126
  *
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Supported video container formats for screen recordings.\n *\n * @since 8.3.0\n */\nexport type ScreenRecorderVideoFormat = 'mp4' | 'mov';\n\n/**\n * Options for {@link ScreenRecorderPlugin.start}.\n *\n * @since 8.3.0\n */\nexport interface StartRecordingOptions {\n /**\n * Whether to record audio along with the screen video.\n *\n * @default false\n * @since 1.0.0\n */\n recordAudio?: boolean;\n\n /**\n * Video container format for the saved recording.\n *\n * Accepts `mp4`, `mov`, or MIME types `video/mp4` and `video/quicktime`.\n * iOS supports both `mp4` and `mov`. Android records MPEG-4 (`.mp4`) regardless of this value.\n *\n * @default 'mp4'\n * @since 8.3.0\n * @example 'mov'\n */\n format?: ScreenRecorderVideoFormat | 'video/mp4' | 'video/quicktime';\n}\n\n/**\n * Capacitor Screen Recorder Plugin for recording the device screen.\n * Allows you to capture video recordings of the screen with optional audio.\n *\n * @since 1.0.0\n */\nexport interface ScreenRecorderPlugin {\n /**\n * Start recording the device screen.\n *\n * Initiates screen recording with optional audio capture. The user will be\n * prompted to grant screen recording permissions if not already granted.\n * On iOS, the system recording UI will be displayed. On Android, the recording\n * starts immediately after permission is granted.\n *\n * @param options - Recording configuration options\n * @param options.recordAudio - Whether to record audio along with the screen video. Defaults to false.\n * @param options.format - Video container format for the saved recording. Defaults to `mp4`.\n * @returns Promise that resolves when recording starts\n * @throws Error if recording fails to start or permissions are denied\n * @since 1.0.0\n * @example\n * ```typescript\n * // Start recording without audio\n * await ScreenRecorder.start();\n *\n * // Start recording with audio\n * await ScreenRecorder.start({ recordAudio: true });\n *\n * // Start recording as MOV on iOS\n * await ScreenRecorder.start({ format: 'mov' });\n * ```\n */\n start(options?: StartRecordingOptions): Promise<void>;\n\n /**\n * Stop the current screen recording.\n *\n * Stops the active screen recording and saves the video to the device's\n * camera roll or gallery. On iOS, the system will show a preview of the\n * recording. On Android, the video is saved directly to the gallery.\n *\n * @returns Promise that resolves when recording stops and the video is saved\n * @throws Error if stopping the recording fails or no recording is active\n * @since 1.0.0\n * @example\n * ```typescript\n * await ScreenRecorder.stop();\n * console.log('Recording saved to gallery');\n * ```\n */\n stop(): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version.\n *\n * @returns Promise that resolves with the plugin version\n * @throws Error if getting the version fails\n * @since 1.0.0\n * @example\n * ```typescript\n * const { version } = await ScreenRecorder.getPluginVersion();\n * console.log('Plugin version:', version);\n * ```\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Payload for the `onStopped` event.\n *\n * Fired when a recording ends without {@link ScreenRecorderPlugin.stop} being\n * called: the user stopped capture from the system UI (Android \"Stop sharing\"),\n * the recorder hit a limit (max duration or max file size), or the system\n * interrupted capture (iOS, for example an incoming call).\n *\n * @since 8.4.0\n */\nexport interface ScreenRecorderStoppedEvent {\n /**\n * Local URI of the finished recording (`file://` on Android).\n * Empty string when the file path could not be resolved.\n *\n * @since 8.4.0\n */\n url: string;\n\n /**\n * Error message when the recording ended with a failure.\n *\n * @since 8.4.0\n */\n error?: string;\n}\n\n/**\n * Supported video container formats for screen recordings.\n *\n * @since 8.3.0\n */\nexport type ScreenRecorderVideoFormat = 'mp4' | 'mov';\n\n/**\n * Options for {@link ScreenRecorderPlugin.start}.\n *\n * @since 8.3.0\n */\nexport interface StartRecordingOptions {\n /**\n * Whether to record audio along with the screen video.\n *\n * @default false\n * @since 1.0.0\n */\n recordAudio?: boolean;\n\n /**\n * Video container format for the saved recording.\n *\n * Accepts `mp4`, `mov`, or MIME types `video/mp4` and `video/quicktime`.\n * iOS supports both `mp4` and `mov`. Android records MPEG-4 (`.mp4`) regardless of this value.\n *\n * @default 'mp4'\n * @since 8.3.0\n * @example 'mov'\n */\n format?: ScreenRecorderVideoFormat | 'video/mp4' | 'video/quicktime';\n}\n\n/**\n * Capacitor Screen Recorder Plugin for recording the device screen.\n * Allows you to capture video recordings of the screen with optional audio.\n *\n * @since 1.0.0\n */\nexport interface ScreenRecorderPlugin {\n /**\n * Start recording the device screen.\n *\n * Initiates screen recording with optional audio capture. The user will be\n * prompted to grant screen recording permissions if not already granted.\n * On iOS, the system recording UI will be displayed. On Android, the recording\n * starts immediately after permission is granted.\n *\n * @param options - Recording configuration options\n * @param options.recordAudio - Whether to record audio along with the screen video. Defaults to false.\n * @param options.format - Video container format for the saved recording. Defaults to `mp4`.\n * @returns Promise that resolves when recording starts\n * @throws Error if recording fails to start or permissions are denied\n * @since 1.0.0\n * @example\n * ```typescript\n * // Start recording without audio\n * await ScreenRecorder.start();\n *\n * // Start recording with audio\n * await ScreenRecorder.start({ recordAudio: true });\n *\n * // Start recording as MOV on iOS\n * await ScreenRecorder.start({ format: 'mov' });\n * ```\n */\n start(options?: StartRecordingOptions): Promise<void>;\n\n /**\n * Stop the current screen recording.\n *\n * Stops the active screen recording and saves the video to the device's\n * camera roll or gallery. On iOS, the system will show a preview of the\n * recording. On Android, the video is saved directly to the gallery.\n *\n * @returns Promise that resolves when recording stops and the video is saved\n * @throws Error if stopping the recording fails or no recording is active\n * @since 1.0.0\n * @example\n * ```typescript\n * await ScreenRecorder.stop();\n * console.log('Recording saved to gallery');\n * ```\n */\n stop(): Promise<void>;\n\n /**\n * Listen for recordings that ended without {@link stop} being called.\n *\n * @param eventName - `onStopped`\n * @param listenerFunc - Called with a `ScreenRecorderStoppedEvent`\n * (`url` and optional `error`) when the recording ended outside `stop()`.\n * @since 8.4.0\n */\n addListener(\n eventName: 'onStopped',\n listenerFunc: (event: ScreenRecorderStoppedEvent) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Remove all listeners registered with {@link addListener}.\n *\n * @since 8.4.0\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version.\n *\n * @returns Promise that resolves with the plugin version\n * @throws Error if getting the version fails\n * @since 1.0.0\n * @example\n * ```typescript\n * const { version } = await ScreenRecorder.getPluginVersion();\n * console.log('Plugin version:', version);\n * ```\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n"]}
@@ -7,7 +7,7 @@ import Capacitor
7
7
  */
8
8
  @objc(ScreenRecorderPlugin)
9
9
  public class ScreenRecorderPlugin: CAPPlugin, CAPBridgedPlugin {
10
- private let pluginVersion: String = "8.3.17"
10
+ private let pluginVersion: String = "8.4.0"
11
11
  public let identifier = "ScreenRecorderPlugin"
12
12
  public let jsName = "ScreenRecorder"
13
13
  public let pluginMethods: [CAPPluginMethod] = [
@@ -17,6 +17,17 @@ public class ScreenRecorderPlugin: CAPPlugin, CAPBridgedPlugin {
17
17
  ]
18
18
  private let implementation = ScreenRecorder()
19
19
 
20
+ override public func load() {
21
+ implementation.onExternalStop = { [weak self] url, error in
22
+ guard let self = self else { return }
23
+ var data: [String: Any] = ["url": url?.absoluteString ?? ""]
24
+ if let error = error {
25
+ data["error"] = error.localizedDescription
26
+ }
27
+ self.notifyListeners("onStopped", data: data)
28
+ }
29
+ }
30
+
20
31
  @objc func start(_ call: CAPPluginCall) {
21
32
  let recordAudio = call.getBool("recordAudio") ?? false
22
33
  let videoFormat = VideoContainerFormat.from(call.getString("format"))
@@ -50,7 +50,7 @@ public enum VideoContainerFormat {
50
50
  }
51
51
  }
52
52
 
53
- public final class ScreenRecorder {
53
+ public final class ScreenRecorder: NSObject, RPScreenRecorderDelegate {
54
54
  private var videoOutputURL: URL?
55
55
  private var didCreateOutputFile = false
56
56
  private var videoWriter: AVAssetWriter?
@@ -62,6 +62,14 @@ public final class ScreenRecorder {
62
62
  private var recordAudio = false
63
63
  private var videoFormat: VideoContainerFormat = .mp4
64
64
  private var isRecording = false
65
+ private var stopRequested = false
66
+ private var startSettled = false
67
+ private var videoHasContent = false
68
+ private var isFinalizingExternally = false
69
+ private var pendingStartHandler: ((Error?) -> Void)?
70
+ private let stateLock = NSLock()
71
+ /// Fired when recording ends without `stoprecording()` being called.
72
+ public var onExternalStop: ((URL?, Error?) -> Void)?
65
73
  let recorder = RPScreenRecorder.shared()
66
74
 
67
75
  public func startRecording(to outputURL: URL? = nil,
@@ -70,10 +78,16 @@ public final class ScreenRecorder {
70
78
  recordAudio: Bool = false,
71
79
  videoFormat: VideoContainerFormat = .mp4,
72
80
  handler: @escaping (Error?) -> Void) {
73
- guard !isRecording else {
81
+ guard !isRecording, !isFinalizingExternally else {
74
82
  return handler(ScreenRecorderError.alreadyRecording)
75
83
  }
76
84
  isRecording = true
85
+ pendingStartHandler = nil
86
+ stopRequested = false
87
+ startSettled = false
88
+ videoHasContent = false
89
+ isFinalizingExternally = false
90
+ recorder.delegate = self
77
91
 
78
92
  self.saveToCameraRoll = saveToCameraRoll
79
93
  self.recordAudio = recordAudio
@@ -180,19 +194,15 @@ public final class ScreenRecorder {
180
194
  self.isRecording = false
181
195
  return handler(ScreenRecorderError.notAvailable)
182
196
  }
197
+ pendingStartHandler = handler
183
198
  var sent = false
184
199
  recorder.startCapture(handler: { (sampleBuffer, sampleType, passedError) in
185
200
  if let passedError = passedError {
186
- self.videoWriterInput?.markAsFinished()
187
- self.audioWriterInput?.markAsFinished()
188
- if self.videoWriter?.status == .writing {
189
- self.videoWriter?.cancelWriting()
190
- }
191
- self.deleteOutputFileIfNeeded(outputURL: outputURL, didCreate: didCreate)
192
201
  if !sent {
193
- self.isRecording = false
194
- handler(passedError)
202
+ self.failPendingStart(passedError, outputURL: outputURL, didCreate: didCreate)
195
203
  sent = true
204
+ } else if self.isRecording && !self.stopRequested && !self.isFinalizingExternally {
205
+ self.handleRecordingEndedExternally(error: passedError)
196
206
  }
197
207
  return
198
208
  }
@@ -220,12 +230,35 @@ public final class ScreenRecorder {
220
230
  break
221
231
  }
222
232
  if !sent {
223
- handler(nil)
233
+ self.completePendingStart(nil)
224
234
  sent = true
225
235
  }
226
236
  })
227
237
  }
228
238
 
239
+ private func completePendingStart(_ error: Error?) {
240
+ stateLock.lock()
241
+ let startHandler = pendingStartHandler
242
+ pendingStartHandler = nil
243
+ startSettled = true
244
+ if error != nil {
245
+ isRecording = false
246
+ }
247
+ stateLock.unlock()
248
+ startHandler?(error)
249
+ }
250
+
251
+ private func failPendingStart(_ error: Error, outputURL: URL?, didCreate: Bool) {
252
+ recorder.delegate = nil
253
+ videoWriterInput?.markAsFinished()
254
+ audioWriterInput?.markAsFinished()
255
+ if videoWriter?.status == .writing {
256
+ videoWriter?.cancelWriting()
257
+ }
258
+ deleteOutputFileIfNeeded(outputURL: outputURL, didCreate: didCreate)
259
+ completePendingStart(error)
260
+ }
261
+
229
262
  private func handleSampleBuffer(sampleBuffer: CMSampleBuffer) {
230
263
  if self.videoWriter?.status == AVAssetWriter.Status.unknown {
231
264
  self.videoWriter?.startWriting()
@@ -233,6 +266,7 @@ public final class ScreenRecorder {
233
266
  } else if self.videoWriter?.status == AVAssetWriter.Status.writing &&
234
267
  self.videoWriterInput?.isReadyForMoreMediaData == true {
235
268
  self.videoWriterInput?.append(sampleBuffer)
269
+ self.videoHasContent = true
236
270
  }
237
271
  }
238
272
 
@@ -245,6 +279,12 @@ public final class ScreenRecorder {
245
279
  }
246
280
 
247
281
  public func stoprecording(handler: @escaping (Error?) -> Void) {
282
+ if isFinalizingExternally || !isRecording {
283
+ handler(nil)
284
+ return
285
+ }
286
+ stopRequested = true
287
+ recorder.delegate = nil
248
288
  recorder.stopCapture(handler: { error in
249
289
  let outputURL = self.videoOutputURL
250
290
  let didCreate = self.didCreateOutputFile
@@ -381,4 +421,112 @@ public final class ScreenRecorder {
381
421
  debugPrint("Failed to delete recording file \(fileURL): \(error)")
382
422
  }
383
423
  }
424
+
425
+ private func handleRecordingEndedExternally(error: Error?) {
426
+ stateLock.lock()
427
+ let shouldHandle = isRecording && !stopRequested && !isFinalizingExternally
428
+ if shouldHandle {
429
+ isFinalizingExternally = true
430
+ }
431
+ stateLock.unlock()
432
+ guard shouldHandle else { return }
433
+ recorder.delegate = nil
434
+
435
+ let outputURL = videoOutputURL
436
+ let didCreate = didCreateOutputFile
437
+ let hadVideo = videoHasContent
438
+
439
+ recorder.stopCapture(handler: { _ in
440
+ self.audioMixerQueue.sync {
441
+ if let mixer = self.audioTrackMixer {
442
+ for sample in mixer.drain() {
443
+ self.add(sample: sample, to: self.audioWriterInput)
444
+ }
445
+ }
446
+ }
447
+
448
+ self.videoWriterInput?.markAsFinished()
449
+ self.audioWriterInput?.markAsFinished()
450
+
451
+ guard let writer = self.videoWriter else {
452
+ self.finishExternalStop(
453
+ outputURL: outputURL,
454
+ didCreate: didCreate,
455
+ hadVideo: hadVideo,
456
+ triggerError: error,
457
+ finishError: nil,
458
+ writerFailed: true,
459
+ )
460
+ return
461
+ }
462
+
463
+ let deliver: (Error?, Bool) -> Void = { finishError, writerFailed in
464
+ self.finishExternalStop(
465
+ outputURL: outputURL,
466
+ didCreate: didCreate,
467
+ hadVideo: hadVideo,
468
+ triggerError: error,
469
+ finishError: finishError,
470
+ writerFailed: writerFailed,
471
+ )
472
+ }
473
+
474
+ if writer.status == .writing {
475
+ writer.finishWriting {
476
+ if let finishError = writer.error {
477
+ self.deleteOutputFileIfNeeded(outputURL: outputURL, didCreate: didCreate)
478
+ deliver(finishError, true)
479
+ return
480
+ }
481
+ deliver(nil, false)
482
+ }
483
+ } else if writer.status == .failed {
484
+ self.deleteOutputFileIfNeeded(outputURL: outputURL, didCreate: didCreate)
485
+ deliver(writer.error, true)
486
+ } else {
487
+ deliver(nil, false)
488
+ }
489
+ })
490
+ }
491
+
492
+ private func finishExternalStop(
493
+ outputURL: URL?,
494
+ didCreate: Bool,
495
+ hadVideo: Bool,
496
+ triggerError: Error?,
497
+ finishError: Error?,
498
+ writerFailed: Bool,
499
+ ) {
500
+ stateLock.lock()
501
+ isFinalizingExternally = false
502
+ isRecording = false
503
+ stateLock.unlock()
504
+
505
+ let combinedError = triggerError ?? finishError
506
+ if !hadVideo {
507
+ deleteOutputFileIfNeeded(outputURL: outputURL, didCreate: didCreate)
508
+ onExternalStop?(nil, combinedError)
509
+ return
510
+ }
511
+ let exists = outputURL.map { FileManager.default.fileExists(atPath: $0.path) } ?? false
512
+ let url: URL? = (!writerFailed && exists) ? outputURL : nil
513
+ onExternalStop?(url, combinedError)
514
+ }
515
+
516
+ public func screenRecorder(
517
+ _ screenRecorder: RPScreenRecorder,
518
+ didStopRecordingWithError error: Error,
519
+ previewViewController: RPPreviewViewController?
520
+ ) {
521
+ if !startSettled {
522
+ failPendingStart(
523
+ error,
524
+ outputURL: videoOutputURL,
525
+ didCreate: didCreateOutputFile,
526
+ )
527
+ return
528
+ }
529
+ guard isRecording, !stopRequested else { return }
530
+ handleRecordingEndedExternally(error: error)
531
+ }
384
532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-screen-recorder",
3
- "version": "8.3.17",
3
+ "version": "8.4.0",
4
4
  "description": "Record device's screen",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",