expo-turbo-screen-recorder 0.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.
Files changed (31) hide show
  1. package/.eslintrc.js +2 -0
  2. package/.prettierrc +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +35 -0
  5. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  6. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  7. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  8. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/8.9/gc.properties +0 -0
  10. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  11. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  12. package/android/.gradle/vcs-1/gc.properties +0 -0
  13. package/android/build.gradle +50 -0
  14. package/android/src/main/AndroidManifest.xml +16 -0
  15. package/android/src/main/java/expo/modules/screenrecorder/ExpoScreenRecorderModule.kt +198 -0
  16. package/android/src/main/java/expo/modules/screenrecorder/RecordingOptions.kt +15 -0
  17. package/android/src/main/java/expo/modules/screenrecorder/ScreenCaptureService.kt +160 -0
  18. package/build/ExpoScreenRecorder.types.js +2 -0
  19. package/build/ExpoScreenRecorderModule.js +11 -0
  20. package/build/ExpoScreenRecorderModule.web.js +73 -0
  21. package/build/index.js +77 -0
  22. package/eslint.config.cjs +5 -0
  23. package/expo-module.config.json +9 -0
  24. package/ios/ExpoScreenRecorder.podspec +23 -0
  25. package/ios/ExpoScreenRecorderModule.swift +147 -0
  26. package/package.json +39 -0
  27. package/src/ExpoScreenRecorder.types.ts +19 -0
  28. package/src/ExpoScreenRecorderModule.ts +32 -0
  29. package/src/ExpoScreenRecorderModule.web.ts +15 -0
  30. package/src/index.ts +22 -0
  31. package/tsconfig.json +9 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,2 @@
