Files
lck-control/app/src/main/cpp/egl_context.cpp
omigamedev c632e22033 Custom RTMP saved accounts, RTMP test server, composition pipeline
- Backend: POST /providers/accounts/custom-rtmp to save reusable RTMP servers
- Backend: Encrypt rtmpUrl/streamKey in existing token fields, decrypt on GET
- Backend: Skip token revocation on DELETE for CUSTOM_RTMP accounts
- Backend: Decrypt CUSTOM_RTMP credentials into destinations on plan create/update
- Android: Add rtmpUrl/streamKey to LinkedAccount entity + shared parcelable (Room v6)
- Android: Add Custom RTMP dialog in AccountsScreen, auto-fill in plan destination picker
- Android: Handle CUSTOM_RTMP accounts in CreatePlanViewModel.loadExistingPlan
- Add local RTMP test server (tools/rtmp-server.js) with auto-ffplay on publish
- Add composition pipeline native code
2026-03-01 10:50:23 +01:00

270 lines
8.5 KiB
C++

#include "egl_context.h"
#include <android/log.h>
#include <android/native_window.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;
}
bool EglContext::CreatePreviewSurface(ANativeWindow* window) {
if (!window || display == EGL_NO_DISPLAY) return false;
DestroyPreviewSurface();
previewSurface = eglCreateWindowSurface(display, config, window, nullptr);
if (previewSurface == EGL_NO_SURFACE) {
LOGE("eglCreateWindowSurface (preview) failed: 0x%x", eglGetError());
return false;
}
previewWindow = window;
eglQuerySurface(display, previewSurface, EGL_WIDTH, &previewWidth);
eglQuerySurface(display, previewSurface, EGL_HEIGHT, &previewHeight);
LOGI("Preview surface created: %dx%d", previewWidth, previewHeight);
return true;
}
void EglContext::DestroyPreviewSurface() {
if (previewSurface != EGL_NO_SURFACE && display != EGL_NO_DISPLAY) {
// Make sure preview isn't current before destroying
eglMakeCurrent(display, surface, surface, context);
eglDestroySurface(display, previewSurface);
previewSurface = EGL_NO_SURFACE;
LOGI("Preview surface destroyed");
}
if (previewWindow) {
ANativeWindow_release(previewWindow);
previewWindow = nullptr;
}
previewWidth = 0;
previewHeight = 0;
}
bool EglContext::MakePreviewCurrent() {
if (previewSurface == EGL_NO_SURFACE) return false;
return eglMakeCurrent(display, previewSurface, previewSurface, context) == EGL_TRUE;
}
bool EglContext::MakeEncoderCurrent() {
return eglMakeCurrent(display, surface, surface, context) == EGL_TRUE;
}
bool EglContext::SwapPreviewBuffers() {
if (previewSurface == EGL_NO_SURFACE) return false;
return eglSwapBuffers(display, previewSurface) == EGL_TRUE;
}
void EglContext::Release() {
if (display != EGL_NO_DISPLAY) {
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
DestroyPreviewSurface();
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");
}