@capgo/capacitor-screen-recorder 8.2.37 → 8.3.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
@@ -104,6 +104,8 @@ No configuration required for this plugin.
104
104
  * [`start(...)`](#start)
105
105
  * [`stop()`](#stop)
106
106
  * [`getPluginVersion()`](#getpluginversion)
107
+ * [Interfaces](#interfaces)
108
+ * [Type Aliases](#type-aliases)
107
109
 
108
110
  </docgen-index>
109
111
 
@@ -116,7 +118,7 @@ Allows you to capture video recordings of the screen with optional audio.
116
118
  ### start(...)
117
119
 
118
120
  ```typescript
119
- start(options?: { recordAudio?: boolean | undefined; } | undefined) => Promise<void>
121
+ start(options?: StartRecordingOptions | undefined) => Promise<void>
120
122
  ```
121
123
 
122
124
  Start recording the device screen.
@@ -126,9 +128,9 @@ prompted to grant screen recording permissions if not already granted.
126
128
  On iOS, the system recording UI will be displayed. On Android, the recording
127
129
  starts immediately after permission is granted.
128
130
 
129
- | Param | Type | Description |
130
- | ------------- | --------------------------------------- | --------------------------------- |
131
- | **`options`** | <code>{ recordAudio?: boolean; }</code> | - Recording configuration options |
131
+ | Param | Type | Description |
132
+ | ------------- | ----------------------------------------------------------------------- | --------------------------------- |
133
+ | **`options`** | <code><a href="#startrecordingoptions">StartRecordingOptions</a></code> | - Recording configuration options |
132
134
 
133
135
  **Since:** 1.0.0
134
136
 
@@ -166,4 +168,27 @@ Get the native Capacitor plugin version.
166
168
 
167
169
  --------------------
168
170
 
171
+
172
+ ### Interfaces
173
+
174
+
175
+ #### StartRecordingOptions
176
+
177
+ Options for {@link ScreenRecorderPlugin.start}.
178
+
179
+ | Prop | Type | Description | Default | Since |
180
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- |
181
+ | **`recordAudio`** | <code>boolean</code> | Whether to record audio along with the screen video. | <code>false</code> | 1.0.0 |
182
+ | **`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
+
184
+
185
+ ### Type Aliases
186
+
187
+
188
+ #### ScreenRecorderVideoFormat
189
+
190
+ Supported video container formats for screen recordings.
191
+
192
+ <code>'mp4' | 'mov'</code>
193
+
169
194
  </docgen-api>
@@ -68,4 +68,6 @@ dependencies {
68
68
  implementation "androidx.core:core-ktx:$androidxCoreVersion"
69
69
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
70
70
  implementation ("dev.bmcreations:scrcast:0.3.0")
71
+ implementation "com.karumi:dexter:6.2.3"
72
+ implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.1.0"
71
73
  }
@@ -1,2 +1,12 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ xmlns:tools="http://schemas.android.com/tools">
3
+ <application
4
+ android:fullBackupContent="@xml/screen_recorder_backup_rules"
5
+ android:usesCleartextTraffic="false"
6
+ tools:node="merge">
7
+ <service
8
+ android:name="ee.forgr.plugin.screenrecorder.service.CapgoRecorderService"
9
+ android:exported="false"
10
+ android:foregroundServiceType="mediaProjection|microphone" />
11
+ </application>
2
12
  </manifest>
@@ -0,0 +1,19 @@
1
+ package ee.forgr.plugin.screenrecorder
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.media.projection.MediaProjectionManager
7
+ import androidx.activity.result.ActivityResult
8
+ import androidx.activity.result.contract.ActivityResultContract
9
+
10
+ class CapgoRecordScreen : ActivityResultContract<Unit, ActivityResult>() {
11
+ override fun createIntent(context: Context, input: Unit): Intent {
12
+ val pm = context.getSystemService(MediaProjectionManager::class.java)
13
+ return pm.createScreenCaptureIntent()
14
+ }
15
+
16
+ override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult {
17
+ return ActivityResult(resultCode, intent)
18
+ }
19
+ }
@@ -0,0 +1,192 @@
1
+ package ee.forgr.plugin.screenrecorder
2
+
3
+ import android.Manifest
4
+ import android.app.Activity
5
+ import android.content.ComponentName
6
+ import android.content.Context
7
+ import android.content.Intent
8
+ import android.content.IntentFilter
9
+ import android.content.ServiceConnection
10
+ import android.media.MediaScannerConnection
11
+ import android.os.IBinder
12
+ import android.util.DisplayMetrics
13
+ import android.util.Log
14
+ import androidx.activity.ComponentActivity
15
+ import androidx.activity.result.ActivityResult
16
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager
17
+ import com.karumi.dexter.Dexter
18
+ import com.karumi.dexter.MultiplePermissionsReport
19
+ import com.karumi.dexter.PermissionToken
20
+ import com.karumi.dexter.listener.PermissionRequest
21
+ import com.karumi.dexter.listener.multi.MultiplePermissionsListener
22
+ import dev.bmcreations.scrcast.config.Options
23
+ import dev.bmcreations.scrcast.internal.recorder.Action
24
+ import dev.bmcreations.scrcast.internal.recorder.EXTRA_ERROR
25
+ import dev.bmcreations.scrcast.internal.recorder.STATE_IDLE
26
+ import dev.bmcreations.scrcast.internal.recorder.STATE_RECORDING
27
+ import dev.bmcreations.scrcast.internal.recorder.notification.RecorderNotificationProvider
28
+ import dev.bmcreations.scrcast.recorder.RecordingState
29
+ import ee.forgr.plugin.screenrecorder.service.CapgoRecorderService
30
+ import java.io.File
31
+
32
+ class CapgoScrCastWithAudio private constructor(private val activity: ComponentActivity) {
33
+ var options: Options = Options()
34
+ private set
35
+
36
+ private var fileExtension: String = "mp4"
37
+
38
+ private var recordingSession: Intent? = null
39
+ private var serviceBinder: CapgoRecorderService? = null
40
+ private var outputFile: File? = null
41
+
42
+ private val metrics by lazy {
43
+ DisplayMetrics().apply { activity.windowManager.defaultDisplay.getMetrics(this) }
44
+ }
45
+
46
+ private val dpi by lazy { metrics.density }
47
+
48
+ private val notificationProvider by lazy {
49
+ RecorderNotificationProvider(activity, options.notification)
50
+ }
51
+
52
+ private val broadcaster by lazy { LocalBroadcastManager.getInstance(activity) }
53
+
54
+ private val connection = object : ServiceConnection {
55
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
56
+ val binder = service as CapgoRecorderService.LocalBinder
57
+ serviceBinder = binder.service
58
+ serviceBinder?.setNotificationProvider(notificationProvider)
59
+ }
60
+
61
+ override fun onServiceDisconnected(arg0: ComponentName) {
62
+ serviceBinder = null
63
+ }
64
+ }
65
+
66
+ private val recordingStateHandler = object : android.content.BroadcastReceiver() {
67
+ override fun onReceive(context: Context?, intent: Intent?) {
68
+ if (intent?.action == STATE_IDLE) {
69
+ cleanupSession()
70
+ }
71
+ }
72
+ }
73
+
74
+ private val permissionListener = object : MultiplePermissionsListener {
75
+ override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
76
+ startProjection.launch(Unit)
77
+ }
78
+
79
+ override fun onPermissionRationaleShouldBeShown(
80
+ permissions: MutableList<PermissionRequest>?,
81
+ token: PermissionToken?,
82
+ ) {
83
+ token?.continuePermissionRequest()
84
+ }
85
+ }
86
+
87
+ private val startProjection = activity.registerForActivityResult(CapgoRecordScreen()) { result ->
88
+ if (result.resultCode != Activity.RESULT_OK) {
89
+ return@registerForActivityResult
90
+ }
91
+ val file = resolveOutputFile() ?: return@registerForActivityResult
92
+ startService(result, file)
93
+ }
94
+
95
+ fun updateOptions(options: Options) {
96
+ this.options = handleDynamicVideoSize(options)
97
+ }
98
+
99
+ fun updateVideoFormat(format: String?) {
100
+ val resolved = VideoFormatResolver.resolve(format)
101
+ fileExtension = resolved.extension
102
+ options = handleDynamicVideoSize(
103
+ options.copy(storage = options.storage.copy(outputFormat = resolved.outputFormat)),
104
+ )
105
+ }
106
+
107
+ fun record() {
108
+ Dexter.withContext(activity)
109
+ .withPermissions(
110
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
111
+ Manifest.permission.READ_EXTERNAL_STORAGE,
112
+ Manifest.permission.RECORD_AUDIO,
113
+ )
114
+ .withListener(permissionListener)
115
+ .check()
116
+ }
117
+
118
+ fun stopRecording() {
119
+ broadcaster.sendBroadcast(Intent(Action.Stop.name))
120
+ }
121
+
122
+ private fun resolveOutputFile(): File? {
123
+ val dir = options.storage.mediaStorageLocation ?: return null
124
+ return File("${dir.path}${File.separator}${options.storage.fileNameFormatter()}.$fileExtension")
125
+ }
126
+
127
+ private fun startService(result: ActivityResult, file: File) {
128
+ outputFile = file
129
+ val session = Intent(activity, CapgoRecorderService::class.java).apply {
130
+ putExtra("code", result.resultCode)
131
+ putExtra("data", result.data)
132
+ putExtra("options", options)
133
+ putExtra("outputFile", file.absolutePath)
134
+ putExtra("dpi", dpi)
135
+ putExtra("rotation", activity.windowManager.defaultDisplay.rotation)
136
+ }
137
+ recordingSession = session
138
+
139
+ broadcaster.registerReceiver(
140
+ recordingStateHandler,
141
+ IntentFilter().apply {
142
+ addAction(STATE_IDLE)
143
+ addAction(STATE_RECORDING)
144
+ },
145
+ )
146
+
147
+ activity.bindService(session, connection, Context.BIND_AUTO_CREATE)
148
+ activity.startService(session)
149
+ }
150
+
151
+ private fun cleanupSession() {
152
+ try {
153
+ broadcaster.unregisterReceiver(recordingStateHandler)
154
+ } catch (ignored: Exception) {
155
+ Log.d("CapgoScreenRecorder", "Receiver already unregistered", ignored)
156
+ }
157
+
158
+ try {
159
+ activity.unbindService(connection)
160
+ } catch (ignored: Exception) {
161
+ Log.d("CapgoScreenRecorder", "Service already unbound", ignored)
162
+ }
163
+
164
+ recordingSession?.let { activity.stopService(it) }
165
+ recordingSession = null
166
+
167
+ outputFile?.let { file ->
168
+ MediaScannerConnection.scanFile(activity, arrayOf(file.absolutePath), null) { path, uri ->
169
+ Log.i("CapgoScreenRecorder", "Saved recording: $path uri=$uri")
170
+ }
171
+ }
172
+ outputFile = null
173
+ }
174
+
175
+ private fun handleDynamicVideoSize(options: Options): Options {
176
+ var reconfig = options
177
+ if (options.video.width == -1) {
178
+ reconfig = reconfig.copy(video = reconfig.video.copy(width = metrics.widthPixels))
179
+ }
180
+ if (options.video.height == -1) {
181
+ reconfig = reconfig.copy(video = reconfig.video.copy(height = metrics.heightPixels))
182
+ }
183
+ return reconfig
184
+ }
185
+
186
+ companion object {
187
+ @JvmStatic
188
+ fun use(activity: ComponentActivity): CapgoScrCastWithAudio {
189
+ return CapgoScrCastWithAudio(activity)
190
+ }
191
+ }
192
+ }
@@ -11,27 +11,56 @@ import dev.bmcreations.scrcast.config.Options;
11
11
  @CapacitorPlugin(name = "ScreenRecorder")
12
12
  public class ScreenRecorderPlugin extends Plugin {
13
13
 
14
- private final String pluginVersion = "8.2.37";
14
+ private final String pluginVersion = "8.3.0";
15
15
 
16
16
  private ScrCast recorder;
17
+ private CapgoScrCastWithAudio audioRecorder;
18
+ private boolean recordingWithAudio = false;
17
19
 
18
20
  @Override
19
21
  public void load() {
20
22
  recorder = ScrCast.use(this.bridge.getActivity());
23
+ audioRecorder = CapgoScrCastWithAudio.use(this.bridge.getActivity());
21
24
  Options options = new Options();
22
25
  recorder.updateOptions(options);
26
+ audioRecorder.updateOptions(options);
23
27
  }
24
28
 
25
29
  @PluginMethod
26
30
  public void start(PluginCall call) {
27
- recorder.record();
28
- call.resolve();
31
+ try {
32
+ final boolean recordAudio = call.getBoolean("recordAudio", false);
33
+ final String format = call.getString("format");
34
+ recordingWithAudio = recordAudio;
35
+
36
+ final Options configuredOptions = VideoFormatResolver.INSTANCE.applyTo(recorder.getOptions(), format);
37
+ recorder.updateOptions(configuredOptions);
38
+ audioRecorder.updateVideoFormat(format);
39
+
40
+ if (recordAudio) {
41
+ audioRecorder.record();
42
+ } else {
43
+ recorder.record();
44
+ }
45
+ call.resolve();
46
+ } catch (final Exception e) {
47
+ call.reject("Could not start screen recording", e);
48
+ }
29
49
  }
30
50
 
31
51
  @PluginMethod
32
52
  public void stop(PluginCall call) {
33
- recorder.stopRecording();
34
- call.resolve();
53
+ try {
54
+ if (recordingWithAudio) {
55
+ audioRecorder.stopRecording();
56
+ } else {
57
+ recorder.stopRecording();
58
+ }
59
+ recordingWithAudio = false;
60
+ call.resolve();
61
+ } catch (final Exception e) {
62
+ call.reject("Could not stop screen recording", e);
63
+ }
35
64
  }
36
65
 
37
66
  @PluginMethod
@@ -0,0 +1,21 @@
1
+ package ee.forgr.plugin.screenrecorder
2
+
3
+ import android.media.MediaRecorder
4
+ import dev.bmcreations.scrcast.config.Options
5
+
6
+ object VideoFormatResolver {
7
+ data class Resolved(
8
+ val extension: String = "mp4",
9
+ val outputFormat: Int = MediaRecorder.OutputFormat.MPEG_4,
10
+ )
11
+
12
+ fun resolve(@Suppress("UNUSED_PARAMETER") format: String?): Resolved {
13
+ // Android MediaRecorder only supports MPEG-4 output.
14
+ return Resolved()
15
+ }
16
+
17
+ fun applyTo(options: Options, format: String?): Options {
18
+ val resolved = resolve(format)
19
+ return options.copy(storage = options.storage.copy(outputFormat = resolved.outputFormat))
20
+ }
21
+ }
@@ -0,0 +1,317 @@
1
+ package ee.forgr.plugin.screenrecorder.service
2
+
3
+ import android.app.Service
4
+ import android.content.BroadcastReceiver
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.content.IntentFilter
8
+ import android.hardware.display.DisplayManager
9
+ import android.hardware.display.VirtualDisplay
10
+ import android.media.MediaRecorder
11
+ import android.media.MediaRecorder.*
12
+ import android.media.projection.MediaProjection
13
+ import android.media.projection.MediaProjectionManager
14
+ import android.os.Binder
15
+ import android.os.Build
16
+ import android.os.Handler
17
+ import android.os.IBinder
18
+ import android.util.Log
19
+ import androidx.localbroadcastmanager.content.LocalBroadcastManager
20
+ import dev.bmcreations.scrcast.config.Options
21
+ import dev.bmcreations.scrcast.internal.extensions.countdown
22
+ import dev.bmcreations.scrcast.internal.recorder.*
23
+ import dev.bmcreations.scrcast.internal.recorder.service.orientations
24
+ import dev.bmcreations.scrcast.recorder.*
25
+ import dev.bmcreations.scrcast.recorder.notification.NotificationProvider
26
+ import kotlinx.coroutines.Dispatchers
27
+ import kotlinx.coroutines.GlobalScope
28
+ import kotlinx.coroutines.launch
29
+ import java.io.File
30
+
31
+ class CapgoRecorderService : Service() {
32
+
33
+ private val projectionManager: MediaProjectionManager by lazy {
34
+ getSystemService(MediaProjectionManager::class.java)
35
+ }
36
+
37
+ private val broadcaster by lazy {
38
+ LocalBroadcastManager.getInstance(this)
39
+ }
40
+
41
+ private val binder = LocalBinder()
42
+
43
+ private lateinit var notificationProvider: NotificationProvider
44
+
45
+ private val pauseResumeHandler = object : BroadcastReceiver() {
46
+ override fun onReceive(context: Context?, intent: Intent?) {
47
+ when (intent?.action) {
48
+ ACTION_PAUSE -> pause()
49
+ ACTION_RESUME -> resume()
50
+ ACTION_STOP -> stopRecording()
51
+ }
52
+ }
53
+ }
54
+ private val screenHandler = object : BroadcastReceiver() {
55
+ override fun onReceive(context: Context?, intent: Intent?) {
56
+ when (intent?.action) {
57
+ Intent.ACTION_SCREEN_OFF -> {
58
+ Log.d("scrcast", "stopping recording with screen off per request")
59
+ if (state == RecordingState.Recording) {
60
+ stopRecording()
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ private var state: RecordingState = RecordingState.Idle()
68
+ set(value) {
69
+ field = value
70
+ broadcaster.sendBroadcast(Intent(value.stateString()).apply {
71
+ if (value is RecordingState.Delay) {
72
+ putExtra(EXTRA_DELAY_REMAINING, value.remainingSeconds)
73
+ } else if (value is RecordingState.Idle) {
74
+ putExtra(EXTRA_ERROR, value.error)
75
+ }
76
+ })
77
+ }
78
+
79
+ private var options: Options = Options()
80
+ private lateinit var outputFile: String
81
+ private var rotation = 0
82
+ private val orientation by lazy {
83
+ orientations.get(rotation + 90)
84
+ }
85
+ private var dpi: Float = 0f
86
+
87
+ private var requestCode: Int = -1
88
+ private var requestData: Intent = Intent()
89
+
90
+ private var mediaProjection: MediaProjection? = null
91
+ private var mediaProjectionCallback = MediaProjectionCallback()
92
+
93
+ private var _virtualDisplay: VirtualDisplay? = null
94
+ private val virtualDisplay: VirtualDisplay?
95
+ get() {
96
+ if (_virtualDisplay == null) {
97
+ _virtualDisplay = mediaProjection?.createVirtualDisplay(
98
+ "SrcCast",
99
+ options.video.width,
100
+ options.video.height,
101
+ dpi.toInt(),
102
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
103
+ mediaRecorder?.surface,
104
+ null,
105
+ null
106
+ )
107
+ }
108
+ return _virtualDisplay
109
+ }
110
+
111
+ private var mediaRecorder: MediaRecorder? = null
112
+
113
+
114
+ private fun createMediaRecorder(): MediaRecorder {
115
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
116
+ MediaRecorder(this)
117
+ } else {
118
+ @Suppress("DEPRECATION")
119
+ MediaRecorder()
120
+ }
121
+ }
122
+
123
+ private fun createRecorder() {
124
+ Log.d("scrcast", "createRecorder()")
125
+ mediaRecorder = createMediaRecorder().apply {
126
+ setAudioSource(AudioSource.MIC)
127
+ setVideoSource(VideoSource.SURFACE)
128
+ setOutputFormat(options.storage.outputFormat)
129
+ setAudioEncoder(AudioEncoder.AAC)
130
+ setOutputFile(outputFile)
131
+ with(options.video) {
132
+ setVideoSize(width, height)
133
+ setVideoEncoder(videoEncoder)
134
+ setVideoEncodingBitRate(bitrate)
135
+ setVideoFrameRate(frameRate)
136
+ if (maxLengthSecs > 0) {
137
+ setMaxDuration(maxLengthSecs * 1000)
138
+ }
139
+ }
140
+ with(options.storage) {
141
+ if (maxSizeMB > 0) {
142
+ setMaxFileSize((maxSizeMB * (1024 * 1024)).toLong())
143
+ }
144
+ }
145
+ setOnInfoListener { _, what, _ ->
146
+ when (what) {
147
+ MEDIA_RECORDER_INFO_MAX_DURATION_REACHED -> {
148
+ Log.d(
149
+ "scrcast",
150
+ "max duration of ${options.video.maxLengthSecs} seconds reached. Stopping reconrding..."
151
+ )
152
+ stopRecording()
153
+ }
154
+ MEDIA_RECORDER_INFO_MAX_FILESIZE_APPROACHING -> Log.d(
155
+ "scrcast",
156
+ "Approaching max file size of ${options.storage.maxSizeMB}MB"
157
+ )
158
+ MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED -> {
159
+ Log.d(
160
+ "scrcast",
161
+ "max file size of ${options.storage.maxSizeMB}MB reached. Stopping reconrding..."
162
+ )
163
+ stopRecording()
164
+ }
165
+
166
+ }
167
+ }
168
+ setOrientationHint(orientation)
169
+ }
170
+ mediaRecorder?.prepare()
171
+ }
172
+
173
+ fun setNotificationProvider(provider: NotificationProvider) {
174
+ notificationProvider = provider
175
+ }
176
+
177
+ private fun pause() {
178
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
179
+ if (state.isRecording) {
180
+ mediaRecorder?.pause()
181
+ }
182
+ state = RecordingState.Paused
183
+
184
+ notificationProvider.update(state)
185
+ }
186
+ }
187
+
188
+ private fun resume() {
189
+ when (state) {
190
+ is RecordingState.Idle -> startRecording()
191
+ RecordingState.Paused -> {
192
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
193
+ mediaRecorder?.resume()
194
+ state = RecordingState.Recording
195
+ notificationProvider.update(state)
196
+ }
197
+ }
198
+ else -> Unit
199
+ }
200
+ }
201
+
202
+ private fun startRecording(code: Int = requestCode, data: Intent = requestData) {
203
+ requestCode = code
204
+ requestData = data
205
+
206
+ if (options.startDelayMs > 0) {
207
+ options.startDelayMs.countdown(
208
+ repeatMillis = 1_000,
209
+ onTick = { state = RecordingState.Delay((it / 1000).toInt() + 1) },
210
+ after = { recordInternal(code, data) }
211
+ )
212
+ } else {
213
+ recordInternal(code, data)
214
+ }
215
+ }
216
+
217
+ private fun recordInternal(code: Int, data: Intent) {
218
+ GlobalScope.launch(Dispatchers.Main) {
219
+ startForeground(
220
+ notificationProvider.getNotificationId(),
221
+ notificationProvider.get(state)
222
+ )
223
+ mediaProjection = projectionManager.getMediaProjection(code, data)
224
+
225
+ if (options.stopOnScreenOff) {
226
+ with(IntentFilter(Intent.ACTION_SCREEN_OFF)) {
227
+ registerReceiver(screenHandler, this)
228
+ }
229
+ }
230
+
231
+ with(IntentFilter(ACTION_PAUSE).apply {
232
+ addAction(ACTION_RESUME)
233
+ addAction(ACTION_STOP)
234
+ }) {
235
+ broadcaster.registerReceiver(pauseResumeHandler, this)
236
+ }
237
+
238
+ mediaProjection?.registerCallback(mediaProjectionCallback, Handler())
239
+ createRecorder()
240
+ virtualDisplay // touch
241
+ try {
242
+ mediaRecorder?.start()
243
+ state = RecordingState.Recording
244
+ notificationProvider.update(state)
245
+ } catch (e: Exception) {
246
+ stopRecording(e)
247
+ }
248
+ }
249
+ }
250
+
251
+ private fun stopRecording(error: Throwable? = null) {
252
+ mediaProjection?.stop()
253
+
254
+ state = RecordingState.Idle(error)
255
+ stopForeground(true)
256
+ }
257
+
258
+ private fun cleanupProjection() {
259
+ mediaProjection?.unregisterCallback(mediaProjectionCallback)
260
+ mediaProjection = null
261
+
262
+ _virtualDisplay?.release()
263
+
264
+ runCatching {
265
+ mediaRecorder?.stop()
266
+ mediaRecorder?.reset()
267
+ mediaRecorder?.release()
268
+ }
269
+ }
270
+
271
+ private inner class MediaProjectionCallback : MediaProjection.Callback() {
272
+ override fun onStop() {
273
+ Log.d("scrcast", "projection on stop")
274
+ cleanupProjection()
275
+ }
276
+ }
277
+
278
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
279
+ intent?.let {
280
+ options = it.getParcelableExtra("options") ?: Options()
281
+ rotation = it.getIntExtra("rotation", 0)
282
+ dpi = it.getFloatExtra("dpi", 0f)
283
+ outputFile = it.getStringExtra("outputFile") ?: ""
284
+
285
+ startRecording(
286
+ code = it.getIntExtra("code", -1),
287
+ data = it.getParcelableExtra("data") ?: Intent()
288
+ )
289
+ }
290
+
291
+ return START_STICKY
292
+ }
293
+
294
+ override fun onDestroy() {
295
+ Log.d("scrcast", "onDestroy: service")
296
+ stopRecording()
297
+ if (options.stopOnScreenOff) {
298
+ unregisterReceiver(screenHandler)
299
+ }
300
+ try {
301
+ broadcaster.unregisterReceiver(pauseResumeHandler)
302
+ } catch (swallow: Exception) {
303
+ Log.d("CapgoScreenRecorder", "Receiver already unregistered", swallow)
304
+ }
305
+
306
+ super.onDestroy()
307
+ }
308
+
309
+ override fun onBind(p0: Intent?): IBinder? = binder
310
+
311
+ // Class used for the client Binder.
312
+ inner class LocalBinder : Binder() {
313
+ // Return this instance of MyService so clients can call public methods
314
+ val service: CapgoRecorderService
315
+ get() = this@CapgoRecorderService
316
+ }
317
+ }
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <full-backup-content />
package/dist/docs.json CHANGED
@@ -12,12 +12,12 @@
12
12
  "methods": [
13
13
  {
14
14
  "name": "start",
15
- "signature": "(options?: { recordAudio?: boolean | undefined; } | undefined) => Promise<void>",
15
+ "signature": "(options?: StartRecordingOptions | undefined) => Promise<void>",
16
16
  "parameters": [
17
17
  {
18
18
  "name": "options",
19
19
  "docs": "- Recording configuration options",
20
- "type": "{ recordAudio?: boolean | undefined; } | undefined"
20
+ "type": "StartRecordingOptions | undefined"
21
21
  }
22
22
  ],
23
23
  "returns": "Promise<void>",
@@ -30,6 +30,10 @@
30
30
  "name": "param",
31
31
  "text": "options.recordAudio - Whether to record audio along with the screen video. Defaults to false."
32
32
  },
33
+ {
34
+ "name": "param",
35
+ "text": "options.format - Video container format for the saved recording. Defaults to `mp4`."
36
+ },
33
37
  {
34
38
  "name": "returns",
35
39
  "text": "Promise that resolves when recording starts"
@@ -44,11 +48,13 @@
44
48
  },
45
49
  {
46
50
  "name": "example",
47
- "text": "```typescript\n// Start recording without audio\nawait ScreenRecorder.start();\n\n// Start recording with audio\nawait ScreenRecorder.start({ recordAudio: true });\n```"
51
+ "text": "```typescript\n// Start recording without audio\nawait ScreenRecorder.start();\n\n// Start recording with audio\nawait ScreenRecorder.start({ recordAudio: true });\n\n// Start recording as MOV on iOS\nawait ScreenRecorder.start({ format: 'mov' });\n```"
48
52
  }
49
53
  ],
50
54
  "docs": "Start recording the device screen.\n\nInitiates screen recording with optional audio capture. The user will be\nprompted to grant screen recording permissions if not already granted.\nOn iOS, the system recording UI will be displayed. On Android, the recording\nstarts immediately after permission is granted.",
51
- "complexTypes": [],
55
+ "complexTypes": [
56
+ "StartRecordingOptions"
57
+ ],
52
58
  "slug": "start"
53
59
  },
54
60
  {
@@ -108,8 +114,77 @@
108
114
  ],
109
115
  "properties": []
110
116
  },
111
- "interfaces": [],
117
+ "interfaces": [
118
+ {
119
+ "name": "StartRecordingOptions",
120
+ "slug": "startrecordingoptions",
121
+ "docs": "Options for {@link ScreenRecorderPlugin.start}.",
122
+ "tags": [
123
+ {
124
+ "text": "8.3.0",
125
+ "name": "since"
126
+ }
127
+ ],
128
+ "methods": [],
129
+ "properties": [
130
+ {
131
+ "name": "recordAudio",
132
+ "tags": [
133
+ {
134
+ "text": "false",
135
+ "name": "default"
136
+ },
137
+ {
138
+ "text": "1.0.0",
139
+ "name": "since"
140
+ }
141
+ ],
142
+ "docs": "Whether to record audio along with the screen video.",
143
+ "complexTypes": [],
144
+ "type": "boolean | undefined"
145
+ },
146
+ {
147
+ "name": "format",
148
+ "tags": [
149
+ {
150
+ "text": "'mp4'",
151
+ "name": "default"
152
+ },
153
+ {
154
+ "text": "8.3.0",
155
+ "name": "since"
156
+ },
157
+ {
158
+ "text": "'mov'",
159
+ "name": "example"
160
+ }
161
+ ],
162
+ "docs": "Video container format for the saved recording.\n\nAccepts `mp4`, `mov`, or MIME types `video/mp4` and `video/quicktime`.\niOS supports both `mp4` and `mov`. Android records MPEG-4 (`.mp4`) regardless of this value.",
163
+ "complexTypes": [
164
+ "ScreenRecorderVideoFormat"
165
+ ],
166
+ "type": "ScreenRecorderVideoFormat | 'video/mp4' | 'video/quicktime' | undefined"
167
+ }
168
+ ]
169
+ }
170
+ ],
112
171
  "enums": [],
113
- "typeAliases": [],
172
+ "typeAliases": [
173
+ {
174
+ "name": "ScreenRecorderVideoFormat",
175
+ "slug": "screenrecordervideoformat",
176
+ "docs": "Supported video container formats for screen recordings.",
177
+ "types": [
178
+ {
179
+ "text": "'mp4'",
180
+ "complexTypes": []
181
+ },
182
+ {
183
+ "text": "'mov'",
184
+ "complexTypes": []
185
+ }
186
+ ]
187
+ }
188
+ ],
114
189
  "pluginConfigs": []
115
190
  }
