@desklink/react-native 0.1.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/LICENSE +202 -0
- package/NOTICE +18 -0
- package/README.md +131 -0
- package/android/build.gradle +29 -0
- package/android/gradle.properties +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/expo/modules/desklink/DesklinkModule.kt +199 -0
- package/android/src/main/java/expo/modules/desklink/DesktopRenderer.kt +284 -0
- package/android/src/main/java/expo/modules/desklink/DesktopSession.kt +478 -0
- package/android/src/main/java/expo/modules/desklink/DesktopView.kt +1019 -0
- package/android/src/main/java/expo/modules/desklink/ImeDeletion.kt +20 -0
- package/expo-module.config.json +6 -0
- package/index.ts +1 -0
- package/package.json +33 -0
- package/src/DesktopView.tsx +61 -0
- package/src/DesktopView.web.tsx +56 -0
- package/src/availability.ts +5 -0
- package/src/availability.web.ts +2 -0
- package/src/index.ts +21 -0
- package/src/native.ts +65 -0
- package/src/native.web.ts +1164 -0
- package/src/protocol.ts +151 -0
- package/src/stickyModifiers.ts +66 -0
- package/src/useDesktopSession.ts +661 -0
- package/src/webKeyboardMotion.ts +57 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
package expo.modules.desklink
|
|
2
|
+
|
|
3
|
+
import android.opengl.GLES20
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.HandlerThread
|
|
6
|
+
import android.os.SystemClock
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.SurfaceHolder
|
|
9
|
+
import org.webrtc.EglBase
|
|
10
|
+
import org.webrtc.GlRectDrawer
|
|
11
|
+
import org.webrtc.GlShader
|
|
12
|
+
import org.webrtc.GlTextureFrameBuffer
|
|
13
|
+
import org.webrtc.GlUtil
|
|
14
|
+
import org.webrtc.ThreadUtils
|
|
15
|
+
import org.webrtc.VideoFrame
|
|
16
|
+
import org.webrtc.VideoFrameDrawer
|
|
17
|
+
import org.webrtc.VideoSink
|
|
18
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
19
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
20
|
+
|
|
21
|
+
private const val TAG = "DesklinkRenderer"
|
|
22
|
+
|
|
23
|
+
/** How long the first picture takes to come up out of black. */
|
|
24
|
+
private const val REVEAL_MS = 280f
|
|
25
|
+
|
|
26
|
+
/** A redraw while the first picture is still coming up: one display frame. */
|
|
27
|
+
private const val REVEAL_STEP_MS = 16L
|
|
28
|
+
|
|
29
|
+
private const val VERTEX_SHADER = """
|
|
30
|
+
attribute vec2 position;
|
|
31
|
+
attribute vec2 texCoord;
|
|
32
|
+
varying vec2 tc;
|
|
33
|
+
void main() {
|
|
34
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
35
|
+
tc = texCoord;
|
|
36
|
+
}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Nine taps across the screen pixel's footprint when the picture is shown
|
|
41
|
+
* smaller than its own size, one when it is shown at or above it. A 4K desktop
|
|
42
|
+
* fitted to a phone is a 3–4x reduction, which a single bilinear tap aliases
|
|
43
|
+
* into shimmering, broken text; averaging the footprint is what keeps it
|
|
44
|
+
* legible. `highp` where the GPU has it, because a 4K texture needs more
|
|
45
|
+
* precision than `mediump` gives a texture coordinate.
|
|
46
|
+
*/
|
|
47
|
+
private const val FRAGMENT_SHADER = """
|
|
48
|
+
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
|
49
|
+
precision highp float;
|
|
50
|
+
#else
|
|
51
|
+
precision mediump float;
|
|
52
|
+
#endif
|
|
53
|
+
varying vec2 tc;
|
|
54
|
+
uniform sampler2D picture;
|
|
55
|
+
uniform vec2 stepX;
|
|
56
|
+
uniform vec2 stepY;
|
|
57
|
+
uniform float reveal;
|
|
58
|
+
void main() {
|
|
59
|
+
vec4 sum = vec4(0.0);
|
|
60
|
+
for (int i = -1; i <= 1; i++) {
|
|
61
|
+
for (int j = -1; j <= 1; j++) {
|
|
62
|
+
sum += texture2D(picture, tc + float(i) * stepX + float(j) * stepY);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
gl_FragColor = vec4(sum.rgb / 9.0 * reveal, 1.0);
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Draws the desktop into a surface at any zoom and position.
|
|
71
|
+
*
|
|
72
|
+
* Each decoded frame is copied once into a texture this renderer owns, and the
|
|
73
|
+
* decoder's buffer is released at once: the decoder has only a few output
|
|
74
|
+
* buffers, and a renderer that held one to redraw it would stall the stream.
|
|
75
|
+
* The copy is what a pinch or a pan redraws, instantly and without waiting for
|
|
76
|
+
* the desktop to change.
|
|
77
|
+
*
|
|
78
|
+
* All GL work runs on one render thread, and only while the view has a
|
|
79
|
+
* surface: frames that arrive without one are dropped, and the stream's next
|
|
80
|
+
* frame follows within a second. The transform is set from the UI thread and
|
|
81
|
+
* read here; nothing is drawn until the view has placed the picture.
|
|
82
|
+
*/
|
|
83
|
+
internal class DesktopRenderer(
|
|
84
|
+
private val shared: EglBase.Context,
|
|
85
|
+
private val onFirstFrame: () -> Unit,
|
|
86
|
+
private val onFrameSize: (width: Int, height: Int) -> Unit,
|
|
87
|
+
) : VideoSink, SurfaceHolder.Callback {
|
|
88
|
+
private val thread = HandlerThread("desklink-render").apply { start() }
|
|
89
|
+
private val handler = Handler(thread.looper)
|
|
90
|
+
private val pending = AtomicReference<VideoFrame?>(null)
|
|
91
|
+
private val drawQueued = AtomicBoolean(false)
|
|
92
|
+
private val released = AtomicBoolean(false)
|
|
93
|
+
|
|
94
|
+
// Render thread only.
|
|
95
|
+
private var egl: EglBase? = null
|
|
96
|
+
private var windowSurface = false
|
|
97
|
+
private var picture: GlTextureFrameBuffer? = null
|
|
98
|
+
private var frameDrawer: VideoFrameDrawer? = null
|
|
99
|
+
private var copyDrawer: GlRectDrawer? = null
|
|
100
|
+
private var shader: GlShader? = null
|
|
101
|
+
private var presented = false
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* When the first picture was presented. It comes up out of black from
|
|
105
|
+
* there: the app's start state sits over this surface, and a surface cannot
|
|
106
|
+
* be faded from above, so the crossfade is drawn here.
|
|
107
|
+
*/
|
|
108
|
+
private var revealedAt = 0L
|
|
109
|
+
|
|
110
|
+
// Surface pixels per picture pixel, and where the picture's top-left sits on
|
|
111
|
+
// the surface; null until the view has placed it. Written by the UI thread.
|
|
112
|
+
@Volatile private var transform: FloatArray? = null
|
|
113
|
+
|
|
114
|
+
init {
|
|
115
|
+
handler.post { context() }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The render thread's GL context, created on first use. */
|
|
119
|
+
private fun context(): EglBase? {
|
|
120
|
+
egl?.let { return it }
|
|
121
|
+
return runCatching {
|
|
122
|
+
EglBase.create(shared, EglBase.CONFIG_PLAIN).also {
|
|
123
|
+
egl = it
|
|
124
|
+
frameDrawer = VideoFrameDrawer()
|
|
125
|
+
copyDrawer = GlRectDrawer()
|
|
126
|
+
}
|
|
127
|
+
}.onFailure { Log.w(TAG, "no GL context for the desktop picture", it) }.getOrNull()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Where the picture goes, in surface pixels; redraws what is already there. */
|
|
131
|
+
fun setTransform(scale: Float, originX: Float, originY: Float) {
|
|
132
|
+
transform = floatArrayOf(scale, originX, originY)
|
|
133
|
+
requestDraw()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun onFrame(frame: VideoFrame) {
|
|
137
|
+
if (released.get()) return
|
|
138
|
+
frame.retain()
|
|
139
|
+
pending.getAndSet(frame)?.release()
|
|
140
|
+
handler.post { copyPending() }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override fun surfaceCreated(holder: SurfaceHolder) {
|
|
144
|
+
ThreadUtils.invokeAtFrontUninterruptibly(handler) {
|
|
145
|
+
val base = context() ?: return@invokeAtFrontUninterruptibly
|
|
146
|
+
runCatching {
|
|
147
|
+
base.createSurface(holder.surface)
|
|
148
|
+
base.makeCurrent()
|
|
149
|
+
windowSurface = true
|
|
150
|
+
}.onFailure { Log.w(TAG, "could not draw into the view's surface", it) }
|
|
151
|
+
}
|
|
152
|
+
requestDraw()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
156
|
+
requestDraw()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
160
|
+
// The surface is gone when this returns, so the render thread must have
|
|
161
|
+
// let go of it first.
|
|
162
|
+
ThreadUtils.invokeAtFrontUninterruptibly(handler) {
|
|
163
|
+
val base = egl ?: return@invokeAtFrontUninterruptibly
|
|
164
|
+
if (!windowSurface) return@invokeAtFrontUninterruptibly
|
|
165
|
+
windowSurface = false
|
|
166
|
+
// The context, and the picture copied into it, outlive the surface.
|
|
167
|
+
runCatching { base.detachCurrent() }
|
|
168
|
+
base.releaseSurface()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fun release() {
|
|
173
|
+
if (!released.compareAndSet(false, true)) return
|
|
174
|
+
handler.post {
|
|
175
|
+
pending.getAndSet(null)?.release()
|
|
176
|
+
// GL objects die with their context; releasing them first is only tidy.
|
|
177
|
+
runCatching {
|
|
178
|
+
picture?.release()
|
|
179
|
+
shader?.release()
|
|
180
|
+
copyDrawer?.release()
|
|
181
|
+
frameDrawer?.release()
|
|
182
|
+
}
|
|
183
|
+
runCatching { egl?.release() }
|
|
184
|
+
picture = null
|
|
185
|
+
shader = null
|
|
186
|
+
copyDrawer = null
|
|
187
|
+
frameDrawer = null
|
|
188
|
+
egl = null
|
|
189
|
+
thread.quitSafely()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private fun requestDraw() {
|
|
194
|
+
if (released.get()) return
|
|
195
|
+
if (drawQueued.compareAndSet(false, true)) {
|
|
196
|
+
handler.post {
|
|
197
|
+
drawQueued.set(false)
|
|
198
|
+
draw()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private fun copyPending() {
|
|
204
|
+
val frame = pending.getAndSet(null) ?: return
|
|
205
|
+
try {
|
|
206
|
+
if (!windowSurface) return
|
|
207
|
+
val drawer = frameDrawer ?: return
|
|
208
|
+
val rect = copyDrawer ?: return
|
|
209
|
+
val width = frame.rotatedWidth
|
|
210
|
+
val height = frame.rotatedHeight
|
|
211
|
+
val target = picture ?: GlTextureFrameBuffer(GLES20.GL_RGBA).also { picture = it }
|
|
212
|
+
if (target.width != width || target.height != height) {
|
|
213
|
+
target.setSize(width, height)
|
|
214
|
+
onFrameSize(width, height)
|
|
215
|
+
}
|
|
216
|
+
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, target.frameBufferId)
|
|
217
|
+
drawer.drawFrame(frame, rect, null, 0, 0, width, height)
|
|
218
|
+
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0)
|
|
219
|
+
} catch (error: RuntimeException) {
|
|
220
|
+
Log.w(TAG, "could not copy a frame", error)
|
|
221
|
+
return
|
|
222
|
+
} finally {
|
|
223
|
+
frame.release()
|
|
224
|
+
}
|
|
225
|
+
draw()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private fun draw() {
|
|
229
|
+
try {
|
|
230
|
+
drawPicture()
|
|
231
|
+
} catch (error: RuntimeException) {
|
|
232
|
+
// A GL failure costs this frame, not the app.
|
|
233
|
+
Log.w(TAG, "could not draw the desktop", error)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun drawPicture() {
|
|
238
|
+
val base = egl ?: return
|
|
239
|
+
val source = picture ?: return
|
|
240
|
+
val (scale, originX, originY) = transform ?: return
|
|
241
|
+
if (!windowSurface || source.width == 0) return
|
|
242
|
+
val width = base.surfaceWidth()
|
|
243
|
+
val height = base.surfaceHeight()
|
|
244
|
+
if (width <= 0 || height <= 0) return
|
|
245
|
+
val program = shader ?: GlShader(VERTEX_SHADER, FRAGMENT_SHADER).also { shader = it }
|
|
246
|
+
val left = originX / width * 2f - 1f
|
|
247
|
+
val right = (originX + source.width * scale) / width * 2f - 1f
|
|
248
|
+
val top = 1f - originY / height * 2f
|
|
249
|
+
val bottom = 1f - (originY + source.height * scale) / height * 2f
|
|
250
|
+
|
|
251
|
+
// One screen pixel covers 1/scale picture pixels; spread the taps over it.
|
|
252
|
+
val footprint = 1f / scale
|
|
253
|
+
val spread = if (footprint > 1f) footprint / 3f else 0f
|
|
254
|
+
|
|
255
|
+
GLES20.glViewport(0, 0, width, height)
|
|
256
|
+
GLES20.glClearColor(0f, 0f, 0f, 1f)
|
|
257
|
+
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
|
258
|
+
program.useProgram()
|
|
259
|
+
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
|
260
|
+
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, source.textureId)
|
|
261
|
+
GLES20.glUniform1i(program.getUniformLocation("picture"), 0)
|
|
262
|
+
GLES20.glUniform2f(program.getUniformLocation("stepX"), spread / source.width, 0f)
|
|
263
|
+
GLES20.glUniform2f(program.getUniformLocation("stepY"), 0f, spread / source.height)
|
|
264
|
+
val reveal = if (revealedAt == 0L) 0f else ((SystemClock.uptimeMillis() - revealedAt) / REVEAL_MS).coerceIn(0f, 1f)
|
|
265
|
+
GLES20.glUniform1f(program.getUniformLocation("reveal"), reveal)
|
|
266
|
+
program.setVertexAttribArray("position", 2, GlUtil.createFloatBuffer(floatArrayOf(
|
|
267
|
+
left, bottom, right, bottom, left, top, right, top,
|
|
268
|
+
)))
|
|
269
|
+
// The copy holds the picture upright in GL's bottom-up rows.
|
|
270
|
+
program.setVertexAttribArray("texCoord", 2, GlUtil.createFloatBuffer(floatArrayOf(
|
|
271
|
+
0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f,
|
|
272
|
+
)))
|
|
273
|
+
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
|
|
274
|
+
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
|
|
275
|
+
base.swapBuffers()
|
|
276
|
+
|
|
277
|
+
if (reveal < 1f) handler.postDelayed({ requestDraw() }, REVEAL_STEP_MS)
|
|
278
|
+
if (!presented) {
|
|
279
|
+
presented = true
|
|
280
|
+
revealedAt = SystemClock.uptimeMillis()
|
|
281
|
+
onFirstFrame()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|