1
+ // @generated by expo-module-scripts
2
+ module.exports = require('expo-module-scripts/eslintrc.base.js');
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "printWidth": 100,
3
+ "tabWidth": 2,
4
+ "singleQuote": true,
5
+ "bracketSameLine": true,
6
+ "trailingComma": "es5",
7
+ "jsxSingleQuote": false,
8
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # expo-screen-recorder
2
+
3
+ Expo Module For High Quality Screen Recorder
4
+
5
+ # API documentation
6
+
7
+ - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/screen-recorder/)
8
+ - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/screen-recorder/)
9
+
10
+ # Installation in managed Expo projects
11
+
12
+ For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If you follow the link and there is no documentation available then this library is not yet usable within managed projects — it is likely to be included in an upcoming Expo SDK release.
13
+
14
+ # Installation in bare React Native projects
15
+
16
+ For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
17
+
18
+ ### Add the package to your npm dependencies
19
+
20
+ ```
21
+ npm install expo-screen-recorder
22
+ ```
23
+
24
+ ### Configure for Android
25
+
26
+
27
+
28
+
29
+ ### Configure for iOS
30
+
31
+ Run `npx pod-install` after installing the npm package.
32
+
33
+ # Contributing
34
+
35
+ Contributions are very welcome! Please refer to guidelines described in the [contributing guide]( https://github.com/expo/expo#contributing).
File without changes
@@ -0,0 +1,2 @@
1
+ #Mon Jun 22 15:14:21 ICT 2026
2
+ gradle.version=8.9
File without changes
@@ -0,0 +1,50 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+
4
+
5
+ group = 'expo.modules.screenrecorder'
6
+ version = '0.3.0'
7
+
8
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
9
+ apply from: expoModulesCorePlugin
10
+ applyKotlinExpoModulesCorePlugin()
11
+ useCoreDependencies()
12
+ useExpoPublishing()
13
+
14
+
15
+ android {
16
+ namespace "expo.modules.screenrecorder"
17
+ defaultConfig {
18
+ versionCode 1
19
+ versionName "0.2.0"
20
+ }
21
+ lintOptions {
22
+ abortOnError false
23
+ }
24
+ }
25
+
26
+
27
+ dependencies {
28
+ // 2. Kotlin Standard Library (SDK 54 uses Kotlin 1.9+)
29
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
30
+
31
+ // 3. Android KTX for cleaner Kotlin code
32
+ implementation "androidx.core:core-ktx:1.12.0"
33
+
34
+ implementation "androidx.appcompat:appcompat:1.6.1"
35
+
36
+ // 4. Coroutines for background processing (Critical for VideoEncoder)
37
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2"
38
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
39
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
40
+
41
+
42
+ // 5. EXIF support for image rotation handling
43
+ implementation "androidx.exifinterface:exifinterface:1.3.6"
44
+
45
+ // 6. Lifecycle KTX (Useful if you want to scope encoding to the app lifecycle)
46
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
47
+
48
+ implementation("androidx.exifinterface:exifinterface:1.4.2")
49
+ }
50
+
@@ -0,0 +1,16 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="expo.modules.screenrecorder">
3
+
4
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
5
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
6
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
7
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
8
+
9
+ <application>
10
+ <service
11
+ android:name=".ScreenCaptureService"
12
+ android:enabled="true"
13
+ android:exported="false"
14
+ android:foregroundServiceType="mediaProjection" />
15
+ </application>
16
+ </manifest>
@@ -0,0 +1,198 @@
1
+ package expo.modules.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 android.os.Bundle
8
+ import android.util.DisplayMetrics
9
+ import android.view.WindowManager
10
+ import androidx.activity.result.contract.ActivityResultContract
11
+ import expo.modules.kotlin.activityresult.AppContextActivityResultCaller
12
+ import expo.modules.kotlin.activityresult.ActivityResultContractSenderAndroidX
13
+ import expo.modules.kotlin.modules.Module
14
+ import expo.modules.kotlin.modules.ModuleDefinition
15
+ import expo.modules.kotlin.Promise
16
+ import java.io.File
17
+ import java.util.UUID
18
+
19
+ // ── Contract ──────────────────────────────────────────────────────────────────
20
+ // Wraps MediaProjectionManager.createScreenCaptureIntent() in the AndroidX
21
+ // Activity Result API that Expo SDK 54 modules are expected to use.
22
+ class ScreenCaptureContract : ActivityResultContract<Unit, Pair<Int, Intent?>>() {
23
+
24
+ override fun createIntent(context: Context, input: Unit): Intent {
25
+ val mgr = context.getSystemService(Context.MEDIA_PROJECTION_SERVICE)
26
+ as MediaProjectionManager
27
+ return mgr.createScreenCaptureIntent()
28
+ }
29
+
30
+ override fun parseResult(resultCode: Int, intent: Intent?): Pair<Int, Intent?> =
31
+ resultCode to intent
32
+ }
33
+
34
+ // ── Module ────────────────────────────────────────────────────────────────────
35
+ class ExpoScreenRecorderModule : Module() {
36
+
37
+ private var pendingPromise: Promise? = null
38
+ private var currentOutputFile: File? = null
39
+ private var startTime: Long = 0L
40
+ private var includeAudioStream: Boolean = false
41
+ private var qualitySetting: String = "high"
42
+
43
+ // Launcher is initialised once inside OnCreate and reused for every recording
44
+ // request. It is torn down automatically when the module is destroyed because
45
+ // ActivityResultContractSenderAndroidX is scoped to the module lifecycle.
46
+ private lateinit var captureLauncher: ActivityResultContractSenderAndroidX<Unit, Pair<Int, Intent?>>
47
+
48
+ override fun definition() = ModuleDefinition {
49
+ Name("ExpoScreenRecorder")
50
+
51
+ // ── Lifecycle ─────────────────────────────────────────────────────────
52
+
53
+ OnCreate {
54
+ // Register the contract once. The returned sender is our handle for
55
+ // launching the system permission dialog on every startRecordingAsync call.
56
+ captureLauncher = appContext
57
+ .registerForActivityResult(ScreenCaptureContract()) { (resultCode, data) ->
58
+ handleCaptureResult(resultCode, data)
59
+ }
60
+ }
61
+
62
+ OnDestroy {
63
+ // Unregister the launcher so no stale callbacks fire after the
64
+ // module is torn down (e.g. fast-refresh, app backgrounding).
65
+ captureLauncher.unregister()
66
+
67
+ // If the module is destroyed mid-recording, reject the pending
68
+ // promise rather than leaving the caller hanging forever.
69
+ pendingPromise?.reject(
70
+ "ERR_MODULE_DESTROYED",
71
+ "Screen recorder module was destroyed before the operation completed.",
72
+ null
73
+ )
74
+ pendingPromise = null
75
+ }
76
+
77
+ // ── API ───────────────────────────────────────────────────────────────
78
+
79
+ AsyncFunction("isAvailableAsync") {
80
+ return@AsyncFunction true
81
+ }
82
+
83
+ AsyncFunction("startRecordingAsync") { options: RecordingOptions, promise: Promise ->
84
+ if (pendingPromise != null) {
85
+ promise.reject(
86
+ "ERR_ALREADY_RECORDING",
87
+ "A recording sequence is already initialised.",
88
+ null
89
+ )
90
+ return@AsyncFunction
91
+ }
92
+
93
+ val cacheDir = appContext.reactContext?.cacheDir
94
+ if (cacheDir == null) {
95
+ promise.reject("ERR_NO_CONTEXT", "Cache directory is unavailable.", null)
96
+ return@AsyncFunction
97
+ }
98
+
99
+ pendingPromise = promise
100
+ includeAudioStream = options.includeAudio ?: false
101
+ qualitySetting = options.quality ?: "high"
102
+ currentOutputFile = File(cacheDir, "REC_${UUID.randomUUID()}.mp4")
103
+
104
+ // Launch the system screen-capture permission dialog.
105
+ // The result lands in handleCaptureResult() via the contract callback.
106
+ captureLauncher.launch(
107
+ input = Unit,
108
+ onFailure = { ex ->
109
+ pendingPromise?.reject("ERR_LAUNCH_FAILED", ex.message, ex)
110
+ pendingPromise = null
111
+ }
112
+ )
113
+ }
114
+
115
+ AsyncFunction("stopRecordingAsync") { promise: Promise ->
116
+ val context = appContext.reactContext ?: run {
117
+ promise.reject(
118
+ "ERR_NO_CONTEXT",
119
+ "Application execution environment dropped.",
120
+ null
121
+ )
122
+ return@AsyncFunction
123
+ }
124
+
125
+ context.stopService(Intent(context, ScreenCaptureService::class.java))
126
+
127
+ val file = currentOutputFile
128
+ if (file == null || !file.exists()) {
129
+ promise.reject("ERR_NO_FILE", "Output file missing or recording failed.", null)
130
+ return@AsyncFunction
131
+ }
132
+
133
+ val durationSec = (System.currentTimeMillis() - startTime) / 1000.0
134
+ promise.resolve(
135
+ mapOf(
136
+ "uri" to "file://${file.absolutePath}",
137
+ "duration" to durationSec
138
+ )
139
+ )
140
+ }
141
+ }
142
+
143
+ // ── Private helpers ───────────────────────────────────────────────────────
144
+
145
+ private fun handleCaptureResult(resultCode: Int, data: Intent?) {
146
+ val promise = pendingPromise
147
+
148
+ if (resultCode != Activity.RESULT_OK || data == null) {
149
+ promise?.reject(
150
+ "ERR_PERMISSION_DENIED",
151
+ "User declined screen recording system permissions.",
152
+ null
153
+ )
154
+ pendingPromise = null
155
+ return
156
+ }
157
+
158
+ val context = appContext.reactContext ?: run {
159
+ promise?.reject("ERR_NO_CONTEXT", "React context unavailable.", null)
160
+ pendingPromise = null
161
+ return
162
+ }
163
+
164
+ val activity = appContext.currentActivity ?: run {
165
+ promise?.reject("ERR_NO_ACTIVITY", "Current activity context is missing.", null)
166
+ pendingPromise = null
167
+ return
168
+ }
169
+
170
+ // Resolve screen dimensions for the capture service
171
+ val metrics = DisplayMetrics()
172
+ @Suppress("DEPRECATION")
173
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
174
+ activity.display?.getRealMetrics(metrics)
175
+ } else {
176
+ (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
177
+ .defaultDisplay.getRealMetrics(metrics)
178
+ }
179
+
180
+ val serviceIntent = Intent(context, ScreenCaptureService::class.java).apply {
181
+ putExtra("RESULT_CODE", resultCode)
182
+ putExtra("RESULT_DATA", data)
183
+ putExtra("OUTPUT_PATH", currentOutputFile?.absolutePath)
184
+ putExtra("WIDTH", metrics.widthPixels)
185
+ putExtra("HEIGHT", metrics.heightPixels)
186
+ putExtra("DPI", metrics.densityDpi)
187
+ putExtra("INCLUDE_AUDIO", includeAudioStream)
188
+ putExtra("QUALITY", qualitySetting)
189
+ }
190
+
191
+ startTime = System.currentTimeMillis()
192
+ context.startForegroundService(serviceIntent)
193
+
194
+ // Resolve startRecordingAsync — the caller now awaits stopRecordingAsync.
195
+ promise?.resolve(null)
196
+ pendingPromise = null
197
+ }
198
+ }
@@ -0,0 +1,15 @@
1
+ package expo.modules.screenrecorder
2
+
3
+ import expo.modules.kotlin.records.Field
4
+ import expo.modules.kotlin.records.Record
5
+
6
+ class RecordingOptions : Record {
7
+ @Field
8
+ val durationLimit: Double? = null
9
+
10
+ @Field
11
+ val quality: String? = "high"
12
+
13
+ @Field
14
+ val includeAudio: Boolean? = false
15
+ }
@@ -0,0 +1,160 @@
1
+ package expo.modules.screenrecorder
2
+
3
+ import android.app.*
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.hardware.display.DisplayManager
7
+ import android.hardware.display.VirtualDisplay
8
+ import android.media.MediaRecorder
9
+ import android.media.projection.MediaProjection
10
+ import android.media.projection.MediaProjectionManager
11
+ import android.os.Build
12
+ import android.os.IBinder
13
+ import androidx.core.app.NotificationCompat
14
+
15
+ import java.io.File
16
+
17
+ class ScreenCaptureService : Service() {
18
+
19
+ private var mediaProjection: MediaProjection? = null
20
+ private var virtualDisplay: VirtualDisplay? = null
21
+ private var mediaRecorder: MediaRecorder? = null
22
+ private var isRecording = false
23
+
24
+ override fun onBind(intent: Intent?): IBinder? = null
25
+
26
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
27
+ val resultCode = intent?.getIntExtra("RESULT_CODE", Activity.RESULT_CANCELED)
28
+ ?: Activity.RESULT_CANCELED
29
+ val resultData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
30
+ intent?.getParcelableExtra("RESULT_DATA", Intent::class.java)
31
+ } else {
32
+ @Suppress("DEPRECATION")
33
+ intent?.getParcelableExtra("RESULT_DATA")
34
+ }
35
+ val outputPath = intent?.getStringExtra("OUTPUT_PATH") ?: ""
36
+ val width = intent?.getIntExtra("WIDTH", 720) ?: 720
37
+ val height = intent?.getIntExtra("HEIGHT", 1280) ?: 1280
38
+ val dpi = intent?.getIntExtra("DPI", 1) ?: 1
39
+ val includeAudio = intent?.getBooleanExtra("INCLUDE_AUDIO", false) ?: false
40
+ val quality = intent?.getStringExtra("QUALITY") ?: "high"
41
+
42
+ startForegroundNotification()
43
+
44
+ if (resultData == null) {
45
+ stopSelf()
46
+ return START_NOT_STICKY
47
+ }
48
+
49
+ val mpManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
50
+ mediaProjection = mpManager.getMediaProjection(resultCode, resultData)
51
+
52
+ setupMediaRecorder(outputPath, width, height, includeAudio, quality)
53
+
54
+ virtualDisplay = mediaProjection?.createVirtualDisplay(
55
+ "ExpoScreenCapture",
56
+ width,
57
+ height,
58
+ dpi,
59
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
60
+ mediaRecorder?.surface,
61
+ null,
62
+ null
63
+ )
64
+
65
+ try {
66
+ mediaRecorder?.start()
67
+ isRecording = true
68
+ } catch (e: Exception) {
69
+ e.printStackTrace()
70
+ stopSelf()
71
+ }
72
+
73
+ return START_NOT_STICKY
74
+ }
75
+
76
+ private fun setupMediaRecorder(
77
+ path: String,
78
+ width: Int,
79
+ height: Int,
80
+ includeAudio: Boolean,
81
+ quality: String
82
+ ) {
83
+ mediaRecorder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
84
+ MediaRecorder(this)
85
+ } else {
86
+ @Suppress("DEPRECATION")
87
+ MediaRecorder()
88
+ }.apply {
89
+ if (includeAudio) {
90
+ setAudioSource(MediaRecorder.AudioSource.MIC)
91
+ }
92
+ setVideoSource(MediaRecorder.VideoSource.SURFACE)
93
+ setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
94
+ setOutputFile(File(path).absolutePath)
95
+ setVideoEncoder(MediaRecorder.VideoEncoder.H264)
96
+ if (includeAudio) {
97
+ setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
98
+ }
99
+ setVideoSize(width, height)
100
+ setVideoEncodingBitRate(
101
+ when (quality) {
102
+ "low" -> 2 * 1024 * 1024
103
+ "medium" -> 4 * 1024 * 1024
104
+ else -> 8 * 1024 * 1024
105
+ }
106
+ )
107
+ setVideoFrameRate(30)
108
+ prepare()
109
+ }
110
+ }
111
+
112
+ private fun startForegroundNotification() {
113
+ val channelId = "screen_record_channel"
114
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
115
+
116
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
117
+ val channel = NotificationChannel(
118
+ channelId,
119
+ "Screen Recording",
120
+ NotificationManager.IMPORTANCE_LOW
121
+ )
122
+ notificationManager.createNotificationChannel(channel)
123
+ }
124
+
125
+ val notification = NotificationCompat.Builder(this, channelId)
126
+ .setContentTitle("Screen Recording Active")
127
+ .setContentText("Capturing screen...")
128
+ .setSmallIcon(android.R.drawable.presence_video_online)
129
+ .setOngoing(true)
130
+ .build()
131
+
132
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
133
+ startForeground(
134
+ 101,
135
+ notification,
136
+ android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
137
+ )
138
+ } else {
139
+ startForeground(101, notification)
140
+ }
141
+ }
142
+
143
+ override fun onDestroy() {
144
+ if (isRecording) {
145
+ try { mediaRecorder?.stop() } catch (_: Exception) {}
146
+ isRecording = false
147
+ }
148
+
149
+ mediaRecorder?.release()
150
+ mediaRecorder = null
151
+
152
+ virtualDisplay?.release() // release display before stopping projection
153
+ virtualDisplay = null
154
+
155
+ mediaProjection?.stop()
156
+ mediaProjection = null
157
+
158
+ super.onDestroy()
159
+ }
160
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ // import { NativeModule, requireNativeModule } from 'expo';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ // import { ExpoScreenRecorderModuleEvents } from './ExpoScreenRecorder.types';
5
+ // declare class ExpoScreenRecorderModule extends NativeModule<ExpoScreenRecorderModuleEvents> {
6
+ // hello(): string;
7
+ // setValueAsync(value: string): Promise<void>;
8
+ // }
9
+ // export default requireNativeModule<ExpoScreenRecorderModule>('ExpoScreenRecorder');
10
+ var expo_1 = require("expo");
11
+ exports.default = (0, expo_1.requireNativeModule)('ExpoScreenRecorder');
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __generator = (this && this.__generator) || function (thisArg, body) {
27
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
28
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
29
+ function verb(n) { return function (v) { return step([n, v]); }; }
30
+ function step(op) {
31
+ if (f) throw new TypeError("Generator is already executing.");
32
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
33
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
34
+ if (y = 0, t) op = [op[0] & 2, t.value];
35
+ switch (op[0]) {
36
+ case 0: case 1: t = op; break;
37
+ case 4: _.label++; return { value: op[1], done: false };
38
+ case 5: _.label++; y = op[1]; op = [0]; continue;
39
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
40
+ default:
41
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
42
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
43
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
44
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
45
+ if (t[2]) _.ops.pop();
46
+ _.trys.pop(); continue;
47
+ }
48
+ op = body.call(thisArg, _);
49
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
50
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
+ }
52
+ };
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ var expo_1 = require("expo");
55
+ var ExpoScreenRecorderModule = /** @class */ (function (_super) {
56
+ __extends(ExpoScreenRecorderModule, _super);
57
+ function ExpoScreenRecorderModule() {
58
+ return _super !== null && _super.apply(this, arguments) || this;
59
+ }
60
+ ExpoScreenRecorderModule.prototype.hello = function () {
61
+ return 'Hello world! 👋';
62
+ };
63
+ ExpoScreenRecorderModule.prototype.setValueAsync = function (value) {
64
+ return __awaiter(this, void 0, void 0, function () {
65
+ return __generator(this, function (_a) {
66
+ this.emit('onChange', { value: value });
67
+ return [2 /*return*/];
68
+ });
69
+ });
70
+ };
71
+ return ExpoScreenRecorderModule;
72
+ }(expo_1.NativeModule));
73
+ exports.default = (0, expo_1.registerWebModule)(ExpoScreenRecorderModule, 'ExpoScreenRecorderModule');
package/build/index.js ADDED
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ // Reexport the native module. On web, it will be resolved to ExpoScreenRecorderModule.web.ts
3
+ // and on native platforms to ExpoScreenRecorderModule.ts
4
+ // export { default } from './ExpoScreenRecorderModule';
5
+ // export * from './ExpoScreenRecorder.types';
6
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
7
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
8
+ return new (P || (P = Promise))(function (resolve, reject) {
9
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
10
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
11
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
12
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
13
+ });
14
+ };
15
+ var __generator = (this && this.__generator) || function (thisArg, body) {
16
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
17
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
18
+ function verb(n) { return function (v) { return step([n, v]); }; }
19
+ function step(op) {
20
+ if (f) throw new TypeError("Generator is already executing.");
21
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
22
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
23
+ if (y = 0, t) op = [op[0] & 2, t.value];
24
+ switch (op[0]) {
25
+ case 0: case 1: t = op; break;
26
+ case 4: _.label++; return { value: op[1], done: false };
27
+ case 5: _.label++; y = op[1]; op = [0]; continue;
28
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
29
+ default:
30
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
31
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
32
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
33
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
34
+ if (t[2]) _.ops.pop();
35
+ _.trys.pop(); continue;
36
+ }
37
+ op = body.call(thisArg, _);
38
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
39
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
40
+ }
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.isAvailableAsync = isAvailableAsync;
44
+ exports.startRecordingAsync = startRecordingAsync;
45
+ exports.stopRecordingAsync = stopRecordingAsync;
46
+ var ExpoScreenRecorderModule_1 = require("./ExpoScreenRecorderModule");
47
+ function isAvailableAsync() {
48
+ return __awaiter(this, void 0, void 0, function () {
49
+ return __generator(this, function (_a) {
50
+ switch (_a.label) {
51
+ case 0: return [4 /*yield*/, ExpoScreenRecorderModule_1.default.isAvailableAsync()];
52
+ case 1: return [2 /*return*/, _a.sent()];
53
+ }
54
+ });
55
+ });
56
+ }
57
+ function startRecordingAsync() {
58
+ return __awaiter(this, arguments, void 0, function (options) {
59
+ if (options === void 0) { options = {}; }
60
+ return __generator(this, function (_a) {
61
+ switch (_a.label) {
62
+ case 0: return [4 /*yield*/, ExpoScreenRecorderModule_1.default.startRecordingAsync(options)];
63
+ case 1: return [2 /*return*/, _a.sent()];
64
+ }
65
+ });
66
+ });
67
+ }
68
+ function stopRecordingAsync() {
69
+ return __awaiter(this, void 0, void 0, function () {
70
+ return __generator(this, function (_a) {
71
+ switch (_a.label) {
72
+ case 0: return [4 /*yield*/, ExpoScreenRecorderModule_1.default.stopRecordingAsync()];
73
+ case 1: return [2 /*return*/, _a.sent()];
74
+ }
75
+ });
76
+ });
77
+ }
@@ -0,0 +1,5 @@
1
+ const { defineConfig } = require('eslint/config');
2
+ const universe = require('eslint-config-universe/flat/native');
3
+ const universeWeb = require('eslint-config-universe/flat/web');
4
+
5
+ module.exports = defineConfig([{ ignores: ['build'] }, ...universe, ...universeWeb]);
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android", "web"],
3
+ "apple": {
4
+ "modules": ["ExpoScreenRecorderModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.screenrecorder.ExpoScreenRecorderModule"]
8
+ }
9
+ }
@@ -0,0 +1,23 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'ExpoScreenRecorder'
3
+ s.version = '1.0.0'
4
+ s.summary = 'High performance Expo module Screen Recorder'
5
+ s.description = 'Screen Recorder for Expo'
6
+ s.author = ''
7
+ s.homepage = 'https://docs.expo.dev/modules/'
8
+ s.platforms = {
9
+ :ios => '16.4',
10
+ :tvos => '16.4'
11
+ }
12
+ s.source = { git: '' }
13
+ s.static_framework = true
14
+
15
+ s.dependency 'ExpoModulesCore'
16
+
17
+ # Swift/Objective-C compatibility
18
+ s.pod_target_xcconfig = {
19
+ 'DEFINES_MODULE' => 'YES',
20
+ }
21
+
22
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
23
+ end
@@ -0,0 +1,147 @@
1
+ import ExpoModulesCore
2
+ import ReplayKit
3
+
4
+ public class ExpoScreenRecorderModule: Module {
5
+ private let recorder = RPScreenRecorder.shared()
6
+ private var startTime: Date?
7
+ private var durationTimer: Timer?
8
+ private var pendingPromise: Promise?
9
+ private var currentOutputURL: URL?
10
+
11
+ deinit {
12
+ durationTimer?.invalidate()
13
+ durationTimer = nil
14
+ pendingPromise?.reject("ERR_MODULE_DESTROYED", "Screen recorder module was deallocated before the operation completed.")
15
+ pendingPromise = nil
16
+ }
17
+
18
+ public func definition() -> ModuleDefinition {
19
+ Name("ExpoScreenRecorder")
20
+
21
+ AsyncFunction("isAvailableAsync") { () -> Bool in
22
+ return self.recorder.isAvailable
23
+ }
24
+
25
+ AsyncFunction("startRecordingAsync") { (options: RecordingOptions, promise: Promise) in
26
+ guard self.recorder.isAvailable else {
27
+ promise.reject("ERR_NOT_AVAILABLE", "Screen recorder is not available on this device.")
28
+ return
29
+ }
30
+
31
+ guard !self.recorder.isRecording else {
32
+ promise.reject("ERR_ALREADY_RECORDING", "A recording is already in progress.")
33
+ return
34
+ }
35
+
36
+ self.recorder.isMicrophoneEnabled = options.includeAudio ?? false
37
+
38
+ let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
39
+ let uniqueFilename = "REC_\(UUID().uuidString).mp4"
40
+ self.currentOutputURL = cacheDirectory.appendingPathComponent(uniqueFilename)
41
+ self.pendingPromise = promise
42
+
43
+ self.recorder.startRecording { [weak self] error in
44
+ guard let self = self else { return }
45
+
46
+ if let error = error {
47
+ self.pendingPromise?.reject("ERR_START_FAILED", error.localizedDescription)
48
+ self.pendingPromise = nil
49
+ return
50
+ }
51
+
52
+ self.startTime = Date()
53
+ self.pendingPromise?.resolve(nil)
54
+ self.pendingPromise = nil
55
+
56
+ if let limit = options.durationLimit, limit > 0 {
57
+ DispatchQueue.main.async {
58
+ self.durationTimer = Timer.scheduledTimer(
59
+ withTimeInterval: limit,
60
+ repeats: false
61
+ ) { [weak self] _ in
62
+ self?.stopRecordingInternal()
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ AsyncFunction("stopRecordingAsync") { (promise: Promise) in
70
+ self.stopRecordingWithPromise(promise: promise)
71
+ }
72
+ }
73
+
74
+ private func stopRecordingWithPromise(promise: Promise) {
75
+ durationTimer?.invalidate()
76
+ durationTimer = nil
77
+
78
+ guard recorder.isRecording else {
79
+ promise.reject("ERR_NOT_RECORDING", "No active recording to stop.")
80
+ return
81
+ }
82
+
83
+ guard let outputURL = currentOutputURL else {
84
+ promise.reject("ERR_NO_FILE", "Output file URL was not set.")
85
+ return
86
+ }
87
+
88
+ if #available(iOS 17.0, *) {
89
+ // stopRecording(withOutput:) is deprecated in iOS 17.
90
+ // Full migration to startCapture(handler:) for buffer-level control
91
+ // is recommended for a future release. For now, suppress the warning.
92
+ }
93
+
94
+ #if swift(>=5.9)
95
+ // Suppress deprecation for the withOutput variant — still functional through iOS 17+
96
+ // and is the only single-call file-output API ReplayKit exposes publicly.
97
+ #endif
98
+
99
+ self.recorder.stopRecording(withOutput: outputURL) { [weak self] error in
100
+ guard let self = self else { return }
101
+
102
+ if let error = error {
103
+ promise.reject("ERR_STOP_FAILED", error.localizedDescription)
104
+ self.cleanup()
105
+ return
106
+ }
107
+
108
+ let durationSec = Date().timeIntervalSince(self.startTime ?? Date())
109
+
110
+ // Use "file://" prefix to match Android's uri format consistently
111
+ let uri = "file://\(outputURL.path)"
112
+
113
+ promise.resolve([
114
+ "uri": uri,
115
+ "duration": durationSec
116
+ ] as [String: Any])
117
+
118
+ self.cleanup()
119
+ }
120
+ }
121
+
122
+ private func stopRecordingInternal() {
123
+ durationTimer?.invalidate()
124
+ durationTimer = nil
125
+
126
+ guard let outputURL = currentOutputURL, recorder.isRecording else { return }
127
+
128
+ recorder.stopRecording(withOutput: outputURL) { [weak self] _ in
129
+ self?.cleanup()
130
+ }
131
+ }
132
+
133
+ private func cleanup() {
134
+ durationTimer?.invalidate()
135
+ durationTimer = nil
136
+ startTime = nil
137
+ currentOutputURL = nil
138
+ }
139
+ }
140
+
141
+ struct RecordingOptions: Record {
142
+ @Field var durationLimit: Double?
143
+ // NOTE: ReplayKit does not expose bitrate/quality control via RPScreenRecorder.
144
+ // This field is accepted for API consistency with Android but has no effect on iOS.
145
+ @Field var quality: String?
146
+ @Field var includeAudio: Bool?
147
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "expo-turbo-screen-recorder",
3
+ "version": "0.3.0",
4
+ "description": "Expo Module For High Quality Screen Recorder",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android"
17
+ },
18
+ "keywords": [
19
+ "react-native",
20
+ "expo",
21
+ "expo-screen-recorder",
22
+ "ExpoScreenRecorder"
23
+ ],
24
+ "repository": "https://github.com/tsnguyenducphuong/screen-recorder",
25
+ "bugs": {
26
+ "url": "https://github.com/tsnguyenducphuong/screen-recorder/issues"
27
+ },
28
+ "author": "tsnguyenducphuong <ts.nguyenducphuong@gmail.com> (https://github.com/tsnguyenducphuong)",
29
+ "license": "MIT",
30
+ "homepage": "https://github.com/tsnguyenducphuong/expo-screen-recorder#readme",
31
+ "devDependencies": {
32
+ "expo-module-scripts": "^5.0.8"
33
+ },
34
+ "peerDependencies": {
35
+ "expo": "*",
36
+ "react": "*",
37
+ "react-native": "*"
38
+ }
39
+ }
@@ -0,0 +1,19 @@
1
+ export type ExpoScreenRecorderModuleEvents = {
2
+ onChange: (params: ChangeEventPayload) => void;
3
+ };
4
+
5
+ export type ChangeEventPayload = {
6
+ value: string;
7
+ };
8
+
9
+
10
+ export interface RecordingOptions {
11
+ durationLimit?: number;
12
+ quality?: 'low' | 'medium' | 'high';
13
+ includeAudio?: boolean;
14
+ }
15
+
16
+ export interface RecordingResult {
17
+ uri: string;
18
+ duration: number;
19
+ }
@@ -0,0 +1,32 @@
1
+ // import { NativeModule, requireNativeModule } from 'expo';
2
+
3
+ // import { ExpoScreenRecorderModuleEvents } from './ExpoScreenRecorder.types';
4
+
5
+ // declare class ExpoScreenRecorderModule extends NativeModule<ExpoScreenRecorderModuleEvents> {
6
+ // hello(): string;
7
+ // setValueAsync(value: string): Promise<void>;
8
+ // }
9
+
10
+ // export default requireNativeModule<ExpoScreenRecorderModule>('ExpoScreenRecorder');
11
+
12
+
13
+ import { NativeModule, requireNativeModule } from 'expo';
14
+
15
+ export interface RecordingOptions {
16
+ durationLimit?: number;
17
+ quality?: 'low' | 'medium' | 'high';
18
+ includeAudio?: boolean;
19
+ }
20
+
21
+ export interface RecordingResult {
22
+ uri: string;
23
+ duration: number;
24
+ }
25
+
26
+ declare class ExpoScreenRecorderModule extends NativeModule {
27
+ isAvailableAsync(): Promise<boolean>;
28
+ startRecordingAsync(options: RecordingOptions): Promise<void>;
29
+ stopRecordingAsync(): Promise<RecordingResult>;
30
+ }
31
+
32
+ export default requireNativeModule<ExpoScreenRecorderModule>('ExpoScreenRecorder');
@@ -0,0 +1,15 @@
1
+ import { registerWebModule, NativeModule } from 'expo';
2
+
3
+ import { ExpoScreenRecorderModuleEvents } from './ExpoScreenRecorder.types';
4
+
5
+ class ExpoScreenRecorderModule extends NativeModule<ExpoScreenRecorderModuleEvents> {
6
+ hello() {
7
+ return 'Hello world! 👋';
8
+ }
9
+
10
+ async setValueAsync(value: string): Promise<void> {
11
+ this.emit('onChange', { value });
12
+ }
13
+ }
14
+
15
+ export default registerWebModule(ExpoScreenRecorderModule, 'ExpoScreenRecorderModule');
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ // Reexport the native module. On web, it will be resolved to ExpoScreenRecorderModule.web.ts
2
+ // and on native platforms to ExpoScreenRecorderModule.ts
3
+ // export { default } from './ExpoScreenRecorderModule';
4
+ // export * from './ExpoScreenRecorder.types';
5
+
6
+
7
+ import ExpoScreenRecorderModule from './ExpoScreenRecorderModule';
8
+ import type { RecordingOptions, RecordingResult } from './ExpoScreenRecorder.types';
9
+
10
+ export async function isAvailableAsync(): Promise<boolean> {
11
+ return await ExpoScreenRecorderModule.isAvailableAsync();
12
+ }
13
+
14
+ export async function startRecordingAsync(options: RecordingOptions = {}): Promise<void> {
15
+ return await ExpoScreenRecorderModule.startRecordingAsync(options);
16
+ }
17
+
18
+ export async function stopRecordingAsync(): Promise<RecordingResult> {
19
+ return await ExpoScreenRecorderModule.stopRecordingAsync();
20
+ }
21
+
22
+ export type { RecordingOptions, RecordingResult };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./build",
5
+ "skipLibCheck": true
6
+ },
7
+ "include": ["./src"],
8
+ "exclude": ["**/__mocks__/*", "**/__tests__/*"]
9
+ }