@@ -1,3 +1,34 @@
1
+ /**
2
+ * Supported video container formats for screen recordings.
3
+ *
4
+ * @since 8.3.0
5
+ */
6
+ export type ScreenRecorderVideoFormat = 'mp4' | 'mov';
7
+ /**
8
+ * Options for {@link ScreenRecorderPlugin.start}.
9
+ *
10
+ * @since 8.3.0
11
+ */
12
+ export interface StartRecordingOptions {
13
+ /**
14
+ * Whether to record audio along with the screen video.
15
+ *
16
+ * @default false
17
+ * @since 1.0.0
18
+ */
19
+ recordAudio?: boolean;
20
+ /**
21
+ * Video container format for the saved recording.
22
+ *
23
+ * Accepts `mp4`, `mov`, or MIME types `video/mp4` and `video/quicktime`.
24
+ * iOS supports both `mp4` and `mov`. Android records MPEG-4 (`.mp4`) regardless of this value.
25
+ *
26
+ * @default 'mp4'
27
+ * @since 8.3.0
28
+ * @example 'mov'
29
+ */
30
+ format?: ScreenRecorderVideoFormat | 'video/mp4' | 'video/quicktime';
31
+ }
1
32
  /**
2
33
  * Capacitor Screen Recorder Plugin for recording the device screen.
3
34
  * Allows you to capture video recordings of the screen with optional audio.
@@ -15,6 +46,7 @@ export interface ScreenRecorderPlugin {
15
46
  *
16
47
  * @param options - Recording configuration options
17
48
  * @param options.recordAudio - Whether to record audio along with the screen video. Defaults to false.
49
+ * @param options.format - Video container format for the saved recording. Defaults to `mp4`.
18
50
  * @returns Promise that resolves when recording starts
19
51
  * @throws Error if recording fails to start or permissions are denied
20
52
  * @since 1.0.0
@@ -25,11 +57,12 @@ export interface ScreenRecorderPlugin {
25
57
  *
26
58
  * // Start recording with audio
27
59
  * await ScreenRecorder.start({ recordAudio: true });
60
+ *
61
+ * // Start recording as MOV on iOS
62
+ * await ScreenRecorder.start({ format: 'mov' });
28
63
  * ```
29
64
  */
