@capgo/capacitor-screen-recorder 8.2.36 → 8.2.38

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.
@@ -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,182 @@
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 recordingSession: Intent? = null
37
+ private var serviceBinder: CapgoRecorderService? = null
38
+ private var outputFile: File? = null
39
+
40
+ private val metrics by lazy {
41
+ DisplayMetrics().apply { activity.windowManager.defaultDisplay.getMetrics(this) }
42
+ }
43
+
44
+ private val dpi by lazy { metrics.density }
45
+
46
+ private val notificationProvider by lazy {
47
+ RecorderNotificationProvider(activity, options.notification)
48
+ }
49
+
50
+ private val broadcaster by lazy { LocalBroadcastManager.getInstance(activity) }
51
+
52
+ private val connection = object : ServiceConnection {
53
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
54
+ val binder = service as CapgoRecorderService.LocalBinder
55
+ serviceBinder = binder.service
56
+ serviceBinder?.setNotificationProvider(notificationProvider)
57
+ }
58
+
59
+ override fun onServiceDisconnected(arg0: ComponentName) {
60
+ serviceBinder = null
61
+ }
62
+ }
63
+
64
+ private val recordingStateHandler = object : android.content.BroadcastReceiver() {
65
+ override fun onReceive(context: Context?, intent: Intent?) {
66
+ if (intent?.action == STATE_IDLE) {
67
+ cleanupSession()
68
+ }
69
+ }
70
+ }
71
+
72
+ private val permissionListener = object : MultiplePermissionsListener {
73
+ override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
74
+ startProjection.launch(Unit)
75
+ }
76
+
77
+ override fun onPermissionRationaleShouldBeShown(
78
+ permissions: MutableList<PermissionRequest>?,
79
+ token: PermissionToken?,
80
+ ) {
81
+ token?.continuePermissionRequest()
82
+ }
83
+ }
84
+
85
+ private val startProjection = activity.registerForActivityResult(CapgoRecordScreen()) { result ->
86
+ if (result.resultCode != Activity.RESULT_OK) {
87
+ return@registerForActivityResult
88
+ }
89
+ val file = resolveOutputFile() ?: return@registerForActivityResult
90
+ startService(result, file)
91
+ }
92
+
93
+ fun updateOptions(options: Options) {
94
+ this.options = handleDynamicVideoSize(options)
95
+ }
96
+
97
+ fun record() {
98
+ Dexter.withContext(activity)
99
+ .withPermissions(
100
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
101
+ Manifest.permission.READ_EXTERNAL_STORAGE,
102
+ Manifest.permission.RECORD_AUDIO,
103
+ )
104
+ .withListener(permissionListener)
105
+ .check()
106
+ }
107
+
108
+ fun stopRecording() {
109
+ broadcaster.sendBroadcast(Intent(Action.Stop.name))
110
+ }
111
+
112
+ private fun resolveOutputFile(): File? {
113
+ val dir = options.storage.mediaStorageLocation ?: return null
114
+ return File("${dir.path}${File.separator}${options.storage.fileNameFormatter()}.mp4")
115
+ }
116
+
117
+ private fun startService(result: ActivityResult, file: File) {
118
+ outputFile = file
119
+ val session = Intent(activity, CapgoRecorderService::class.java).apply {
120
+ putExtra("code", result.resultCode)
121
+ putExtra("data", result.data)
122
+ putExtra("options", options)
123
+ putExtra("outputFile", file.absolutePath)
124
+ putExtra("dpi", dpi)
125
+ putExtra("rotation", activity.windowManager.defaultDisplay.rotation)
126
+ }
127
+ recordingSession = session
128
+
129
+ broadcaster.registerReceiver(
130
+ recordingStateHandler,
131
+ IntentFilter().apply {
132
+ addAction(STATE_IDLE)
133
+ addAction(STATE_RECORDING)
134
+ },
135
+ )
136
+
137
+ activity.bindService(session, connection, Context.BIND_AUTO_CREATE)
138
+ activity.startService(session)
139
+ }
140
+
141
+ private fun cleanupSession() {
142
+ try {
143
+ broadcaster.unregisterReceiver(recordingStateHandler)
144
+ } catch (ignored: Exception) {
145
+ Log.d("CapgoScreenRecorder", "Receiver already unregistered", ignored)
146
+ }
147
+
148
+ try {
149
+ activity.unbindService(connection)
150
+ } catch (ignored: Exception) {
151
+ Log.d("CapgoScreenRecorder", "Service already unbound", ignored)
152
+ }
153
+
154
+ recordingSession?.let { activity.stopService(it) }
155
+ recordingSession = null
156
+
157
+ outputFile?.let { file ->
158
+ MediaScannerConnection.scanFile(activity, arrayOf(file.absolutePath), null) { path, uri ->
159
+ Log.i("CapgoScreenRecorder", "Saved recording: $path uri=$uri")
160
+ }
161
+ }
162
+ outputFile = null
163
+ }
164
+
165
+ private fun handleDynamicVideoSize(options: Options): Options {
166
+ var reconfig = options
167
+ if (options.video.width == -1) {
168
+ reconfig = reconfig.copy(video = reconfig.video.copy(width = metrics.widthPixels))
169
+ }
170
+ if (options.video.height == -1) {
171
+ reconfig = reconfig.copy(video = reconfig.video.copy(height = metrics.heightPixels))
172
+ }
173
+ return reconfig
174
+ }
175
+
176
+ companion object {
177
+ @JvmStatic
178
+ fun use(activity: ComponentActivity): CapgoScrCastWithAudio {
179
+ return CapgoScrCastWithAudio(activity)
180
+ }
181
+ }
182
+ }
@@ -11,27 +11,51 @@ 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.36";
14
+ private final String pluginVersion = "8.2.38";
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
+ recordingWithAudio = recordAudio;
34
+
35
+ if (recordAudio) {
36
+ audioRecorder.record();
37
+ } else {
38
+ recorder.record();
39
+ }
40
+ call.resolve();
41
+ } catch (final Exception e) {
42
+ call.reject("Could not start screen recording", e);
43
+ }
29
44
  }
