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:
2026-02-28 20:05:21 +01:00
parent 1480a2944b
commit 097cd24ea9
59 changed files with 13609 additions and 89 deletions

View 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");
}