@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.
- package/android/build.gradle +4 -0
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/{CapgoScrCastWithAudio.kt → CapgoScrCast.kt} +52 -26
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/MediaRecorderPrepare.kt +14 -0
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/ScreenRecorderPlugin.java +33 -25
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/VideoSizeResolver.kt +31 -0
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/service/CapgoRecorderService.kt +31 -7
- package/ios/Sources/ScreenRecorderPlugin/ScreenRecorderPlugin.swift +1 -1
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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):
|
|
189
|
-
return
|
|
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.
|
|
13
|
+
private final String pluginVersion = "8.3.5";
|
|
15
14
|
|
|
16
|
-
private
|
|
17
|
-
private
|
|
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
|
-
|
|
23
|
-
audioRecorder =
|
|
24
|
-
Options options = new Options();
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/android/src/main/java/ee/forgr/plugin/screenrecorder/service/CapgoRecorderService.kt
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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] = [
|