30
- start(options?: {
31
- recordAudio?: boolean;
32
- }): Promise<void>;
65
+ start(options?: StartRecordingOptions): Promise<void>;
33
66
  /**
34
67
  * Stop the current screen recording.
35
68
  *
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\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 * @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 */\n start(options?: { recordAudio?: boolean }): 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":["/**\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"]}
package/dist/esm/web.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
- import type { ScreenRecorderPlugin } from './definitions';
2
+ import type { ScreenRecorderPlugin, StartRecordingOptions } from './definitions';
3
3
  export declare class ScreenRecorderWeb extends WebPlugin implements ScreenRecorderPlugin {
4
- start(): Promise<void>;
4
+ start(_options?: StartRecordingOptions): Promise<void>;
5
5
  stop(): Promise<void>;
6
6
  getPluginVersion(): Promise<{
7
7
  version: string;
package/dist/esm/web.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
2
  export class ScreenRecorderWeb extends WebPlugin {
3
- async start() {
3
+ async start(_options) {
4
4
  throw new Error('Method not implemented.');
5
5
  }
6
6
  async stop() {
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,iBAAkB,SAAQ,SAAS;IAC9C,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IACD,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { ScreenRecorderPlugin } from './definitions';\n\nexport class ScreenRecorderWeb extends WebPlugin implements ScreenRecorderPlugin {\n async start(): Promise<void> {\n throw new Error('Method not implemented.');\n }\n async stop(): Promise<void> {\n throw new Error('Method not implemented.');\n }\n\n async getPluginVersion(): Promise<{ version: string }> {\n return { version: 'web' };\n }\n}\n"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,iBAAkB,SAAQ,SAAS;IAC9C,KAAK,CAAC,KAAK,CAAC,QAAgC;QAC1C,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IACD,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { ScreenRecorderPlugin, StartRecordingOptions } from './definitions';\n\nexport class ScreenRecorderWeb extends WebPlugin implements ScreenRecorderPlugin {\n async start(_options?: StartRecordingOptions): Promise<void> {\n throw new Error('Method not implemented.');\n }\n async stop(): Promise<void> {\n throw new Error('Method not implemented.');\n }\n\n async getPluginVersion(): Promise<{ version: string }> {\n return { version: 'web' };\n }\n}\n"]}
@@ -7,7 +7,7 @@ const ScreenRecorder = core.registerPlugin('ScreenRecorder', {
7
7
  });
8
8
 
9
9
  class ScreenRecorderWeb extends core.WebPlugin {
10
- async start() {
10
+ async start(_options) {
11
11
  throw new Error('Method not implemented.');
12
12
  }
13
13
  async stop() {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst ScreenRecorder = registerPlugin('ScreenRecorder', {\n web: () => import('./web').then((m) => new m.ScreenRecorderWeb()),\n});\nexport * from './definitions';\nexport { ScreenRecorder };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class ScreenRecorderWeb extends WebPlugin {\n async start() {\n throw new Error('Method not implemented.');\n }\n async stop() {\n throw new Error('Method not implemented.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,cAAc,GAAGA,mBAAc,CAAC,gBAAgB,EAAE;AACxD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,iBAAiB,EAAE,CAAC;AACrE,CAAC;;ACFM,MAAM,iBAAiB,SAASC,cAAS,CAAC;AACjD,IAAI,MAAM,KAAK,GAAG;AAClB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;AAClD,IAAI;AACJ,IAAI,MAAM,IAAI,GAAG;AACjB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;AAClD,IAAI;AACJ,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC,IAAI;AACJ;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst ScreenRecorder = registerPlugin('ScreenRecorder', {\n web: () => import('./web').then((m) => new m.ScreenRecorderWeb()),\n});\nexport * from './definitions';\nexport { ScreenRecorder };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class ScreenRecorderWeb extends WebPlugin {\n async start(_options) {\n throw new Error('Method not implemented.');\n }\n async stop() {\n throw new Error('Method not implemented.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,cAAc,GAAGA,mBAAc,CAAC,gBAAgB,EAAE;AACxD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,iBAAiB,EAAE,CAAC;AACrE,CAAC;;ACFM,MAAM,iBAAiB,SAASC,cAAS,CAAC;AACjD,IAAI,MAAM,KAAK,CAAC,QAAQ,EAAE;AAC1B,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;AAClD,IAAI;AACJ,IAAI,MAAM,IAAI,GAAG;AACjB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;AAClD,IAAI;AACJ,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -6,7 +6,7 @@ var capacitorScreenRecorder = (function (exports, core) {
6
6
  });
7
7
 
8
8
  class ScreenRecorderWeb extends core.WebPlugin {
9
- async start() {
9
+ async start(_options) {
10
10
  throw new Error('Method not implemented.');
11
11
  }
12
12
  async stop() {
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst ScreenRecorder = registerPlugin('ScreenRecorder', {\n web: () => import('./web').then((m) => new m.ScreenRecorderWeb()),\n});\nexport * from './definitions';\nexport { ScreenRecorder };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class ScreenRecorderWeb extends WebPlugin {\n async start() {\n throw new Error('Method not implemented.');\n }\n async stop() {\n throw new Error('Method not implemented.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,cAAc,GAAGA,mBAAc,CAAC,gBAAgB,EAAE;IACxD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,iBAAiB,EAAE,CAAC;IACrE,CAAC;;ICFM,MAAM,iBAAiB,SAASC,cAAS,CAAC;IACjD,IAAI,MAAM,KAAK,GAAG;IAClB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;IAClD,IAAI;IACJ,IAAI,MAAM,IAAI,GAAG;IACjB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;IAClD,IAAI;IACJ,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst ScreenRecorder = registerPlugin('ScreenRecorder', {\n web: () => import('./web').then((m) => new m.ScreenRecorderWeb()),\n});\nexport * from './definitions';\nexport { ScreenRecorder };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class ScreenRecorderWeb extends WebPlugin {\n async start(_options) {\n throw new Error('Method not implemented.');\n }\n async stop() {\n throw new Error('Method not implemented.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,cAAc,GAAGA,mBAAc,CAAC,gBAAgB,EAAE;IACxD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,iBAAiB,EAAE,CAAC;IACrE,CAAC;;ICFM,MAAM,iBAAiB,SAASC,cAAS,CAAC;IACjD,IAAI,MAAM,KAAK,CAAC,QAAQ,EAAE;IAC1B,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;IAClD,IAAI;IACJ,IAAI,MAAM,IAAI,GAAG;IACjB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;IAClD,IAAI;IACJ,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -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.2.37"
10
+ private let pluginVersion: String = "8.3.0"
11
11
  public let identifier = "ScreenRecorderPlugin"
12
12
  public let jsName = "ScreenRecorder"
13
13
  public let pluginMethods: [CAPPluginMethod] = [
@@ -18,7 +18,9 @@ public class ScreenRecorderPlugin: CAPPlugin, CAPBridgedPlugin {
18
18
  private let implementation = ScreenRecorder()
19
19
 
20
20
  @objc func start(_ call: CAPPluginCall) {
21
- implementation.startRecording(saveToCameraRoll: true, handler: { error in
21
+ let recordAudio = call.getBool("recordAudio") ?? false
22
+ let videoFormat = VideoContainerFormat.from(call.getString("format"))
23
+ implementation.startRecording(saveToCameraRoll: true, recordAudio: recordAudio, videoFormat: videoFormat, handler: { error in
22
24
  if let error = error {
23
25
  debugPrint("Error when start recording \(error)")
24
26
  call.reject("Cannot start recording")
@@ -27,6 +29,7 @@ public class ScreenRecorderPlugin: CAPPlugin, CAPBridgedPlugin {
27
29
  }
28
30
  })
29
31
  }
32
+
30
33
  @objc func stop(_ call: CAPPluginCall) {
31
34
  implementation.stoprecording(handler: { error in
32
35
  if let error = error {
@@ -41,5 +44,4 @@ public class ScreenRecorderPlugin: CAPPlugin, CAPBridgedPlugin {
41
44
  @objc func getPluginVersion(_ call: CAPPluginCall) {
42
45
  call.resolve(["version": self.pluginVersion])
43
46
  }
44
-
45
47
  }
@@ -6,15 +6,49 @@
6
6
  // Copyright © 2020 Cesar Vargas. All rights reserved.
7
7
  //
8
8
 
9
+ import AVFoundation
9
10
  import Foundation
10
- import ReplayKit
11
11
  import Photos
12
+ import ReplayKit
13
+ import UIKit
12
14
 
13
15
  public enum ScreenRecorderError: Error {
14
16
  case notAvailable
15
17
  case photoLibraryAccessNotGranted
16
18
  }
17
19
 
20
+ public enum VideoContainerFormat {
21
+ case mp4
22
+ case mov
23
+
24
+ var fileType: AVFileType {
25
+ switch self {
26
+ case .mp4:
27
+ return .mp4
28
+ case .mov:
29
+ return .mov
30
+ }
31
+ }
32
+
33
+ var fileExtension: String {
34
+ switch self {
35
+ case .mp4:
36
+ return "mp4"
37
+ case .mov:
38
+ return "mov"
39
+ }
40
+ }
41
+
42
+ static func from(_ value: String?) -> VideoContainerFormat {
43
+ switch value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
44
+ case "mov", "video/quicktime", "quicktime":
45
+ return .mov
46
+ default:
47
+ return .mp4
48
+ }
49
+ }
50
+ }
51
+
18
52
  public final class ScreenRecorder {
19
53
  private var videoOutputURL: URL?
20
54
  private var videoWriter: AVAssetWriter?
@@ -22,37 +56,50 @@ public final class ScreenRecorder {
22
56
  private var micAudioWriterInput: AVAssetWriterInput?
23
57
  private var appAudioWriterInput: AVAssetWriterInput?
24
58
  private var saveToCameraRoll = false
59
+ private var recordAudio = false
60
+ private var videoFormat: VideoContainerFormat = .mp4
25
61
  let recorder = RPScreenRecorder.shared()
26
62
 
27
- /**
28
- Starts recording the content of the application screen. It works together with stopRecording
29
-
30
- - Parameter outputURL: The output where the video will be saved. If nil, it saves it in the documents directory.
31
- - Parameter size: The size of the video. If nil, it will use the app screen size.
32
- - Parameter saveToCameraRoll: Whether to save it to camera roll. False by default.
33
- - Parameter errorHandler: Called when an error is found
34
- */
35
63
  public func startRecording(to outputURL: URL? = nil,
36
64
  size: CGSize? = nil,
37
65
  saveToCameraRoll: Bool = false,
66
+ recordAudio: Bool = false,
67
+ videoFormat: VideoContainerFormat = .mp4,
38
68
  handler: @escaping (Error?) -> Void) {
39
- recorder.isMicrophoneEnabled = true
69
+ self.saveToCameraRoll = saveToCameraRoll
70
+ self.recordAudio = recordAudio
71
+ self.videoFormat = videoFormat
72
+ resetWriterState()
73
+
74
+ recorder.isMicrophoneEnabled = recordAudio
75
+
40
76
  do {
77
+ if recordAudio {
78
+ try configureAudioSession()
79
+ }
41
80
  try createVideoWriter(in: outputURL)
42
81
  addVideoWriterInput(size: size)
43
- self.micAudioWriterInput = createAndAddAudioInput()
44
- self.appAudioWriterInput = createAndAddAudioInput()
82
+ if recordAudio {
83
+ self.micAudioWriterInput = createAndAddAudioInput()
84
+ self.appAudioWriterInput = createAndAddAudioInput()
85
+ }
45
86
  startCapture(handler: handler)
46
87
  } catch let err {
47
88
  handler(err)
48
89
  }
49
90
  }
