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