@pickleball/rtmp-native 0.1.0 → 0.2.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/android/build.gradle +15 -6
- package/android/src/main/AndroidManifest.xml +26 -0
- package/android/src/main/java/expo/modules/pickleballrtmp/CameraSession.kt +252 -0
- package/android/src/main/java/expo/modules/pickleballrtmp/Encoders.kt +222 -0
- package/android/src/main/java/expo/modules/pickleballrtmp/GlCompositor.kt +755 -0
- package/android/src/main/java/expo/modules/pickleballrtmp/PickleballRtmpNativeModule.kt +975 -135
- package/android/src/main/java/expo/modules/pickleballrtmp/PickleballRtmpPreviewView.kt +119 -23
- package/android/src/main/java/expo/modules/pickleballrtmp/PickleballRtmpService.kt +126 -0
- package/ios/PickleballRtmpNativeModule.swift +1161 -8
- package/package.json +3 -3
- package/src/PickleballRtmpNative.types.ts +140 -0
- package/src/PickleballRtmpNativeModule.ts +69 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
package expo.modules.pickleballrtmp
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Movie
|
|
8
|
+
import android.graphics.Paint
|
|
9
|
+
import android.graphics.PorterDuff
|
|
10
|
+
import android.graphics.RectF
|
|
11
|
+
import android.graphics.SurfaceTexture
|
|
12
|
+
import android.graphics.Typeface
|
|
13
|
+
import android.media.MediaPlayer
|
|
14
|
+
import android.opengl.EGL14
|
|
15
|
+
import android.opengl.EGLConfig
|
|
16
|
+
import android.opengl.EGLContext
|
|
17
|
+
import android.opengl.EGLDisplay
|
|
18
|
+
import android.opengl.EGLExt
|
|
19
|
+
import android.opengl.EGLSurface
|
|
20
|
+
import android.opengl.GLES11Ext
|
|
21
|
+
import android.opengl.GLES20
|
|
22
|
+
import android.opengl.GLUtils
|
|
23
|
+
import android.os.Handler
|
|
24
|
+
import android.os.HandlerThread
|
|
25
|
+
import android.os.SystemClock
|
|
26
|
+
import android.view.Surface
|
|
27
|
+
import java.io.File
|
|
28
|
+
import java.nio.ByteBuffer
|
|
29
|
+
import java.nio.ByteOrder
|
|
30
|
+
import java.nio.FloatBuffer
|
|
31
|
+
import java.util.concurrent.CountDownLatch
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pass GL chen giữa Camera2 và encoder — CHỈ tồn tại khi giải có overlay.
|
|
35
|
+
*
|
|
36
|
+
* Camera2 ──addTarget──▶ Surface(SurfaceTexture OES)
|
|
37
|
+
* │ onFrameAvailable (nhịp = camera, ~30fps)
|
|
38
|
+
* ▼
|
|
39
|
+
* GlCompositor (EGL riêng, 1 render thread)
|
|
40
|
+
* camera quad + từng lớp overlay theo thứ tự
|
|
41
|
+
* ├─▶ EGLSurface(encoder input) (+presentation time)
|
|
42
|
+
* └─▶ EGLSurface(preview view) (nếu có)
|
|
43
|
+
*
|
|
44
|
+
* Không có overlay thì module đi đường addTarget thẳng như kiến trúc 08/08 —
|
|
45
|
+
* pass này là opt-in, không phải hồi sinh tầng GL cũ. Hai bài học từ tầng cũ
|
|
46
|
+
* được khắc ở đây:
|
|
47
|
+
* · GL_LINEAR cho MỌI texture (GL_NEAREST là lý do tầng cũ bị gỡ — răng cưa);
|
|
48
|
+
* · blend GL_ONE / GL_ONE_MINUS_SRC_ALPHA vì Bitmap Android decode
|
|
49
|
+
* PREMULTIPLIED — dùng GL_SRC_ALPHA là viền đen quanh mép trong suốt.
|
|
50
|
+
*
|
|
51
|
+
* Timestamp: lấy từ SurfaceTexture của camera và đóng qua
|
|
52
|
+
* eglPresentationTimeANDROID — encoder KHÔNG được tự đóng dấu (VFR loạn nhịp).
|
|
53
|
+
*
|
|
54
|
+
* GIF đi đường Movie (deprecated nhưng vẫn sống ở API 35): mình tự cầm đồng hồ
|
|
55
|
+
* (setTime theo elapsed % duration) nên nhịp khung tất định như GifSource bên
|
|
56
|
+
* iOS, RAM O(1 khung). Movie chết ở Android nào đó thì lớp đó rơi riêng —
|
|
57
|
+
* animation là trang trí, không được đụng đường phát.
|
|
58
|
+
* VIDEO đi MediaPlayer → SurfaceTexture OES thứ hai, loop + câm tiếng.
|
|
59
|
+
*/
|
|
60
|
+
internal class GlCompositor(
|
|
61
|
+
private val context: Context,
|
|
62
|
+
private val outWidth: Int,
|
|
63
|
+
private val outHeight: Int,
|
|
64
|
+
private val encoderSurface: Surface,
|
|
65
|
+
/**
|
|
66
|
+
* Xoay nguồn camera (độ, bội 90). Chuẩn tham chiếu là CAMERA SAU của Redmi
|
|
67
|
+
* (sensor 90° — buffer tự nhiên đúng chiều ngang, đã kiểm 2 ngày field):
|
|
68
|
+
* module truyền (SENSOR_ORIENTATION − 90). Camera TRƯỚC (sensor 270) → 180.
|
|
69
|
+
* Hiện chỉ xử 0/180; 90/270 (dựng dọc) ngoài phạm vi như đường cũ.
|
|
70
|
+
*/
|
|
71
|
+
private val cameraRotation: Int = 0,
|
|
72
|
+
) {
|
|
73
|
+
/** Một lớp overlay theo hợp đồng JS (pixel của khung encode, gốc trên-trái). */
|
|
74
|
+
data class LayerSpec(
|
|
75
|
+
val id: String,
|
|
76
|
+
val kind: String, // "image" | "gif" | "video"
|
|
77
|
+
val filePath: String,
|
|
78
|
+
val x: Int,
|
|
79
|
+
val y: Int,
|
|
80
|
+
val width: Int,
|
|
81
|
+
val height: Int,
|
|
82
|
+
val opacity: Float,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Một lớp CHỮ đã điền sẵn (JS điền template — native chỉ vẽ). `x` là MỎ NEO
|
|
87
|
+
* theo `align`; màu đã parse thành ARGB ở module (bgArgb null = không nền,
|
|
88
|
+
* alpha của nền nằm trong chính bgArgb).
|
|
89
|
+
*/
|
|
90
|
+
data class TextSpec(
|
|
91
|
+
val id: String,
|
|
92
|
+
val text: String,
|
|
93
|
+
val x: Int,
|
|
94
|
+
val y: Int,
|
|
95
|
+
val fontSizePx: Int,
|
|
96
|
+
val align: String, // "left" | "center" | "right"
|
|
97
|
+
val bold: Boolean,
|
|
98
|
+
val colorArgb: Int,
|
|
99
|
+
val opacity: Float,
|
|
100
|
+
val bgArgb: Int?,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
private val thread = HandlerThread("pickleball-gl").also { it.start() }
|
|
104
|
+
private val handler = Handler(thread.looper)
|
|
105
|
+
|
|
106
|
+
private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY
|
|
107
|
+
private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT
|
|
108
|
+
private var eglConfig: EGLConfig? = null
|
|
109
|
+
private var encoderEgl: EGLSurface = EGL14.EGL_NO_SURFACE
|
|
110
|
+
private var previewEgl: EGLSurface = EGL14.EGL_NO_SURFACE
|
|
111
|
+
private var previewW = 0
|
|
112
|
+
private var previewH = 0
|
|
113
|
+
|
|
114
|
+
private var cameraTexId = 0
|
|
115
|
+
private var cameraST: SurfaceTexture? = null
|
|
116
|
+
/** Camera2 addTarget vào đây thay vì encoder surface. */
|
|
117
|
+
@Volatile var inputSurface: Surface? = null
|
|
118
|
+
private set
|
|
119
|
+
|
|
120
|
+
private var oesProgram = 0
|
|
121
|
+
private var rgbaProgram = 0
|
|
122
|
+
private val camMatrix = FloatArray(16)
|
|
123
|
+
|
|
124
|
+
private var layers: List<RuntimeLayer> = emptyList()
|
|
125
|
+
/** Lớp CHỮ — danh sách RIÊNG, vẽ TRÊN mọi lớp media. Tách để cập nhật chữ
|
|
126
|
+
* (2 lần/phút theo pha bóng) KHÔNG đụng tới media layer: setLayers rebuild
|
|
127
|
+
* là restart MediaPlayer của lớp video — churn đã đo, cấm tái phạm. */
|
|
128
|
+
private var textLayers: List<TextLayer> = emptyList()
|
|
129
|
+
@Volatile private var released = false
|
|
130
|
+
|
|
131
|
+
/** Báo cáo dựng lớp lần gần nhất — module trả về JS để log. */
|
|
132
|
+
@Volatile var lastLayerReport: List<Map<String, Any>> = emptyList()
|
|
133
|
+
private set
|
|
134
|
+
|
|
135
|
+
/** Báo cáo lớp chữ lần áp gần nhất. */
|
|
136
|
+
@Volatile var lastTextReport: List<Map<String, Any>> = emptyList()
|
|
137
|
+
private set
|
|
138
|
+
|
|
139
|
+
init {
|
|
140
|
+
runBlockingOnGl {
|
|
141
|
+
setupEgl()
|
|
142
|
+
setupCameraSource()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// MARK: - API (mọi việc GL đều nhảy vào render thread)
|
|
147
|
+
|
|
148
|
+
fun setLayers(specs: List<LayerSpec>) {
|
|
149
|
+
runBlockingOnGl {
|
|
150
|
+
layers.forEach { it.release() }
|
|
151
|
+
val report = mutableListOf<Map<String, Any>>()
|
|
152
|
+
layers = specs.mapNotNull { spec ->
|
|
153
|
+
val built = buildLayer(spec)
|
|
154
|
+
report.add(
|
|
155
|
+
if (built != null) {
|
|
156
|
+
mapOf("id" to spec.id, "applied" to true, "kind" to spec.kind)
|
|
157
|
+
} else {
|
|
158
|
+
mapOf("id" to spec.id, "applied" to false, "why" to "${spec.kind}_decode_failed")
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
built
|
|
162
|
+
}
|
|
163
|
+
lastLayerReport = report
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Áp danh sách lớp CHỮ — cập nhật TẠI CHỖ theo id: chữ đổi thì render lại
|
|
169
|
+
* bitmap + texImage2D lên texture cũ; media layers không bị chạm. Spec y hệt
|
|
170
|
+
* lần trước thì lớp đó là no-op tuyệt đối.
|
|
171
|
+
*/
|
|
172
|
+
fun setTextLayers(specs: List<TextSpec>) {
|
|
173
|
+
runBlockingOnGl {
|
|
174
|
+
val byId = textLayers.associateBy { it.id }.toMutableMap()
|
|
175
|
+
val report = mutableListOf<Map<String, Any>>()
|
|
176
|
+
val next = mutableListOf<TextLayer>()
|
|
177
|
+
for (spec in specs) {
|
|
178
|
+
// Tái dùng texture theo id — lấy RA khỏi map để phần còn lại là "lớp bị gỡ".
|
|
179
|
+
val layer = byId.remove(spec.id) ?: TextLayer(spec.id, makeTexture2D())
|
|
180
|
+
val ok = runCatching { layer.update(this, spec) }.getOrDefault(false)
|
|
181
|
+
if (ok) {
|
|
182
|
+
next.add(layer)
|
|
183
|
+
report.add(mapOf("id" to spec.id, "applied" to true))
|
|
184
|
+
} else {
|
|
185
|
+
// Lớp hỏng rơi RIÊNG (chữ là trang trí) — trả texture, không giữ xác.
|
|
186
|
+
layer.release()
|
|
187
|
+
report.add(mapOf("id" to spec.id, "applied" to false, "why" to "text_render_failed"))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
byId.values.forEach { it.release() }
|
|
191
|
+
textLayers = next
|
|
192
|
+
lastTextReport = report
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Preview view đến/đi không cần dựng lại camera — chỉ thay EGLSurface đích. */
|
|
197
|
+
fun setPreviewSurface(surface: Surface?, width: Int, height: Int) {
|
|
198
|
+
runBlockingOnGl {
|
|
199
|
+
if (previewEgl != EGL14.EGL_NO_SURFACE) {
|
|
200
|
+
EGL14.eglDestroySurface(eglDisplay, previewEgl)
|
|
201
|
+
previewEgl = EGL14.EGL_NO_SURFACE
|
|
202
|
+
}
|
|
203
|
+
if (surface != null && surface.isValid) {
|
|
204
|
+
previewEgl = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface, intArrayOf(EGL14.EGL_NONE), 0)
|
|
205
|
+
previewW = width
|
|
206
|
+
previewH = height
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fun release() {
|
|
212
|
+
released = true
|
|
213
|
+
runBlockingOnGl {
|
|
214
|
+
layers.forEach { it.release() }
|
|
215
|
+
layers = emptyList()
|
|
216
|
+
textLayers.forEach { it.release() }
|
|
217
|
+
textLayers = emptyList()
|
|
218
|
+
cameraST?.setOnFrameAvailableListener(null)
|
|
219
|
+
inputSurface?.release()
|
|
220
|
+
inputSurface = null
|
|
221
|
+
cameraST?.release()
|
|
222
|
+
cameraST = null
|
|
223
|
+
if (oesProgram != 0) GLES20.glDeleteProgram(oesProgram)
|
|
224
|
+
if (rgbaProgram != 0) GLES20.glDeleteProgram(rgbaProgram)
|
|
225
|
+
if (cameraTexId != 0) GLES20.glDeleteTextures(1, intArrayOf(cameraTexId), 0)
|
|
226
|
+
if (previewEgl != EGL14.EGL_NO_SURFACE) EGL14.eglDestroySurface(eglDisplay, previewEgl)
|
|
227
|
+
if (encoderEgl != EGL14.EGL_NO_SURFACE) EGL14.eglDestroySurface(eglDisplay, encoderEgl)
|
|
228
|
+
EGL14.eglMakeCurrent(
|
|
229
|
+
eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT,
|
|
230
|
+
)
|
|
231
|
+
if (eglContext != EGL14.EGL_NO_CONTEXT) EGL14.eglDestroyContext(eglDisplay, eglContext)
|
|
232
|
+
if (eglDisplay != EGL14.EGL_NO_DISPLAY) EGL14.eglTerminate(eglDisplay)
|
|
233
|
+
}
|
|
234
|
+
thread.quitSafely()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// MARK: - EGL + camera source
|
|
238
|
+
|
|
239
|
+
private fun setupEgl() {
|
|
240
|
+
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
|
|
241
|
+
check(eglDisplay != EGL14.EGL_NO_DISPLAY) { "không lấy được EGL display" }
|
|
242
|
+
val version = IntArray(2)
|
|
243
|
+
check(EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { "eglInitialize thất bại" }
|
|
244
|
+
val attribs = intArrayOf(
|
|
245
|
+
EGL14.EGL_RED_SIZE, 8,
|
|
246
|
+
EGL14.EGL_GREEN_SIZE, 8,
|
|
247
|
+
EGL14.EGL_BLUE_SIZE, 8,
|
|
248
|
+
EGL14.EGL_ALPHA_SIZE, 8,
|
|
249
|
+
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
|
250
|
+
// EGL_RECORDABLE_ANDROID: bắt buộc để ghi được vào surface của MediaCodec.
|
|
251
|
+
0x3142, 1,
|
|
252
|
+
EGL14.EGL_NONE,
|
|
253
|
+
)
|
|
254
|
+
val configs = arrayOfNulls<EGLConfig>(1)
|
|
255
|
+
val num = IntArray(1)
|
|
256
|
+
check(EGL14.eglChooseConfig(eglDisplay, attribs, 0, configs, 0, 1, num, 0) && num[0] > 0) {
|
|
257
|
+
"không có EGL config phù hợp"
|
|
258
|
+
}
|
|
259
|
+
eglConfig = configs[0]
|
|
260
|
+
eglContext = EGL14.eglCreateContext(
|
|
261
|
+
eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT,
|
|
262
|
+
intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE), 0,
|
|
263
|
+
)
|
|
264
|
+
check(eglContext != EGL14.EGL_NO_CONTEXT) { "không tạo được EGL context" }
|
|
265
|
+
encoderEgl = EGL14.eglCreateWindowSurface(
|
|
266
|
+
eglDisplay, eglConfig, encoderSurface, intArrayOf(EGL14.EGL_NONE), 0,
|
|
267
|
+
)
|
|
268
|
+
check(encoderEgl != EGL14.EGL_NO_SURFACE) { "không tạo được EGLSurface cho encoder" }
|
|
269
|
+
check(EGL14.eglMakeCurrent(eglDisplay, encoderEgl, encoderEgl, eglContext)) {
|
|
270
|
+
"eglMakeCurrent thất bại"
|
|
271
|
+
}
|
|
272
|
+
oesProgram = buildProgram(VERTEX_SHADER, FS_OES)
|
|
273
|
+
rgbaProgram = buildProgram(VERTEX_SHADER, FS_RGBA)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private fun setupCameraSource() {
|
|
277
|
+
val ids = IntArray(1)
|
|
278
|
+
GLES20.glGenTextures(1, ids, 0)
|
|
279
|
+
cameraTexId = ids[0]
|
|
280
|
+
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexId)
|
|
281
|
+
linearClamp(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
|
|
282
|
+
val st = SurfaceTexture(cameraTexId)
|
|
283
|
+
st.setDefaultBufferSize(outWidth, outHeight)
|
|
284
|
+
// Nghe trên CHÍNH render thread — khung camera đến là vẽ, không nhảy thread.
|
|
285
|
+
st.setOnFrameAvailableListener({ onCameraFrame() }, handler)
|
|
286
|
+
cameraST = st
|
|
287
|
+
inputSurface = Surface(st)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// MARK: - Render
|
|
291
|
+
|
|
292
|
+
private fun onCameraFrame() {
|
|
293
|
+
if (released) return
|
|
294
|
+
val st = cameraST ?: return
|
|
295
|
+
if (!EGL14.eglMakeCurrent(eglDisplay, encoderEgl, encoderEgl, eglContext)) return
|
|
296
|
+
st.updateTexImage()
|
|
297
|
+
st.getTransformMatrix(camMatrix)
|
|
298
|
+
layers.forEach { it.tick() }
|
|
299
|
+
|
|
300
|
+
drawScene(outWidth, outHeight)
|
|
301
|
+
EGLExt.eglPresentationTimeANDROID(eglDisplay, encoderEgl, st.timestamp)
|
|
302
|
+
EGL14.eglSwapBuffers(eglDisplay, encoderEgl)
|
|
303
|
+
|
|
304
|
+
if (previewEgl != EGL14.EGL_NO_SURFACE) {
|
|
305
|
+
if (EGL14.eglMakeCurrent(eglDisplay, previewEgl, previewEgl, eglContext)) {
|
|
306
|
+
drawScene(previewW, previewH)
|
|
307
|
+
EGL14.eglSwapBuffers(eglDisplay, previewEgl)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private fun drawScene(viewW: Int, viewH: Int) {
|
|
313
|
+
GLES20.glViewport(0, 0, viewW, viewH)
|
|
314
|
+
GLES20.glClearColor(0f, 0f, 0f, 1f)
|
|
315
|
+
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
|
316
|
+
// Camera full khung — KHÔNG dùng transform matrix của SurfaceTexture: ma
|
|
317
|
+
// trận đó xoay ảnh về chiều dọc-tự-nhiên của điện thoại (chuẩn hiển thị),
|
|
318
|
+
// vẽ vào khung ngang là xoay 90° + bóp dẹt (đo thật 26/08 tối). Đường
|
|
319
|
+
// encoder trực tiếp (không GL, đã kiểm field) nhận BUFFER THÔ — GL phải
|
|
320
|
+
// sample đúng buffer thô đó: identity + lật v cho hệ GL.
|
|
321
|
+
drawQuad(
|
|
322
|
+
oesProgram, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexId,
|
|
323
|
+
if (cameraRotation == 180) FULL_RECT_NDC_ROT180 else FULL_RECT_NDC,
|
|
324
|
+
IDENTITY, 1f, blend = false,
|
|
325
|
+
)
|
|
326
|
+
for (layer in layers) {
|
|
327
|
+
layer.draw(this)
|
|
328
|
+
}
|
|
329
|
+
// Chữ trên cùng — sau mọi lớp media.
|
|
330
|
+
for (layer in textLayers) {
|
|
331
|
+
layer.draw(this)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
internal fun drawLayerQuad(
|
|
336
|
+
program: Int, target: Int, texId: Int,
|
|
337
|
+
rect: FloatArray, texMatrix: FloatArray, opacity: Float,
|
|
338
|
+
) = drawQuad(program, target, texId, rect, texMatrix, opacity, blend = true)
|
|
339
|
+
|
|
340
|
+
private fun drawQuad(
|
|
341
|
+
program: Int, target: Int, texId: Int,
|
|
342
|
+
ndcRect: FloatArray, texMatrix: FloatArray, opacity: Float, blend: Boolean,
|
|
343
|
+
) {
|
|
344
|
+
GLES20.glUseProgram(program)
|
|
345
|
+
if (blend) {
|
|
346
|
+
GLES20.glEnable(GLES20.GL_BLEND)
|
|
347
|
+
// Premultiplied alpha — GL_ONE, không phải GL_SRC_ALPHA (viền đen).
|
|
348
|
+
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA)
|
|
349
|
+
} else {
|
|
350
|
+
GLES20.glDisable(GLES20.GL_BLEND)
|
|
351
|
+
}
|
|
352
|
+
val pos = GLES20.glGetAttribLocation(program, "aPos")
|
|
353
|
+
val tex = GLES20.glGetAttribLocation(program, "aTex")
|
|
354
|
+
val uM = GLES20.glGetUniformLocation(program, "uTexMatrix")
|
|
355
|
+
val uA = GLES20.glGetUniformLocation(program, "uAlpha")
|
|
356
|
+
val uS = GLES20.glGetUniformLocation(program, "uTexture")
|
|
357
|
+
|
|
358
|
+
vertexBuf.clear()
|
|
359
|
+
vertexBuf.put(ndcRect).position(0)
|
|
360
|
+
GLES20.glEnableVertexAttribArray(pos)
|
|
361
|
+
GLES20.glVertexAttribPointer(pos, 2, GLES20.GL_FLOAT, false, 16, vertexBuf)
|
|
362
|
+
vertexBuf.position(2)
|
|
363
|
+
GLES20.glEnableVertexAttribArray(tex)
|
|
364
|
+
GLES20.glVertexAttribPointer(tex, 2, GLES20.GL_FLOAT, false, 16, vertexBuf)
|
|
365
|
+
|
|
366
|
+
GLES20.glUniformMatrix4fv(uM, 1, false, texMatrix, 0)
|
|
367
|
+
GLES20.glUniform1f(uA, opacity)
|
|
368
|
+
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
|
369
|
+
GLES20.glBindTexture(target, texId)
|
|
370
|
+
GLES20.glUniform1i(uS, 0)
|
|
371
|
+
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
|
|
372
|
+
GLES20.glDisableVertexAttribArray(pos)
|
|
373
|
+
GLES20.glDisableVertexAttribArray(tex)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* [x,y,w,h] pixel (gốc trên-trái) → strip NDC xen kẽ pos/tex (x,y,u,v ×4).
|
|
378
|
+
* Hai hệ v NGƯỢC nhau: bitmap upload hàng đầu = mép TRÊN (v=0 ở đỉnh), còn
|
|
379
|
+
* SurfaceTexture OES (video/camera) theo hệ GL v=0 ở ĐÁY — đo thật 26/08:
|
|
380
|
+
* dùng hệ bitmap cho video là chữ trên sóng lật ngược.
|
|
381
|
+
*/
|
|
382
|
+
internal fun ndcRectOf(x: Int, y: Int, w: Int, h: Int, oesV: Boolean = false): FloatArray {
|
|
383
|
+
val x0 = 2f * x / outWidth - 1f
|
|
384
|
+
val x1 = 2f * (x + w) / outWidth - 1f
|
|
385
|
+
val yTop = 1f - 2f * y / outHeight
|
|
386
|
+
val yBot = 1f - 2f * (y + h) / outHeight
|
|
387
|
+
val vTop = if (oesV) 1f else 0f
|
|
388
|
+
val vBot = if (oesV) 0f else 1f
|
|
389
|
+
return floatArrayOf(
|
|
390
|
+
x0, yBot, 0f, vBot,
|
|
391
|
+
x1, yBot, 1f, vBot,
|
|
392
|
+
x0, yTop, 0f, vTop,
|
|
393
|
+
x1, yTop, 1f, vTop,
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// MARK: - Layers
|
|
398
|
+
|
|
399
|
+
private fun buildLayer(spec: LayerSpec): RuntimeLayer? {
|
|
400
|
+
if (spec.width < 1 || spec.height < 1) return null
|
|
401
|
+
if (!File(spec.filePath).exists()) return null
|
|
402
|
+
return when (spec.kind) {
|
|
403
|
+
"image" -> StaticLayer.create(this, spec)
|
|
404
|
+
"gif" -> GifLayer.create(this, spec)
|
|
405
|
+
"video" -> VideoLayer.create(this, spec)
|
|
406
|
+
else -> null
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
internal fun makeTexture2D(): Int {
|
|
411
|
+
val ids = IntArray(1)
|
|
412
|
+
GLES20.glGenTextures(1, ids, 0)
|
|
413
|
+
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, ids[0])
|
|
414
|
+
linearClamp(GLES20.GL_TEXTURE_2D)
|
|
415
|
+
return ids[0]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
internal fun makeTextureOes(): Int {
|
|
419
|
+
val ids = IntArray(1)
|
|
420
|
+
GLES20.glGenTextures(1, ids, 0)
|
|
421
|
+
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, ids[0])
|
|
422
|
+
linearClamp(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)
|
|
423
|
+
return ids[0]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private fun linearClamp(target: Int) {
|
|
427
|
+
GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
|
|
428
|
+
GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
|
|
429
|
+
GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
|
|
430
|
+
GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
internal abstract class RuntimeLayer {
|
|
434
|
+
/** Cập nhật nguồn (khung gif/video mới) — chạy trên render thread. */
|
|
435
|
+
open fun tick() = Unit
|
|
436
|
+
abstract fun draw(gl: GlCompositor)
|
|
437
|
+
open fun release() = Unit
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Ảnh tĩnh (composite/logo): upload MỘT lần, mỗi khung chỉ còn draw. */
|
|
441
|
+
private class StaticLayer(
|
|
442
|
+
private val texId: Int,
|
|
443
|
+
private val rect: FloatArray,
|
|
444
|
+
private val opacity: Float,
|
|
445
|
+
) : RuntimeLayer() {
|
|
446
|
+
override fun draw(gl: GlCompositor) =
|
|
447
|
+
gl.drawLayerQuad(gl.rgbaProgram, GLES20.GL_TEXTURE_2D, texId, rect, IDENTITY, opacity)
|
|
448
|
+
|
|
449
|
+
override fun release() {
|
|
450
|
+
GLES20.glDeleteTextures(1, intArrayOf(texId), 0)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
companion object {
|
|
454
|
+
fun create(gl: GlCompositor, spec: LayerSpec): StaticLayer? {
|
|
455
|
+
val bitmap = BitmapFactory.decodeFile(spec.filePath) ?: return null
|
|
456
|
+
val texId = gl.makeTexture2D()
|
|
457
|
+
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
|
|
458
|
+
bitmap.recycle()
|
|
459
|
+
return StaticLayer(texId, gl.ndcRectOf(spec.x, spec.y, spec.width, spec.height), spec.opacity)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* GIF qua Movie: mình cầm đồng hồ (elapsed % duration), mỗi tick vẽ đúng một
|
|
466
|
+
* khung vào bitmap cỡ ô rồi texSubImage2D — RAM O(1 khung) như GifSource iOS.
|
|
467
|
+
*/
|
|
468
|
+
private class GifLayer(
|
|
469
|
+
private val movie: Movie,
|
|
470
|
+
private val texId: Int,
|
|
471
|
+
private val bitmap: Bitmap,
|
|
472
|
+
private val canvas: Canvas,
|
|
473
|
+
private val rect: FloatArray,
|
|
474
|
+
private val opacity: Float,
|
|
475
|
+
) : RuntimeLayer() {
|
|
476
|
+
private val startedAt = SystemClock.elapsedRealtime()
|
|
477
|
+
private val durationMs = movie.duration().coerceAtLeast(1)
|
|
478
|
+
private var lastShownMs = -1
|
|
479
|
+
|
|
480
|
+
override fun tick() {
|
|
481
|
+
val t = ((SystemClock.elapsedRealtime() - startedAt) % durationMs).toInt()
|
|
482
|
+
// Movie tự biết khung nào ứng thời điểm nào; chỉ upload khi mốc đổi.
|
|
483
|
+
if (t == lastShownMs) return
|
|
484
|
+
lastShownMs = t
|
|
485
|
+
movie.setTime(t)
|
|
486
|
+
canvas.drawColor(0, PorterDuff.Mode.CLEAR)
|
|
487
|
+
canvas.save()
|
|
488
|
+
canvas.scale(
|
|
489
|
+
bitmap.width.toFloat() / movie.width().coerceAtLeast(1),
|
|
490
|
+
bitmap.height.toFloat() / movie.height().coerceAtLeast(1),
|
|
491
|
+
)
|
|
492
|
+
movie.draw(canvas, 0f, 0f)
|
|
493
|
+
canvas.restore()
|
|
494
|
+
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId)
|
|
495
|
+
GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, bitmap)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
override fun draw(gl: GlCompositor) =
|
|
499
|
+
gl.drawLayerQuad(gl.rgbaProgram, GLES20.GL_TEXTURE_2D, texId, rect, IDENTITY, opacity)
|
|
500
|
+
|
|
501
|
+
override fun release() {
|
|
502
|
+
GLES20.glDeleteTextures(1, intArrayOf(texId), 0)
|
|
503
|
+
bitmap.recycle()
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
companion object {
|
|
507
|
+
@Suppress("DEPRECATION")
|
|
508
|
+
fun create(gl: GlCompositor, spec: LayerSpec): GifLayer? {
|
|
509
|
+
val movie = runCatching { Movie.decodeFile(spec.filePath) }.getOrNull() ?: return null
|
|
510
|
+
if (movie.width() < 1 || movie.height() < 1) return null
|
|
511
|
+
val bitmap = Bitmap.createBitmap(spec.width, spec.height, Bitmap.Config.ARGB_8888)
|
|
512
|
+
val canvas = Canvas(bitmap)
|
|
513
|
+
val texId = gl.makeTexture2D()
|
|
514
|
+
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
|
|
515
|
+
return GifLayer(
|
|
516
|
+
movie, texId, bitmap, canvas,
|
|
517
|
+
gl.ndcRectOf(spec.x, spec.y, spec.width, spec.height), spec.opacity,
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* VIDEO qua MediaPlayer → SurfaceTexture OES riêng: decode cứng, loop, câm
|
|
525
|
+
* tiếng. Opacity ép 1 (khớp iOS — nhãn UI đã nói thật điều này).
|
|
526
|
+
*/
|
|
527
|
+
private class VideoLayer(
|
|
528
|
+
private val player: MediaPlayer,
|
|
529
|
+
private val texId: Int,
|
|
530
|
+
private val st: SurfaceTexture,
|
|
531
|
+
private val surface: Surface,
|
|
532
|
+
private val rect: FloatArray,
|
|
533
|
+
) : RuntimeLayer() {
|
|
534
|
+
@Volatile private var frameReady = false
|
|
535
|
+
private val matrix = FloatArray(16)
|
|
536
|
+
|
|
537
|
+
init {
|
|
538
|
+
st.setOnFrameAvailableListener { frameReady = true }
|
|
539
|
+
android.opengl.Matrix.setIdentityM(matrix, 0)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
override fun tick() {
|
|
543
|
+
if (!frameReady) return
|
|
544
|
+
frameReady = false
|
|
545
|
+
st.updateTexImage()
|
|
546
|
+
st.getTransformMatrix(matrix)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
override fun draw(gl: GlCompositor) =
|
|
550
|
+
gl.drawLayerQuad(gl.oesProgram, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId, rect, matrix, 1f)
|
|
551
|
+
|
|
552
|
+
override fun release() {
|
|
553
|
+
runCatching { player.stop() }
|
|
554
|
+
runCatching { player.release() }
|
|
555
|
+
st.setOnFrameAvailableListener(null)
|
|
556
|
+
surface.release()
|
|
557
|
+
st.release()
|
|
558
|
+
GLES20.glDeleteTextures(1, intArrayOf(texId), 0)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
companion object {
|
|
562
|
+
fun create(gl: GlCompositor, spec: LayerSpec): VideoLayer? {
|
|
563
|
+
val texId = gl.makeTextureOes()
|
|
564
|
+
val st = SurfaceTexture(texId)
|
|
565
|
+
st.setDefaultBufferSize(spec.width, spec.height)
|
|
566
|
+
val surface = Surface(st)
|
|
567
|
+
val player = MediaPlayer()
|
|
568
|
+
val ok = runCatching {
|
|
569
|
+
player.setDataSource(spec.filePath)
|
|
570
|
+
player.setSurface(surface)
|
|
571
|
+
player.isLooping = true
|
|
572
|
+
player.setVolume(0f, 0f)
|
|
573
|
+
player.prepare()
|
|
574
|
+
player.start()
|
|
575
|
+
}.isSuccess
|
|
576
|
+
if (!ok) {
|
|
577
|
+
runCatching { player.release() }
|
|
578
|
+
surface.release()
|
|
579
|
+
st.release()
|
|
580
|
+
GLES20.glDeleteTextures(1, intArrayOf(texId), 0)
|
|
581
|
+
return null
|
|
582
|
+
}
|
|
583
|
+
return VideoLayer(
|
|
584
|
+
player, texId, st, surface,
|
|
585
|
+
gl.ndcRectOf(spec.x, spec.y, spec.width, spec.height, oesV = true),
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Lớp CHỮ: bitmap vẽ bằng Paint (font hệ thống — tiếng Việt đủ dấu) upload
|
|
593
|
+
* lên MỘT texture tái dùng theo id. `update` chỉ chạy khi spec ĐỔI (so
|
|
594
|
+
* equality data class) — mỗi pha bóng một lần render ~vài trăm µs, giữa hai
|
|
595
|
+
* pha là no-op tuyệt đối. Hình học nền = bội fontSize (0.45/0.25/0.22),
|
|
596
|
+
* PHẢI khớp OVERLAY_TEXT_BG của @pickleball/shared (preview editor cùng công thức).
|
|
597
|
+
*/
|
|
598
|
+
internal class TextLayer(val id: String, private val texId: Int) : RuntimeLayer() {
|
|
599
|
+
private var applied: TextSpec? = null
|
|
600
|
+
private var rect: FloatArray = FloatArray(16)
|
|
601
|
+
private var opacity = 1f
|
|
602
|
+
private var hasContent = false
|
|
603
|
+
|
|
604
|
+
/** Render + upload nếu spec đổi. Trả false khi không dựng được bitmap. */
|
|
605
|
+
fun update(gl: GlCompositor, spec: TextSpec): Boolean {
|
|
606
|
+
if (spec == applied) return true
|
|
607
|
+
val fontSize = spec.fontSizePx.coerceAtLeast(8).toFloat()
|
|
608
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
609
|
+
textSize = fontSize
|
|
610
|
+
typeface = if (spec.bold) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
|
611
|
+
color = spec.colorArgb
|
|
612
|
+
}
|
|
613
|
+
val fm = paint.fontMetrics
|
|
614
|
+
val textW = paint.measureText(spec.text)
|
|
615
|
+
val hasBg = spec.bgArgb != null
|
|
616
|
+
val padX = if (hasBg) fontSize * 0.45f else 0f
|
|
617
|
+
val padY = if (hasBg) fontSize * 0.25f else 0f
|
|
618
|
+
val w = kotlin.math.ceil(textW + padX * 2).toInt().coerceAtLeast(1)
|
|
619
|
+
val h = kotlin.math.ceil((fm.descent - fm.ascent) + padY * 2).toInt().coerceAtLeast(1)
|
|
620
|
+
if (w > 4096 || h > 1024) return false
|
|
621
|
+
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
|
622
|
+
val canvas = Canvas(bitmap)
|
|
623
|
+
if (hasBg) {
|
|
624
|
+
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = spec.bgArgb!! }
|
|
625
|
+
val radius = fontSize * 0.22f
|
|
626
|
+
canvas.drawRoundRect(RectF(0f, 0f, w.toFloat(), h.toFloat()), radius, radius, bgPaint)
|
|
627
|
+
}
|
|
628
|
+
canvas.drawText(spec.text, padX, padY - fm.ascent, paint)
|
|
629
|
+
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId)
|
|
630
|
+
// texImage2D (không phải SUB): bề rộng chữ đổi theo nội dung ("7"→"10"),
|
|
631
|
+
// cấp phát lại storage trên CÙNG texture id — layer không bị dựng lại.
|
|
632
|
+
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
|
|
633
|
+
bitmap.recycle()
|
|
634
|
+
// Mỏ neo theo align: x là mép trái/tâm/mép phải của ẢNH (ảnh ôm trọn nền).
|
|
635
|
+
val left = when (spec.align) {
|
|
636
|
+
"center" -> spec.x - w / 2
|
|
637
|
+
"right" -> spec.x - w
|
|
638
|
+
else -> spec.x
|
|
639
|
+
}.coerceAtLeast(0)
|
|
640
|
+
rect = gl.ndcRectOf(left, spec.y, w, h)
|
|
641
|
+
opacity = spec.opacity
|
|
642
|
+
applied = spec
|
|
643
|
+
hasContent = true
|
|
644
|
+
return true
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
override fun draw(gl: GlCompositor) {
|
|
648
|
+
if (!hasContent) return
|
|
649
|
+
// Opacity CẢ LỚP qua uAlpha: shader nhân cả rgb lẫn alpha — đúng phép
|
|
650
|
+
// fade premultiplied, không cần bake vào bitmap như đường CPU bên iOS.
|
|
651
|
+
gl.drawLayerQuad(gl.rgbaProgram, GLES20.GL_TEXTURE_2D, texId, rect, IDENTITY, opacity)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
override fun release() {
|
|
655
|
+
GLES20.glDeleteTextures(1, intArrayOf(texId), 0)
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// MARK: - Helpers
|
|
660
|
+
|
|
661
|
+
private fun runBlockingOnGl(block: () -> Unit) {
|
|
662
|
+
if (Thread.currentThread() === thread) {
|
|
663
|
+
block()
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
val latch = CountDownLatch(1)
|
|
667
|
+
handler.post {
|
|
668
|
+
try {
|
|
669
|
+
block()
|
|
670
|
+
} finally {
|
|
671
|
+
latch.countDown()
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
latch.await()
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private fun buildProgram(vs: String, fs: String): Int {
|
|
678
|
+
fun compile(type: Int, src: String): Int {
|
|
679
|
+
val id = GLES20.glCreateShader(type)
|
|
680
|
+
GLES20.glShaderSource(id, src)
|
|
681
|
+
GLES20.glCompileShader(id)
|
|
682
|
+
val ok = IntArray(1)
|
|
683
|
+
GLES20.glGetShaderiv(id, GLES20.GL_COMPILE_STATUS, ok, 0)
|
|
684
|
+
check(ok[0] != 0) { "shader lỗi: ${GLES20.glGetShaderInfoLog(id)}" }
|
|
685
|
+
return id
|
|
686
|
+
}
|
|
687
|
+
val program = GLES20.glCreateProgram()
|
|
688
|
+
GLES20.glAttachShader(program, compile(GLES20.GL_VERTEX_SHADER, vs))
|
|
689
|
+
GLES20.glAttachShader(program, compile(GLES20.GL_FRAGMENT_SHADER, fs))
|
|
690
|
+
GLES20.glLinkProgram(program)
|
|
691
|
+
val ok = IntArray(1)
|
|
692
|
+
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, ok, 0)
|
|
693
|
+
check(ok[0] != 0) { "link program lỗi: ${GLES20.glGetProgramInfoLog(program)}" }
|
|
694
|
+
return program
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private companion object {
|
|
698
|
+
val IDENTITY = FloatArray(16).also { android.opengl.Matrix.setIdentityM(it, 0) }
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Full khung cho CAMERA sample BUFFER THÔ (identity, không qua ma trận ST):
|
|
702
|
+
* hàng-0 của buffer = mép TRÊN ảnh → v=0 phải nằm ở ĐỈNH quad. Gắn v=0
|
|
703
|
+
* xuống đáy là "lộn ngược chốc đầu" (đo thật 26/08 tối, vòng thử 2).
|
|
704
|
+
*/
|
|
705
|
+
val FULL_RECT_NDC = floatArrayOf(
|
|
706
|
+
-1f, -1f, 0f, 1f,
|
|
707
|
+
1f, -1f, 1f, 1f,
|
|
708
|
+
-1f, 1f, 0f, 0f,
|
|
709
|
+
1f, 1f, 1f, 0f,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
/** Như trên nhưng texcoord xoay 180° — (u,v) → (1−u, 1−v). */
|
|
713
|
+
val FULL_RECT_NDC_ROT180 = floatArrayOf(
|
|
714
|
+
-1f, -1f, 1f, 1f,
|
|
715
|
+
1f, -1f, 0f, 1f,
|
|
716
|
+
-1f, 1f, 1f, 0f,
|
|
717
|
+
1f, 1f, 0f, 0f,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
val vertexBuf: FloatBuffer = ByteBuffer.allocateDirect(16 * 4)
|
|
721
|
+
.order(ByteOrder.nativeOrder()).asFloatBuffer()
|
|
722
|
+
|
|
723
|
+
const val VERTEX_SHADER = """
|
|
724
|
+
attribute vec2 aPos;
|
|
725
|
+
attribute vec2 aTex;
|
|
726
|
+
uniform mat4 uTexMatrix;
|
|
727
|
+
varying vec2 vTex;
|
|
728
|
+
void main() {
|
|
729
|
+
gl_Position = vec4(aPos, 0.0, 1.0);
|
|
730
|
+
vTex = (uTexMatrix * vec4(aTex, 0.0, 1.0)).xy;
|
|
731
|
+
}
|
|
732
|
+
"""
|
|
733
|
+
|
|
734
|
+
const val FS_OES = """
|
|
735
|
+
#extension GL_OES_EGL_image_external : require
|
|
736
|
+
precision mediump float;
|
|
737
|
+
varying vec2 vTex;
|
|
738
|
+
uniform samplerExternalOES uTexture;
|
|
739
|
+
uniform float uAlpha;
|
|
740
|
+
void main() {
|
|
741
|
+
gl_FragColor = texture2D(uTexture, vTex) * uAlpha;
|
|
742
|
+
}
|
|
743
|
+
"""
|
|
744
|
+
|
|
745
|
+
const val FS_RGBA = """
|
|
746
|
+
precision mediump float;
|
|
747
|
+
varying vec2 vTex;
|
|
748
|
+
uniform sampler2D uTexture;
|
|
749
|
+
uniform float uAlpha;
|
|
750
|
+
void main() {
|
|
751
|
+
gl_FragColor = texture2D(uTexture, vTex) * uAlpha;
|
|
752
|
+
}
|
|
753
|
+
"""
|
|
754
|
+
}
|
|
755
|
+
}
|