50
91
 
51
- private func checkPhotoLibraryAuthorizationStatus() {
52
- let status = PHPhotoLibrary.authorizationStatus()
53
- if status == .notDetermined {
54
- PHPhotoLibrary.requestAuthorization({ _ in })
55
- }
92
+ private func resetWriterState() {
93
+ videoWriter = nil
94
+ videoWriterInput = nil
95
+ micAudioWriterInput = nil
96
+ appAudioWriterInput = nil
97
+ }
98
+
99
+ private func configureAudioSession() throws {
100
+ let session = AVAudioSession.sharedInstance()
101
+ try session.setCategory(.playAndRecord, mode: .videoRecording, options: [.defaultToSpeaker, .mixWithOthers])
102
+ try session.setActive(true)
56
103
  }
57
104
 
58
105
  private func createVideoWriter(in outputURL: URL? = nil) throws {
@@ -63,7 +110,8 @@ public final class ScreenRecorder {
63
110
  newVideoOutputURL = passedVideoOutput
64
111
  } else {
65
112
  let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
66
- newVideoOutputURL = URL(fileURLWithPath: documentsPath.appendingPathComponent("WylerNewVideo.mp4"))
113
+ let fileName = "WylerNewVideo.\(videoFormat.fileExtension)"
114
+ newVideoOutputURL = URL(fileURLWithPath: documentsPath.appendingPathComponent(fileName))
67
115
  self.videoOutputURL = newVideoOutputURL
68
116
  }
69
117
 
@@ -72,7 +120,7 @@ public final class ScreenRecorder {
72
120
  } catch {}
73
121
 
74
122
  do {
75
- try videoWriter = AVAssetWriter(outputURL: newVideoOutputURL, fileType: AVFileType.mp4)
123
+ try videoWriter = AVAssetWriter(outputURL: newVideoOutputURL, fileType: videoFormat.fileType)
76
124
  } catch let writerError as NSError {
77
125
  videoWriter = nil
78
126
  throw writerError
@@ -93,18 +141,9 @@ public final class ScreenRecorder {
93
141
  }
94
142
 
95
143
  private func createAndAddAudioInput() -> AVAssetWriterInput {
96
- let settings = [
97
- AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
98
- AVSampleRateKey: 12000,
99
- AVNumberOfChannelsKey: 1,
100
- AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
101
- ]
102
-
103
- let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: settings)
104
-
144
+ let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
105
145
  audioInput.expectsMediaDataInRealTime = true
106
146
  videoWriter?.add(audioInput)
107
-
108
147
  return audioInput
109
148
  }
110
149
 
@@ -119,15 +158,20 @@ public final class ScreenRecorder {
119
158
  handler(passedError)
120
159
  sent = true
121
160
  }
161
+ return
122
162
  }
123
163
 
124
164
  switch sampleType {
125
165
  case .video:
126
166
  self.handleSampleBuffer(sampleBuffer: sampleBuffer)
127
167
  case .audioApp:
128
- self.add(sample: sampleBuffer, to: self.appAudioWriterInput)
168
+ if self.recordAudio {
169
+ self.add(sample: sampleBuffer, to: self.appAudioWriterInput)
170
+ }
129
171
  case .audioMic:
130
- self.add(sample: sampleBuffer, to: self.micAudioWriterInput)
172
+ if self.recordAudio {
173
+ self.add(sample: sampleBuffer, to: self.micAudioWriterInput)
174
+ }
131
175
  default:
132
176
  break
133
177
  }
@@ -149,26 +193,48 @@ public final class ScreenRecorder {
149
193
  }
150
194
 
151
195
  private func add(sample: CMSampleBuffer, to writerInput: AVAssetWriterInput?) {
152
- if writerInput?.isReadyForMoreMediaData ?? false {
153
- writerInput?.append(sample)
196
+ guard let writerInput = writerInput else { return }
197
+ guard self.videoWriter?.status == .writing else { return }
198
+ if writerInput.isReadyForMoreMediaData {
199
+ writerInput.append(sample)
154
200
  }
155
201
  }
156
202
 
157
- /**
158
- Stops recording the content of the application screen, after calling startRecording
159
-
160
- - Parameter errorHandler: Called when an error is found
161
- */
162
203
  public func stoprecording(handler: @escaping (Error?) -> Void) {
163
- recorder.stopCapture( handler: { error in
204
+ recorder.stopCapture(handler: { error in
164
205
  if let error = error {
165
206
  handler(error)
207
+ return
208
+ }
209
+
210
+ self.videoWriterInput?.markAsFinished()
211
+ self.micAudioWriterInput?.markAsFinished()
212
+ self.appAudioWriterInput?.markAsFinished()
213
+
214
+ guard let writer = self.videoWriter else {
215
+ handler(nil)
216
+ return
217
+ }
218
+
219
+ if writer.status == .writing {
220
+ writer.finishWriting {
221
+ if let finishError = writer.error {
222
+ handler(finishError)
223
+ return
224
+ }
225
+ if self.saveToCameraRoll {
226
+ self.saveVideoToCameraRollAfterAuthorized(handler: handler)
227
+ } else {
228
+ handler(nil)
229
+ }
230
+ }
231
+ } else if writer.status == .failed {
232
+ handler(writer.error)
166
233
  } else {
167
- self.videoWriterInput?.markAsFinished()
168
- self.micAudioWriterInput?.markAsFinished()
169
- self.appAudioWriterInput?.markAsFinished()
170
- self.videoWriter?.finishWriting {
234
+ if self.saveToCameraRoll {
171
235
  self.saveVideoToCameraRollAfterAuthorized(handler: handler)
236
+ } else {
237
+ handler(nil)
172
238
  }
173
239
  }
174
240
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-screen-recorder",
3
- "version": "8.2.37",
3
+ "version": "8.3.0",
4
4
  "description": "Record device's screen",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",