30
45
 
31
46
  @PluginMethod
32
47
  public void stop(PluginCall call) {
33
- recorder.stopRecording();
34
- call.resolve();
48
+ try {
49
+ if (recordingWithAudio) {
50
+ audioRecorder.stopRecording();
51
+ } else {
52
+ recorder.stopRecording();
53
+ }
54
+ recordingWithAudio = false;
55
+ call.resolve();
56
+ } catch (final Exception e) {
57
+ call.reject("Could not stop screen recording", e);
58
+ }
35
59
  }
36
60
 
37
61
  @PluginMethod
@@ -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 />
@@ -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.36"
10
+ private let pluginVersion: String = "8.2.38"
11
11
  public let identifier = "ScreenRecorderPlugin"
12
12
  public let jsName = "ScreenRecorder"
13
13
  public let pluginMethods: [CAPPluginMethod] = [
@@ -18,7 +18,8 @@ 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
+ implementation.startRecording(saveToCameraRoll: true, recordAudio: recordAudio, handler: { error in
22
23
  if let error = error {
23
24
  debugPrint("Error when start recording \(error)")
24
25
  call.reject("Cannot start recording")
@@ -6,9 +6,11 @@
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
@@ -22,37 +24,47 @@ public final class ScreenRecorder {
22
24
  private var micAudioWriterInput: AVAssetWriterInput?
23
25
  private var appAudioWriterInput: AVAssetWriterInput?
24
26
  private var saveToCameraRoll = false
27
+ private var recordAudio = false
25
28
  let recorder = RPScreenRecorder.shared()
26
29
 
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
30
  public func startRecording(to outputURL: URL? = nil,
36
31
  size: CGSize? = nil,
37
32
  saveToCameraRoll: Bool = false,
33
+ recordAudio: Bool = false,
38
34
  handler: @escaping (Error?) -> Void) {
39
- recorder.isMicrophoneEnabled = true
35
+ self.saveToCameraRoll = saveToCameraRoll
36
+ self.recordAudio = recordAudio
37
+ resetWriterState()
38
+
39
+ recorder.isMicrophoneEnabled = recordAudio
40
+
40
41
  do {
42
+ if recordAudio {
43
+ try configureAudioSession()
44
+ }
41
45
  try createVideoWriter(in: outputURL)
42
46
  addVideoWriterInput(size: size)
43
- self.micAudioWriterInput = createAndAddAudioInput()
44
- self.appAudioWriterInput = createAndAddAudioInput()
47
+ if recordAudio {
48
+ self.micAudioWriterInput = createAndAddAudioInput()
49
+ self.appAudioWriterInput = createAndAddAudioInput()
50
+ }
45
51
  startCapture(handler: handler)
46
52
  } catch let err {
47
53
  handler(err)
48
54
  }
49
55
  }
50
56
 
51
- private func checkPhotoLibraryAuthorizationStatus() {
52
- let status = PHPhotoLibrary.authorizationStatus()
53
- if status == .notDetermined {
54
- PHPhotoLibrary.requestAuthorization({ _ in })
55
- }
57
+ private func resetWriterState() {
58
+ videoWriter = nil
59
+ videoWriterInput = nil
60
+ micAudioWriterInput = nil
61
+ appAudioWriterInput = nil
62
+ }
63
+
64
+ private func configureAudioSession() throws {
65
+ let session = AVAudioSession.sharedInstance()
66
+ try session.setCategory(.playAndRecord, mode: .videoRecording, options: [.defaultToSpeaker, .mixWithOthers])
67
+ try session.setActive(true)
56
68
  }
57
69
 
58
70
  private func createVideoWriter(in outputURL: URL? = nil) throws {
@@ -93,18 +105,9 @@ public final class ScreenRecorder {
93
105
  }
94
106
 
95
107
  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
-
108
+ let audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: nil)
105
109
  audioInput.expectsMediaDataInRealTime = true
106
110
  videoWriter?.add(audioInput)
107
-
108
111
  return audioInput
109
112
  }
110
113
 
@@ -119,15 +122,20 @@ public final class ScreenRecorder {
119
122
  handler(passedError)
120
123
  sent = true
121
124
  }
125
+ return
122
126
  }
123
127
 
124
128
  switch sampleType {
125
129
  case .video:
126
130
  self.handleSampleBuffer(sampleBuffer: sampleBuffer)
127
131
  case .audioApp:
128
- self.add(sample: sampleBuffer, to: self.appAudioWriterInput)
132
+ if self.recordAudio {
133
+ self.add(sample: sampleBuffer, to: self.appAudioWriterInput)
134
+ }
129
135
  case .audioMic:
130
- self.add(sample: sampleBuffer, to: self.micAudioWriterInput)
136
+ if self.recordAudio {
137
+ self.add(sample: sampleBuffer, to: self.micAudioWriterInput)
138
+ }
131
139
  default:
132
140
  break
133
141
  }
@@ -149,26 +157,48 @@ public final class ScreenRecorder {
149
157
  }
150
158
 
151
159
  private func add(sample: CMSampleBuffer, to writerInput: AVAssetWriterInput?) {
152
- if writerInput?.isReadyForMoreMediaData ?? false {
153
- writerInput?.append(sample)
160
+ guard let writerInput = writerInput else { return }
161
+ guard self.videoWriter?.status == .writing else { return }
162
+ if writerInput.isReadyForMoreMediaData {
163
+ writerInput.append(sample)
154
164
  }
155
165
  }
156
166
 
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
167
  public func stoprecording(handler: @escaping (Error?) -> Void) {
163
- recorder.stopCapture( handler: { error in
168
+ recorder.stopCapture(handler: { error in
164
169
  if let error = error {
165
170
  handler(error)
171
+ return
172
+ }
173
+
174
+ self.videoWriterInput?.markAsFinished()
175
+ self.micAudioWriterInput?.markAsFinished()
176
+ self.appAudioWriterInput?.markAsFinished()
177
+
178
+ guard let writer = self.videoWriter else {
179
+ handler(nil)
180
+ return
181
+ }
182
+
183
+ if writer.status == .writing {
184
+ writer.finishWriting {
185
+ if let finishError = writer.error {
186
+ handler(finishError)
187
+ return
188
+ }
189
+ if self.saveToCameraRoll {
190
+ self.saveVideoToCameraRollAfterAuthorized(handler: handler)
191
+ } else {
192
+ handler(nil)
193
+ }
194
+ }
195
+ } else if writer.status == .failed {
196
+ handler(writer.error)
166
197
  } else {
167
- self.videoWriterInput?.markAsFinished()
168
- self.micAudioWriterInput?.markAsFinished()
169
- self.appAudioWriterInput?.markAsFinished()
170
- self.videoWriter?.finishWriting {
198
+ if self.saveToCameraRoll {
171
199
  self.saveVideoToCameraRollAfterAuthorized(handler: handler)
200
+ } else {
201
+ handler(nil)
172
202
  }
173
203
  }
174
204
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-screen-recorder",
3
- "version": "8.2.36",
3
+ "version": "8.2.38",
4
4
  "description": "Record device's screen",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -55,8 +55,7 @@
55
55
  "prepublishOnly": "bun run build",
56
56
  "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
57
57
  "example:install": "cd example-app && bun install --frozen-lockfile",
58
- "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
59
- "example:capgo:deploy": "bun run example:build && bun scripts/deploy-example-capgo.mjs"
58
+ "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
60
59
  },
61
60
  "devDependencies": {
62
61
  "@capacitor/android": "^8.0.0",