sitepong 0.2.3 → 0.2.4

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 (57) hide show
  1. package/android/build.gradle +39 -0
  2. package/android/src/main/java/com/sitepong/deviceid/DeviceIdHelper.kt +45 -0
  3. package/android/src/main/java/com/sitepong/deviceid/DeviceIdModule.kt +45 -0
  4. package/android/src/main/java/com/sitepong/screenrecorder/ChunkUploader.kt +78 -0
  5. package/android/src/main/java/com/sitepong/screenrecorder/H264Encoder.kt +163 -0
  6. package/android/src/main/java/com/sitepong/screenrecorder/ScreenCapture.kt +103 -0
  7. package/android/src/main/java/com/sitepong/screenrecorder/ScreenRecorderModule.kt +80 -0
  8. package/android/src/main/java/com/sitepong/screenrecorder/SensitiveViewManager.kt +19 -0
  9. package/app.plugin.js +426 -0
  10. package/dist/cdn/sitepong.min.js +5 -5
  11. package/dist/cdn/sitepong.min.js.map +1 -1
  12. package/dist/entries/rn.d.ts +188 -16
  13. package/dist/entries/rn.js +188 -35
  14. package/dist/entries/rn.js.map +1 -1
  15. package/dist/entries/web.js +2 -1
  16. package/dist/entries/web.js.map +1 -1
  17. package/dist/entries/web.mjs +2 -1
  18. package/dist/entries/web.mjs.map +1 -1
  19. package/dist/index.d.mts +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +2 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/react/index.js +2 -1
  26. package/dist/react/index.js.map +1 -1
  27. package/dist/react/index.mjs +2 -1
  28. package/dist/react/index.mjs.map +1 -1
  29. package/expo-module.config.json +16 -0
  30. package/ios/DeviceId/DeviceIdModule.swift +55 -0
  31. package/ios/DeviceId/KeychainHelper.swift +68 -0
  32. package/ios/LiveActivity/ConfigStore.swift +43 -0
  33. package/ios/LiveActivity/DSLNode.swift +188 -0
  34. package/ios/LiveActivity/SitePongActivityAttributes.swift +112 -0
  35. package/ios/LiveActivity/SitePongLiveActivityModule.swift +250 -0
  36. package/ios/LiveActivity/widget/ColorHex.swift +30 -0
  37. package/ios/LiveActivity/widget/DSLAnimations.swift +128 -0
  38. package/ios/LiveActivity/widget/DSLRenderer.swift +538 -0
  39. package/ios/LiveActivity/widget/SitePongLiveActivityWidget.swift +393 -0
  40. package/ios/LiveActivity/widget/SitePongWidgetBundle.swift +84 -0
  41. package/ios/ScreenRecorder/ChunkUploader.swift +52 -0
  42. package/ios/ScreenRecorder/H264Encoder.swift +229 -0
  43. package/ios/ScreenRecorder/ScreenCapture.swift +149 -0
  44. package/ios/ScreenRecorder/ScreenRecorderModule.swift +179 -0
  45. package/ios/ScreenRecorder/SensitiveViewManager.swift +63 -0
  46. package/ios/Sitepong.podspec +57 -0
  47. package/ios/WatchtowerCore/Screenshotter.swift +163 -0
  48. package/ios/WatchtowerCore/SwiftUIModifiers.swift +78 -0
  49. package/ios/WatchtowerCore/Swizzling.swift +59 -0
  50. package/ios/WatchtowerCore/TapEvent.swift +61 -0
  51. package/ios/WatchtowerCore/Transport.swift +136 -0
  52. package/ios/WatchtowerCore/UIViewExtensions.swift +31 -0
  53. package/ios/WatchtowerCore/Watchtower.swift +80 -0
  54. package/ios/WatchtowerCore/WatchtowerEngine.swift +565 -0
  55. package/ios/WatchtowerCore/WatchtowerHash.swift +120 -0
  56. package/ios/WatchtowerCore/WatchtowerRegistry.swift +87 -0
  57. package/package.json +17 -10
