App streaming pipeline, dashboard server status, account enable/disable, game-linked plans
- Add C++ native streaming engine (RTMP client, EGL context, streaming engine, JNI bridge) - Add pre-built arm64-v8a libs (librtmp, libssl, libcrypto, libz) and headers - Add Kotlin streaming layer (NativeStreamingEngine, StreamingManager, StreamingStats) - Add AIDL streaming interface (ILckStreamingService, ILckStreamingCallback, StreamingConfig) - Add LckStreamingServiceImpl with BIND_STREAMING action support - Add APP_STREAMING execution mode with auto-start/stop on plan lifecycle - SDK: add bindStreaming(), submitVideoFrame(), submitAudioFrame() to LckControlClient - Dashboard: replace linked accounts with server status card, move health polling from nav - Remove health check dot overlay from Dashboard nav icon - Accounts: add enable/disable toggle per account (persists locally, excluded from default plans) - Plans: add gameId field linked to game package ID, resolved from ClientTracker for default plans - Service: pass executionMode+gameId through createStreamPlan, filter enabled accounts in createDefaultPlan - Room DB migration 4→5: add isEnabled column to linked_accounts, gameId column to stream_plans - Add docs (hub vs control comparison)
This commit is contained in:
219
app/src/main/cpp/egl_context.cpp
Normal file
219
app/src/main/cpp/egl_context.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "egl_context.h"
|
||||
|
||||
#include <android/log.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define TAG "LckEglContext"
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
|
||||
#ifndef EGL_NATIVE_BUFFER_ANDROID
|
||||
#define EGL_NATIVE_BUFFER_ANDROID 0x3140
|
||||
#endif
|
||||
|
||||
#ifndef EGL_SYNC_NATIVE_FENCE_ANDROID
|
||||
#define EGL_SYNC_NATIVE_FENCE_ANDROID 0x3144
|
||||
#endif
|
||||
|
||||
#ifndef EGL_SYNC_NATIVE_FENCE_FD_ANDROID
|
||||
#define EGL_SYNC_NATIVE_FENCE_FD_ANDROID 0x3145
|
||||
#endif
|
||||
|
||||
#ifndef EGL_RECORDABLE_ANDROID
|
||||
#define EGL_RECORDABLE_ANDROID 0x3142
|
||||
#endif
|
||||
|
||||
EglContext::EglContext() {}
|
||||
|
||||
EglContext::~EglContext() {
|
||||
Release();
|
||||
}
|
||||
|
||||
bool EglContext::LoadExtensions() {
|
||||
eglCreateSyncKHR = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
|
||||
eglWaitSyncKHR = (PFNEGLWAITSYNCKHRPROC)eglGetProcAddress("eglWaitSyncKHR");
|
||||
eglDestroySyncKHR = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
|
||||
eglGetNativeClientBufferANDROID = (PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)eglGetProcAddress("eglGetNativeClientBufferANDROID");
|
||||
eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
|
||||
eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
|
||||
glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
eglPresentationTimeANDROID = (PFNEGLPRESENTATIONTIMEANDROIDPROC)eglGetProcAddress("eglPresentationTimeANDROID");
|
||||
|
||||
if (!eglGetNativeClientBufferANDROID || !eglCreateImageKHR ||
|
||||
!eglDestroyImageKHR || !glEGLImageTargetTexture2DOES) {
|
||||
LOGE("Missing required EGL extensions for HardwareBuffer import");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EglContext::Init() {
|
||||
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
LOGE("eglGetDisplay failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
EGLint major, minor;
|
||||
if (!eglInitialize(display, &major, &minor)) {
|
||||
LOGE("eglInitialize failed");
|
||||
return false;
|
||||
}
|
||||
LOGI("EGL initialized: %d.%d", major, minor);
|
||||
|
||||
// EGL config: RGBA8, ES3, recordable for MediaCodec
|
||||
EGLint configAttribs[] = {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_ALPHA_SIZE, 8,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_RECORDABLE_ANDROID, EGL_TRUE,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLint numConfigs;
|
||||
if (!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs) || numConfigs == 0) {
|
||||
LOGE("eglChooseConfig failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
EGLint contextAttribs[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 3,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (context == EGL_NO_CONTEXT) {
|
||||
LOGE("eglCreateContext failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadExtensions()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("EGL context created successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EglContext::CreateWindowSurface(ANativeWindow* window) {
|
||||
if (surface != EGL_NO_SURFACE) {
|
||||
eglDestroySurface(display, surface);
|
||||
}
|
||||
|
||||
surface = eglCreateWindowSurface(display, config, window, nullptr);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
LOGE("eglCreateWindowSurface failed: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
eglQuerySurface(display, surface, EGL_WIDTH, &surfaceWidth);
|
||||
eglQuerySurface(display, surface, EGL_HEIGHT, &surfaceHeight);
|
||||
LOGI("EGL window surface created: %dx%d", surfaceWidth, surfaceHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
GLuint EglContext::ImportHardwareBuffer(AHardwareBuffer* buffer) {
|
||||
if (!eglGetNativeClientBufferANDROID || !eglCreateImageKHR || !glEGLImageTargetTexture2DOES) {
|
||||
LOGE("Missing EGL extensions for HardwareBuffer import");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(buffer);
|
||||
if (!clientBuffer) {
|
||||
LOGE("eglGetNativeClientBufferANDROID failed");
|
||||
return 0;
|
||||
}
|
||||
|
||||
EGLint imageAttribs[] = {
|
||||
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLImageKHR image = eglCreateImageKHR(display, EGL_NO_CONTEXT,
|
||||
EGL_NATIVE_BUFFER_ANDROID,
|
||||
clientBuffer, imageAttribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) {
|
||||
LOGE("eglCreateImageKHR failed: 0x%x", eglGetError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
GLuint textureId;
|
||||
glGenTextures(1, &textureId);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
|
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, image);
|
||||
|
||||
// We need to keep the image alive — store it associated with the texture
|
||||
// The caller must call ReleaseImportedTexture to clean up
|
||||
// For now, we destroy the image immediately since the texture retains the content
|
||||
eglDestroyImageKHR(display, image);
|
||||
|
||||
return textureId;
|
||||
}
|
||||
|
||||
void EglContext::ReleaseImportedTexture(GLuint textureId, EGLImageKHR image) {
|
||||
if (textureId) {
|
||||
glDeleteTextures(1, &textureId);
|
||||
}
|
||||
if (image != EGL_NO_IMAGE_KHR && eglDestroyImageKHR) {
|
||||
eglDestroyImageKHR(display, image);
|
||||
}
|
||||
}
|
||||
|
||||
void EglContext::WaitFence(int fenceFd) {
|
||||
if (fenceFd < 0) return;
|
||||
|
||||
if (eglCreateSyncKHR && eglWaitSyncKHR && eglDestroySyncKHR) {
|
||||
EGLint attribs[] = {
|
||||
EGL_SYNC_NATIVE_FENCE_FD_ANDROID, fenceFd,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
EGLSyncKHR sync = eglCreateSyncKHR(display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
|
||||
if (sync != EGL_NO_SYNC_KHR) {
|
||||
// GPU-side wait — doesn't block CPU
|
||||
eglWaitSyncKHR(display, sync, 0);
|
||||
eglDestroySyncKHR(display, sync);
|
||||
// eglCreateSyncKHR takes ownership of fenceFd
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: CPU-side wait
|
||||
close(fenceFd);
|
||||
}
|
||||
|
||||
void EglContext::SetPresentationTime(int64_t timestampNs) {
|
||||
if (eglPresentationTimeANDROID && surface != EGL_NO_SURFACE) {
|
||||
eglPresentationTimeANDROID(display, surface, timestampNs);
|
||||
}
|
||||
}
|
||||
|
||||
bool EglContext::MakeCurrent() {
|
||||
return eglMakeCurrent(display, surface, surface, context) == EGL_TRUE;
|
||||
}
|
||||
|
||||
bool EglContext::SwapBuffers() {
|
||||
return eglSwapBuffers(display, surface) == EGL_TRUE;
|
||||
}
|
||||
|
||||
void EglContext::Release() {
|
||||
if (display != EGL_NO_DISPLAY) {
|
||||
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if (surface != EGL_NO_SURFACE) {
|
||||
eglDestroySurface(display, surface);
|
||||
surface = EGL_NO_SURFACE;
|
||||
}
|
||||
if (context != EGL_NO_CONTEXT) {
|
||||
eglDestroyContext(display, context);
|
||||
context = EGL_NO_CONTEXT;
|
||||
}
|
||||
eglTerminate(display);
|
||||
display = EGL_NO_DISPLAY;
|
||||
}
|
||||
LOGI("EGL resources released");
|
||||
}
|
||||
Reference in New Issue
Block a user