@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.
- package/android/build.gradle +2 -0
- package/android/src/main/AndroidManifest.xml +11 -1
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/CapgoRecordScreen.kt +19 -0
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/CapgoScrCastWithAudio.kt +182 -0
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/ScreenRecorderPlugin.java +29 -5
- package/android/src/main/java/ee/forgr/plugin/screenrecorder/service/CapgoRecorderService.kt +317 -0
- package/android/src/main/res/xml/screen_recorder_backup_rules.xml +2 -0
- package/ios/Sources/ScreenRecorderPlugin/ScreenRecorderPlugin.swift +3 -2
- package/ios/Sources/ScreenRecorderPlugin/Wyler.swift +71 -41
- package/package.json +2 -3
package/android/build.gradle
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
+
}
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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.
|
|
132
|
+
if self.recordAudio {
|
|
133
|
+
self.add(sample: sampleBuffer, to: self.appAudioWriterInput)
|
|
134
|
+
}
|
|
129
135
|
case .audioMic:
|
|
130
|
-
self.
|
|
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
|
-
|
|
153
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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",
|