@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.
@@ -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
+ }