- 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
270 lines
8.5 KiB
C++
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");
|
|
}
|