@capgo/capacitor-screen-recorder 8.3.3 → 8.3.5

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.
@@ -45,6 +45,9 @@ android {
45
45
  sourceCompatibility JavaVersion.VERSION_21
46
46
  targetCompatibility JavaVersion.VERSION_21
47
47
  }
48
+ testOptions {
49
+ unitTests.returnDefaultValues = true
50
+ }
48
51
  }
49
52
 
50
53
  kotlin {
@@ -63,6 +66,7 @@ dependencies {
63
66
  implementation project(':capacitor-android')
64
67
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
65
68
  testImplementation "junit:junit:$junitVersion"
69
+ testImplementation "org.mockito:mockito-core:5.14.2"
66
70
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
67
71
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
68
72
  implementation "androidx.core:core-ktx:$androidxCoreVersion"
@@ -25,19 +25,21 @@ import dev.bmcreations.scrcast.internal.recorder.EXTRA_ERROR
25
25
  import dev.bmcreations.scrcast.internal.recorder.STATE_IDLE
26
26
  import dev.bmcreations.scrcast.internal.recorder.STATE_RECORDING
27
27
  import dev.bmcreations.scrcast.internal.recorder.notification.RecorderNotificationProvider
28
- import dev.bmcreations.scrcast.recorder.RecordingState
29
28
  import ee.forgr.plugin.screenrecorder.service.CapgoRecorderService
30
29
  import java.io.File
31
30
 
32
- class CapgoScrCastWithAudio private constructor(private val activity: ComponentActivity) {
31
+ class CapgoScrCast private constructor(
32
+ private val activity: ComponentActivity,
33
+ private val recordAudio: Boolean,
34
+ ) {
33
35
  var options: Options = Options()
34
36
  private set
35
37
 
36
38
  private var fileExtension: String = "mp4"
37
-
38
39
  private var recordingSession: Intent? = null
39
40
  private var serviceBinder: CapgoRecorderService? = null
40
41
  private var outputFile: File? = null
42
+ private var startListener: StartListener? = null
41
43
 
42
44
  private val metrics by lazy {
43
45
  DisplayMetrics().apply { activity.windowManager.defaultDisplay.getMetrics(this) }
@@ -65,15 +67,30 @@ class CapgoScrCastWithAudio private constructor(private val activity: ComponentA
65
67
 
66
68
  private val recordingStateHandler = object : android.content.BroadcastReceiver() {
67
69
  override fun onReceive(context: Context?, intent: Intent?) {
68
- if (intent?.action == STATE_IDLE) {
69
- cleanupSession()
70
+ when (intent?.action) {
71
+ STATE_RECORDING -> {
72
+ startListener?.onStarted()
73
+ startListener = null
74
+ }
75
+ STATE_IDLE -> {
76
+ val error = intent.getSerializableExtra(EXTRA_ERROR) as? Throwable
77
+ if (error != null) {
78
+ startListener?.onFailed(error)
79
+ }
80
+ cleanupSession()
81
+ }
70
82
  }
71
83
  }
72
84
  }
73
85
 
74
86
  private val permissionListener = object : MultiplePermissionsListener {
75
87
  override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
76
- startProjection.launch(Unit)
88
+ if (report?.areAllPermissionsGranted() == true) {
89
+ startProjection.launch(Unit)
90
+ } else {
91
+ startListener?.onFailed(SecurityException("Required permissions were not granted"))
92
+ startListener = null
93
+ }
77
94
  }
78
95
 
79
96
  override fun onPermissionRationaleShouldBeShown(
@@ -86,31 +103,40 @@ class CapgoScrCastWithAudio private constructor(private val activity: ComponentA
86
103
 
87
104
  private val startProjection = activity.registerForActivityResult(CapgoRecordScreen()) { result ->
88
105
  if (result.resultCode != Activity.RESULT_OK) {
106
+ startListener?.onFailed(IllegalStateException("Screen capture permission denied"))
107
+ return@registerForActivityResult
108
+ }
109
+ val file = resolveOutputFile()
110
+ if (file == null) {
111
+ startListener?.onFailed(IllegalStateException("Could not resolve screen recording output file"))
89
112
  return@registerForActivityResult
90
113
  }
91
- val file = resolveOutputFile() ?: return@registerForActivityResult
92
114
  startService(result, file)
93
115
  }
94
116
 
95
117
  fun updateOptions(options: Options) {
96
- this.options = handleDynamicVideoSize(options)
118
+ this.options = resolveVideoSize(options)
97
119
  }
98
120
 
99
121
  fun updateVideoFormat(format: String?) {
100
122
  val resolved = VideoFormatResolver.resolve(format)
101
123
  fileExtension = resolved.extension
102
- options = handleDynamicVideoSize(
124
+ options = resolveVideoSize(
103
125
  options.copy(storage = options.storage.copy(outputFormat = resolved.outputFormat)),
104
126
  )
105
127
  }
106
128
 
107
- fun record() {
129
+ fun record(listener: StartListener) {
130
+ startListener = listener
131
+ val permissions = buildList {
132
+ add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
133
+ add(Manifest.permission.READ_EXTERNAL_STORAGE)
134
+ if (recordAudio) {
135
+ add(Manifest.permission.RECORD_AUDIO)
136
+ }
137
+ }
108
138
  Dexter.withContext(activity)
109
- .withPermissions(
110
- Manifest.permission.WRITE_EXTERNAL_STORAGE,
111
- Manifest.permission.READ_EXTERNAL_STORAGE,
112
- Manifest.permission.RECORD_AUDIO,
113
- )
139
+ .withPermissions(permissions)
114
140
  .withListener(permissionListener)
115
141
  .check()
116
142
  }
@@ -119,6 +145,10 @@ class CapgoScrCastWithAudio private constructor(private val activity: ComponentA
119
145
  broadcaster.sendBroadcast(Intent(Action.Stop.name))
120
146
  }
121
147
 
148
+ private fun resolveVideoSize(options: Options): Options {
149
+ return VideoSizeResolver.applyTo(options, metrics.widthPixels, metrics.heightPixels)
150
+ }
151
+
122
152
  private fun resolveOutputFile(): File? {
123
153
  val dir = options.storage.mediaStorageLocation ?: return null
124
154
  return File("${dir.path}${File.separator}${options.storage.fileNameFormatter()}.$fileExtension")
@@ -133,6 +163,7 @@ class CapgoScrCastWithAudio private constructor(private val activity: ComponentA
133
163
  putExtra("outputFile", file.absolutePath)
134
164
  putExtra("dpi", dpi)
135
165
  putExtra("rotation", activity.windowManager.defaultDisplay.rotation)
166
+ putExtra("recordAudio", recordAudio)
136
167
  }
137
168
  recordingSession = session
138
169
 
@@ -149,6 +180,7 @@ class CapgoScrCastWithAudio private constructor(private val activity: ComponentA
149
180
  }
150
181
 
151
182
  private fun cleanupSession() {
183
+ startListener = null
152
184
  try {
153
185
  broadcaster.unregisterReceiver(recordingStateHandler)
154
186
  } catch (ignored: Exception) {
@@ -172,21 +204,15 @@ class CapgoScrCastWithAudio private constructor(private val activity: ComponentA
172
204
  outputFile = null
173
205
  }
174
206
 
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
207
+ interface StartListener {
208
+ fun onStarted()
209
+ fun onFailed(error: Throwable)
184
210
  }
185
211
 
186
212
  companion object {
187
213
  @JvmStatic
188
- fun use(activity: ComponentActivity): CapgoScrCastWithAudio {
189
- return CapgoScrCastWithAudio(activity)
214
+ fun use(activity: ComponentActivity, recordAudio: Boolean): CapgoScrCast {
215
+ return CapgoScrCast(activity, recordAudio)
190
216
  }
191
217
  }
192
218
  }
@@ -0,0 +1,14 @@
1
+ package ee.forgr.plugin.screenrecorder
2
+
3
+ import android.media.MediaRecorder
4
+
5
+ internal object MediaRecorderPrepare {
6
+ fun tryPrepare(recorder: MediaRecorder?): Throwable? {
7
+ return try {
8
+ recorder?.prepare()
9
+ null
10
+ } catch (error: Throwable) {
11
+ error
12
+ }
13
+ }
14
+ }
@@ -5,47 +5,55 @@ import com.getcapacitor.Plugin;
5
5
  import com.getcapacitor.PluginCall;
6
6
  import com.getcapacitor.PluginMethod;
7
7
  import com.getcapacitor.annotation.CapacitorPlugin;
8
- import dev.bmcreations.scrcast.ScrCast;
9
8
  import dev.bmcreations.scrcast.config.Options;
10
9
 
11
10
  @CapacitorPlugin(name = "ScreenRecorder")
12
11
  public class ScreenRecorderPlugin extends Plugin {
13
12
 
14
- private final String pluginVersion = "8.3.3";
13
+ private final String pluginVersion = "8.3.5";
15
14
 
16
- private ScrCast recorder;
17
- private CapgoScrCastWithAudio audioRecorder;
15
+ private CapgoScrCast videoRecorder;
16
+ private CapgoScrCast audioRecorder;
18
17
  private boolean recordingWithAudio = false;
19
18
 
20
19
  @Override
21
20
  public void load() {
22
- recorder = ScrCast.use(this.bridge.getActivity());
23
- audioRecorder = CapgoScrCastWithAudio.use(this.bridge.getActivity());
24
- Options options = new Options();
25
- recorder.updateOptions(options);
21
+ videoRecorder = CapgoScrCast.use(this.bridge.getActivity(), false);
22
+ audioRecorder = CapgoScrCast.use(this.bridge.getActivity(), true);
23
+ final Options options = new Options();
24
+ videoRecorder.updateOptions(options);
26
25
  audioRecorder.updateOptions(options);
27
26
  }
28
27
 
29
28
  @PluginMethod
30
- public void start(PluginCall call) {
31
- try {
32
- final boolean recordAudio = call.getBoolean("recordAudio", false);
33
- final String format = call.getString("format");
34
- recordingWithAudio = recordAudio;
29
+ public void start(final PluginCall call) {
30
+ final boolean recordAudio = call.getBoolean("recordAudio", false);
31
+ final String format = call.getString("format");
32
+ recordingWithAudio = recordAudio;
35
33
 
36
- final Options configuredOptions = VideoFormatResolver.INSTANCE.applyTo(recorder.getOptions(), format);
37
- recorder.updateOptions(configuredOptions);
38
- audioRecorder.updateVideoFormat(format);
34
+ final CapgoScrCast recorder = recordAudio ? audioRecorder : videoRecorder;
35
+ final Options configuredOptions = VideoFormatResolver.INSTANCE.applyTo(recorder.getOptions(), format);
36
+ recorder.updateOptions(configuredOptions);
37
+ recorder.updateVideoFormat(format);
39
38
 
40
- if (recordAudio) {
41
- audioRecorder.record();
42
- } else {
43
- recorder.record();
39
+ call.setKeepAlive(true);
40
+ recorder.record(
41
+ new CapgoScrCast.StartListener() {
42
+ @Override
43
+ public void onStarted() {
44
+ call.resolve();
45
+ call.release(bridge);
46
+ }
47
+
48
+ @Override
49
+ public void onFailed(final Throwable error) {
50
+ recordingWithAudio = false;
51
+ final Exception exception = error instanceof Exception ? (Exception) error : new Exception(error);
52
+ call.reject("Could not start screen recording", exception);
53
+ call.release(bridge);
54
+ }
44
55
  }
45
- call.resolve();
46
- } catch (final Exception e) {
47
- call.reject("Could not start screen recording", e);
48
- }
56
+ );
49
57
  }
50
58
 
51
59
  @PluginMethod
@@ -54,7 +62,7 @@ public class ScreenRecorderPlugin extends Plugin {
54
62
  if (recordingWithAudio) {
55
63
  audioRecorder.stopRecording();
56
64
  } else {
57
- recorder.stopRecording();
65
+ videoRecorder.stopRecording();
58
66
  }
59
67
  recordingWithAudio = false;
60
68
  call.resolve();
@@ -0,0 +1,31 @@
1
+ package ee.forgr.plugin.screenrecorder
2
+
3
+ import dev.bmcreations.scrcast.config.Options
4
+
5
+ object VideoSizeResolver {
6
+ const val MAX_WIDTH = 1920
7
+ const val MAX_HEIGHT = 1080
8
+
9
+ fun applyTo(options: Options, displayWidth: Int, displayHeight: Int): Options {
10
+ val resolvedWidth = if (options.video.width == -1) displayWidth else options.video.width
11
+ val resolvedHeight = if (options.video.height == -1) displayHeight else options.video.height
12
+ val (width, height) = clamp(resolvedWidth, resolvedHeight)
13
+ return options.copy(video = options.video.copy(width = width, height = height))
14
+ }
15
+
16
+ fun clamp(width: Int, height: Int): Pair<Int, Int> {
17
+ var w = makeEven(width.coerceAtLeast(2))
18
+ var h = makeEven(height.coerceAtLeast(2))
19
+
20
+ if (w <= MAX_WIDTH && h <= MAX_HEIGHT) {
21
+ return w to h
22
+ }
23
+
24
+ val scale = minOf(MAX_WIDTH.toFloat() / w, MAX_HEIGHT.toFloat() / h)
25
+ w = makeEven((w * scale).toInt().coerceAtLeast(2))
26
+ h = makeEven((h * scale).toInt().coerceAtLeast(2))
27
+ return w to h
28
+ }
29
+
30
+ fun makeEven(value: Int): Int = if (value % 2 == 0) value else value - 1
31
+ }
@@ -23,10 +23,12 @@ 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.MediaRecorderPrepare
26
27
  import kotlinx.coroutines.Dispatchers
27
28
  import kotlinx.coroutines.GlobalScope
28
29
  import kotlinx.coroutines.launch
29
30
  import java.io.File
31
+ import java.io.IOException
30
32
 
31
33
  class CapgoRecorderService : Service() {
32
34
 
@@ -86,6 +88,7 @@ class CapgoRecorderService : Service() {
86
88
 
87
89
  private var requestCode: Int = -1
88
90
  private var requestData: Intent = Intent()
91
+ private var recordAudio: Boolean = false
89
92
 
90
93
  private var mediaProjection: MediaProjection? = null
91
94
  private var mediaProjectionCallback = MediaProjectionCallback()
@@ -120,13 +123,15 @@ class CapgoRecorderService : Service() {
120
123
  }
121
124
  }
122
125
 
123
- private fun createRecorder() {
126
+ private fun createRecorder(): Boolean {
124
127
  Log.d("scrcast", "createRecorder()")
125
128
  mediaRecorder = createMediaRecorder().apply {
126
- setAudioSource(AudioSource.MIC)
129
+ if (recordAudio) {
130
+ setAudioSource(AudioSource.MIC)
131
+ setAudioEncoder(AudioEncoder.AAC)
132
+ }
127
133
  setVideoSource(VideoSource.SURFACE)
128
134
  setOutputFormat(options.storage.outputFormat)
129
- setAudioEncoder(AudioEncoder.AAC)
130
135
  setOutputFile(outputFile)
131
136
  with(options.video) {
132
137
  setVideoSize(width, height)
@@ -167,7 +172,22 @@ class CapgoRecorderService : Service() {
167
172
  }
168
173
  setOrientationHint(orientation)
169
174
  }
170
- mediaRecorder?.prepare()
175
+
176
+ val prepareError = MediaRecorderPrepare.tryPrepare(mediaRecorder)
177
+ if (prepareError != null) {
178
+ Log.e("scrcast", "MediaRecorder prepare failed", prepareError)
179
+ releaseRecorder()
180
+ return false
181
+ }
182
+ return true
183
+ }
184
+
185
+ private fun releaseRecorder() {
186
+ runCatching {
187
+ mediaRecorder?.reset()
188
+ mediaRecorder?.release()
189
+ }
190
+ mediaRecorder = null
171
191
  }
172
192
 
173
193
  fun setNotificationProvider(provider: NotificationProvider) {
@@ -236,7 +256,10 @@ class CapgoRecorderService : Service() {
236
256
  }
237
257
 
238
258
  mediaProjection?.registerCallback(mediaProjectionCallback, Handler())
239
- createRecorder()
259
+ if (!createRecorder()) {
260
+ stopRecording(IOException("MediaRecorder prepare failed"))
261
+ return@launch
262
+ }
240
263
  virtualDisplay // touch
241
264
  try {
242
265
  mediaRecorder?.start()
@@ -260,12 +283,12 @@ class CapgoRecorderService : Service() {
260
283
  mediaProjection = null
261
284
 
262
285
  _virtualDisplay?.release()
286
+ _virtualDisplay = null
263
287
 
264
288
  runCatching {
265
289
  mediaRecorder?.stop()
266
- mediaRecorder?.reset()
267
- mediaRecorder?.release()
268
290
  }
291
+ releaseRecorder()
269
292
  }
270
293
 
271
294
  private inner class MediaProjectionCallback : MediaProjection.Callback() {
@@ -281,6 +304,7 @@ class CapgoRecorderService : Service() {
281
304
  rotation = it.getIntExtra("rotation", 0)
282
305
  dpi = it.getFloatExtra("dpi", 0f)
283
306
  outputFile = it.getStringExtra("outputFile") ?: ""
307
+ recordAudio = it.getBooleanExtra("recordAudio", false)
284
308
 
285
309
  startRecording(
286
310
  code = it.getIntExtra("code", -1),
@@ -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.3"
10
+ private let pluginVersion: String = "8.3.5"
11
11
  public let identifier = "ScreenRecorderPlugin"
12
12
  public let jsName = "ScreenRecorder"
13
13
  public let pluginMethods: [CAPPluginMethod] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-screen-recorder",
3
- "version": "8.3.3",
3
+ "version": "8.3.5",
4
4
  "description": "Record device's screen",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",