@@ -0,0 +1,39 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'kotlin-android'
3
+
4
+ // One Android library for the whole SitePong SDK. It hosts every native Expo
5
+ // module that used to ship as its own package — screen recorder + device id.
6
+ // The Kotlin sources keep their original package names
7
+ // (com.sitepong.screenrecorder.*, com.sitepong.deviceid.*); the gradle
8
+ // `namespace` below is just the library's own R/manifest package and is
9
+ // independent of them.
10
+ group = 'com.sitepong.sdk'
11
+ version = '0.2.3'
12
+
13
+ android {
14
+ namespace "com.sitepong.sdk"
15
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
16
+
17
+ defaultConfig {
18
+ minSdkVersion safeExtGet("minSdkVersion", 26)
19
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
20
+ }
21
+
22
+ compileOptions {
23
+ sourceCompatibility JavaVersion.VERSION_17
24
+ targetCompatibility JavaVersion.VERSION_17
25
+ }
26
+
27
+ kotlinOptions {
28
+ jvmTarget = "17"
29
+ }
30
+ }
31
+
32
+ dependencies {
33
+ implementation project(':expo-modules-core')
34
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${safeExtGet('kotlinVersion', '1.9.0')}"
35
+ }
36
+
37
+ def safeExtGet(prop, fallback) {
38
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
39
+ }
@@ -0,0 +1,45 @@
1
+ package com.sitepong.deviceid
2
+
3
+ import android.content.Context
4
+ import android.provider.Settings
5
+ import java.security.MessageDigest
6
+ import java.util.UUID
7
+
8
+ /**
9
+ * Manages a persistent device identifier on Android.
10
+ *
11
+ * Combines two signals:
12
+ * - ANDROID_ID: Stable per app signing key since Android 8. Survives reinstalls.
13
+ * - SharedPreferences UUID: Persists via Auto Backup (if user has same Google account).
14
+ *
15
+ * The final device ID is SHA-256(androidId + storedUuid) for a composite stable ID.
16
+ */
17
+ object DeviceIdHelper {
18
+ private const val PREFS_NAME = "sitepong_device"
19
+ private const val KEY_UUID = "persistent_uuid"
20
+
21
+ fun getOrCreateDeviceId(context: Context): String {
22
+ val androidId = getAndroidId(context)
23
+ val storedUuid = getOrCreateUuid(context)
24
+ return sha256("$androidId$storedUuid")
25
+ }
26
+
27
+ fun getAndroidId(context: Context): String {
28
+ return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: ""
29
+ }
30
+
31
+ private fun getOrCreateUuid(context: Context): String {
32
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
33
+ val existing = prefs.getString(KEY_UUID, null)
34
+ if (existing != null) return existing
35
+
36
+ val uuid = UUID.randomUUID().toString()
37
+ prefs.edit().putString(KEY_UUID, uuid).apply()
38
+ return uuid
39
+ }
40
+
41
+ private fun sha256(input: String): String {
42
+ val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
43
+ return bytes.joinToString("") { "%02x".format(it) }
44
+ }
45
+ }
@@ -0,0 +1,45 @@
1
+ package com.sitepong.deviceid
2
+
3
+ import android.os.Build
4
+ import expo.modules.kotlin.modules.Module
5
+ import expo.modules.kotlin.modules.ModuleDefinition
6
+
7
+ class DeviceIdModule : Module() {
8
+ override fun definition() = ModuleDefinition {
9
+ Name("SitePongDeviceId")
10
+
11
+ AsyncFunction("getDeviceId") {
12
+ val context = appContext.reactContext ?: throw Exception("React context not available")
13
+ DeviceIdHelper.getOrCreateDeviceId(context)
14
+ }
15
+
16
+ AsyncFunction("getDeviceSignals") {
17
+ val context = appContext.reactContext ?: throw Exception("React context not available")
18
+ val deviceId = DeviceIdHelper.getOrCreateDeviceId(context)
19
+ val androidId = DeviceIdHelper.getAndroidId(context)
20
+ val density = context.resources.displayMetrics.density
21
+
22
+ mapOf(
23
+ "deviceId" to deviceId,
24
+ "platform" to "android",
25
+ "osVersion" to Build.VERSION.RELEASE,
26
+ "model" to Build.MODEL,
27
+ "manufacturer" to Build.MANUFACTURER,
28
+ "isEmulator" to isEmulator(),
29
+ "screenScale" to density.toDouble(),
30
+ "androidId" to androidId
31
+ )
32
+ }
33
+ }
34
+
35
+ private fun isEmulator(): Boolean {
36
+ return (Build.FINGERPRINT.startsWith("generic")
37
+ || Build.FINGERPRINT.startsWith("unknown")
38
+ || Build.MODEL.contains("google_sdk")
39
+ || Build.MODEL.contains("Emulator")
40
+ || Build.MODEL.contains("Android SDK built for x86")
41
+ || Build.MANUFACTURER.contains("Genymotion")
42
+ || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
43
+ || "google_sdk" == Build.PRODUCT)
44
+ }
45
+ }
@@ -0,0 +1,78 @@
1
+ package com.sitepong.screenrecorder
2
+
3
+ import java.io.OutputStream
4
+ import java.net.HttpURLConnection
5
+ import java.net.URL
6
+ import java.util.concurrent.Executors
7
+ import java.util.concurrent.locks.ReentrantLock
8
+
9
+ class ChunkUploader(
10
+ private val endpoint: String,
11
+ private val apiKey: String,
12
+ private val sessionId: String
13
+ ) {
14
+ private var chunkSequence = 0
15
+ private val buffer = mutableListOf<ByteArray>()
16
+ private val lock = ReentrantLock()
17
+ private val executor = Executors.newSingleThreadExecutor()
18
+
19
+ fun bufferNALUnit(data: ByteArray, isKeyframe: Boolean) {
20
+ lock.lock()
21
+ try {
22
+ buffer.add(data)
23
+ } finally {
24
+ lock.unlock()
25
+ }
26
+ }
27
+
28
+ fun flushBuffer() {
29
+ lock.lock()
30
+ val dataChunks: List<ByteArray>
31
+ try {
32
+ if (buffer.isEmpty()) return
33
+ dataChunks = buffer.toList()
34
+ buffer.clear()
35
+ } finally {
36
+ lock.unlock()
37
+ }
38
+
39
+ val combined = dataChunks.fold(ByteArray(0)) { acc, bytes -> acc + bytes }
40
+ val seq = chunkSequence++
41
+
42
+ executor.submit {
43
+ uploadWithRetry(combined, seq)
44
+ }
45
+ }
46
+
47
+ private fun uploadWithRetry(data: ByteArray, sequence: Int) {
48
+ for (attempt in 0..1) {
49
+ try {
50
+ val url = URL("$endpoint/api/replays/video")
51
+ val conn = url.openConnection() as HttpURLConnection
52
+ conn.requestMethod = "POST"
53
+ conn.setRequestProperty("Content-Type", "application/octet-stream")
54
+ conn.setRequestProperty("X-API-Key", apiKey)
55
+ conn.setRequestProperty("X-Session-ID", sessionId)
56
+ conn.setRequestProperty("X-Chunk-Sequence", sequence.toString())
57
+ conn.setRequestProperty("X-Trigger", "error")
58
+ conn.connectTimeout = 10_000
59
+ conn.readTimeout = 10_000
60
+ conn.doOutput = true
61
+
62
+ conn.outputStream.use { out: OutputStream ->
63
+ out.write(data)
64
+ }
65
+
66
+ val responseCode = conn.responseCode
67
+ conn.disconnect()
68
+
69
+ if (responseCode in 200..299) return
70
+ if (attempt == 0) continue // retry
71
+ } catch (e: Exception) {
72
+ if (attempt == 1) {
73
+ // Drop the chunk — don't block recording
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,163 @@
1
+ package com.sitepong.screenrecorder
2
+
3
+ import android.app.Activity
4
+ import android.graphics.Bitmap
5
+ import android.media.MediaCodec
6
+ import android.media.MediaCodecInfo
7
+ import android.media.MediaFormat
8
+ import android.os.Bundle
9
+ import java.nio.ByteBuffer
10
+ import java.util.concurrent.ConcurrentLinkedQueue
11
+ import java.util.concurrent.locks.ReentrantLock
12
+
13
+ class H264Encoder(
14
+ private val bitrate: Int,
15
+ private val bufferDurationMs: Int,
16
+ private val onEncodedChunk: (ByteArray, Boolean) -> Unit
17
+ ) {
18
+ private var codec: MediaCodec? = null
19
+ private var inputSurface: android.view.Surface? = null
20
+ private var width = 0
21
+ private var height = 0
22
+ private var started = false
23
+ private var frameCount = 0L
24
+ private val startTimeNs = System.nanoTime()
25
+
26
+ data class NALEntry(val data: ByteArray, val isKeyframe: Boolean, val timestampMs: Long)
27
+
28
+ private val nalBuffer = ConcurrentLinkedQueue<NALEntry>()
29
+ private val bufferLock = ReentrantLock()
30
+
31
+ fun start(activity: Activity) {
32
+ val decorView = activity.window?.decorView ?: return
33
+ width = decorView.width
34
+ height = decorView.height
35
+ if (width <= 0 || height <= 0) return
36
+
37
+ // Ensure dimensions are even (required by MediaCodec)
38
+ width = width and 0x7FFFFFFE.toInt()
39
+ height = height and 0x7FFFFFFE.toInt()
40
+
41
+ val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply {
42
+ setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
43
+ setInteger(MediaFormat.KEY_FRAME_RATE, 3)
44
+ setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
45
+ setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
46
+ setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)
47
+ }
48
+
49
+ codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
50
+ codec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
51
+
52
+ codec?.setCallback(object : MediaCodec.Callback() {
53
+ override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
54
+ // Using surface input mode — this won't be called
55
+ }
56
+
57
+ override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
58
+ val buffer = codec.getOutputBuffer(index) ?: return
59
+ if (info.size > 0 && info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG == 0) {
60
+ val data = ByteArray(info.size)
61
+ buffer.get(data)
62
+ val isKeyframe = info.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0
63
+ val timestampMs = System.currentTimeMillis()
64
+
65
+ val entry = NALEntry(data, isKeyframe, timestampMs)
66
+ nalBuffer.add(entry)
67
+ trimBuffer()
68
+
69
+ onEncodedChunk(data, isKeyframe)
70
+ }
71
+ codec.releaseOutputBuffer(index, false)
72
+ }
73
+
74
+ override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
75
+ // Log but don't crash
76
+ }
77
+
78
+ override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
79
+ // SPS/PPS available in format — extract for first chunk
80
+ val sps = format.getByteBuffer("csd-0")
81
+ val pps = format.getByteBuffer("csd-1")
82
+ if (sps != null && pps != null) {
83
+ val spsData = ByteArray(sps.remaining())
84
+ sps.get(spsData)
85
+ val ppsData = ByteArray(pps.remaining())
86
+ pps.get(ppsData)
87
+ val combined = spsData + ppsData
88
+ nalBuffer.add(NALEntry(combined, true, System.currentTimeMillis()))
89
+ }
90
+ }
91
+ })
92
+
93
+ inputSurface = codec?.createInputSurface()
94
+ codec?.start()
95
+ started = true
96
+ }
97
+
98
+ fun encode(bitmap: Bitmap) {
99
+ if (!started) return
100
+ val surface = inputSurface ?: return
101
+
102
+ try {
103
+ val canvas = surface.lockHardwareCanvas()
104
+ canvas.drawBitmap(bitmap, 0f, 0f, null)
105
+ surface.unlockCanvasAndPost(canvas)
106
+ } catch (e: Exception) {
107
+ // Skip frame on error
108
+ }
109
+ }
110
+
111
+ fun flushPendingFrames() {
112
+ // Request a sync frame to ensure clean output
113
+ if (started) {
114
+ try {
115
+ val params = Bundle()
116
+ params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
117
+ codec?.setParameters(params)
118
+ } catch (e: Exception) {
119
+ // Ignore
120
+ }
121
+ }
122
+ }
123
+
124
+ fun stop() {
125
+ started = false
126
+ try {
127
+ codec?.stop()
128
+ codec?.release()
129
+ } catch (e: Exception) {
130
+ // Ignore
131
+ }
132
+ codec = null
133
+ inputSurface?.release()
134
+ inputSurface = null
135
+ nalBuffer.clear()
136
+ }
137
+
138
+ fun drainBuffer(): ByteArray {
139
+ bufferLock.lock()
140
+ try {
141
+ var combined = ByteArray(0)
142
+ while (nalBuffer.isNotEmpty()) {
143
+ val entry = nalBuffer.poll() ?: break
144
+ combined += entry.data
145
+ }
146
+ return combined
147
+ } finally {
148
+ bufferLock.unlock()
149
+ }
150
+ }
151
+
152
+ private fun trimBuffer() {
153
+ val cutoffMs = System.currentTimeMillis() - bufferDurationMs
154
+ while (nalBuffer.isNotEmpty()) {
155
+ val head = nalBuffer.peek() ?: break
156
+ if (head.timestampMs < cutoffMs) {
157
+ nalBuffer.poll()
158
+ } else {
159
+ break
160
+ }
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,103 @@
1
+ package com.sitepong.screenrecorder
2
+
3
+ import android.app.Activity
4
+ import android.graphics.Bitmap
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Rect
8
+ import android.os.Handler
9
+ import android.os.Looper
10
+ import android.view.PixelCopy
11
+ import android.view.View
12
+
13
+ class ScreenCapture(
14
+ private val activity: Activity,
15
+ private val fps: Int,
16
+ private val sensitiveViewManager: SensitiveViewManager,
17
+ private val onFrame: (Bitmap) -> Unit
18
+ ) {
19
+ private val handler = Handler(Looper.getMainLooper())
20
+ private var running = false
21
+ private val intervalMs = (1000L / fps.coerceIn(1, 10))
22
+
23
+ private val captureRunnable = object : Runnable {
24
+ override fun run() {
25
+ if (!running) return
26
+ captureFrame()
27
+ handler.postDelayed(this, intervalMs)
28
+ }
29
+ }
30
+
31
+ fun start() {
32
+ if (running) return
33
+ running = true
34
+ handler.postDelayed(captureRunnable, intervalMs)
35
+ }
36
+
37
+ fun stop() {
38
+ running = false
39
+ handler.removeCallbacks(captureRunnable)
40
+ }
41
+
42
+ private fun captureFrame() {
43
+ val window = activity.window ?: return
44
+ val decorView = window.decorView
45
+ val width = decorView.width
46
+ val height = decorView.height
47
+ if (width <= 0 || height <= 0) return
48
+
49
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
50
+
51
+ // Try PixelCopy (API 26+)
52
+ try {
53
+ val copyHandler = Handler(Looper.getMainLooper())
54
+ PixelCopy.request(window, bitmap, { result ->
55
+ if (result == PixelCopy.SUCCESS) {
56
+ // Apply sensitive view masks
57
+ applySensitiveMasks(bitmap, decorView)
58
+ onFrame(bitmap)
59
+ } else {
60
+ // Fallback to canvas draw
61
+ captureViaCanvas(decorView, bitmap)
62
+ }
63
+ }, copyHandler)
64
+ } catch (e: Exception) {
65
+ captureViaCanvas(decorView, bitmap)
66
+ }
67
+ }
68
+
69
+ private fun captureViaCanvas(view: View, bitmap: Bitmap) {
70
+ try {
71
+ val canvas = Canvas(bitmap)
72
+ view.draw(canvas)
73
+ applySensitiveMasks(bitmap, view)
74
+ onFrame(bitmap)
75
+ } catch (e: Exception) {
76
+ // Skip this frame
77
+ }
78
+ }
79
+
80
+ private fun applySensitiveMasks(bitmap: Bitmap, rootView: View) {
81
+ val tags = sensitiveViewManager.getViewTags()
82
+ if (tags.isEmpty()) return
83
+
84
+ val canvas = Canvas(bitmap)
85
+ val paint = android.graphics.Paint().apply {
86
+ color = Color.BLACK
87
+ style = android.graphics.Paint.Style.FILL
88
+ }
89
+
90
+ for (tag in tags) {
91
+ val sensitiveView = rootView.findViewWithTag<View>(tag) ?: continue
92
+ val location = IntArray(2)
93
+ sensitiveView.getLocationInWindow(location)
94
+ val rect = Rect(
95
+ location[0],
96
+ location[1],
97
+ location[0] + sensitiveView.width,
98
+ location[1] + sensitiveView.height
99
+ )
100
+ canvas.drawRect(rect, paint)
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,80 @@
1
+ package com.sitepong.screenrecorder
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+ import expo.modules.kotlin.Promise
6
+
7
+ class ScreenRecorderModule : Module() {
8
+ private var screenCapture: ScreenCapture? = null
9
+ private var encoder: H264Encoder? = null
10
+ private var uploader: ChunkUploader? = null
11
+ private val sensitiveViewManager = SensitiveViewManager()
12
+ private var recording = false
13
+
14
+ override fun definition() = ModuleDefinition {
15
+ Name("SitePongScreenRecorder")
16
+
17
+ Function("isRecording") {
18
+ recording
19
+ }
20
+
21
+ AsyncFunction("startRecording") { options: Map<String, Any?> ->
22
+ if (recording) return@AsyncFunction
23
+
24
+ val apiKey = options["apiKey"] as? String ?: ""
25
+ val endpoint = options["endpoint"] as? String ?: "https://ingest.sitepong.com"
26
+ val sessionId = options["sessionId"] as? String ?: ""
27
+ val fps = (options["fps"] as? Double)?.toInt() ?: 3
28
+ val quality = options["quality"] as? String ?: "standard"
29
+ val bufferDuration = (options["bufferDuration"] as? Double)?.toInt() ?: 20_000
30
+
31
+ val bitrate = when (quality) {
32
+ "low" -> 200_000
33
+ "high" -> 800_000
34
+ else -> 400_000
35
+ }
36
+
37
+ uploader = ChunkUploader(endpoint, apiKey, sessionId)
38
+
39
+ encoder = H264Encoder(bitrate, bufferDuration) { data, isKeyframe ->
40
+ uploader?.bufferNALUnit(data, isKeyframe)
41
+ }
42
+
43
+ val activity = appContext.currentActivity ?: return@AsyncFunction
44
+ screenCapture = ScreenCapture(activity, fps, sensitiveViewManager) { bitmap ->
45
+ encoder?.encode(bitmap)
46
+ }
47
+
48
+ encoder?.start(activity)
49
+ screenCapture?.start()
50
+ recording = true
51
+ }
52
+
53
+ AsyncFunction("stopRecording") {
54
+ stopInternal()
55
+ }
56
+
57
+ AsyncFunction("flushBuffer") {
58
+ if (!recording) return@AsyncFunction
59
+ encoder?.flushPendingFrames()
60
+ uploader?.flushBuffer()
61
+ }
62
+
63
+ Function("addSensitiveView") { viewTag: Int ->
64
+ sensitiveViewManager.addView(viewTag)
65
+ }
66
+
67
+ Function("removeSensitiveView") { viewTag: Int ->
68
+ sensitiveViewManager.removeView(viewTag)
69
+ }
70
+ }
71
+
72
+ private fun stopInternal() {
73
+ screenCapture?.stop()
74
+ encoder?.stop()
75
+ screenCapture = null
76
+ encoder = null
77
+ uploader = null
78
+ recording = false
79
+ }
80
+ }
@@ -0,0 +1,19 @@
1
+ package com.sitepong.screenrecorder
2
+
3
+ import java.util.concurrent.CopyOnWriteArraySet
4
+
5
+ class SensitiveViewManager {
6
+ private val viewTags = CopyOnWriteArraySet<Int>()
7
+
8
+ fun addView(tag: Int) {
9
+ viewTags.add(tag)
10
+ }
11
+
12
+ fun removeView(tag: Int) {
13
+ viewTags.remove(tag)
14
+ }
15
+
16
+ fun getViewTags(): Set<Int> {
17
+ return viewTags.toSet()
18
+ }
19
+ }