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
This commit is contained in:
2026-03-01 10:50:23 +01:00
parent c1ff5351b7
commit c632e22033
35 changed files with 2822 additions and 98 deletions

View File

@@ -1,6 +1,7 @@
#include "egl_context.h"
#include <android/log.h>
#include <android/native_window.h>
#include <unistd.h>
#define TAG "LckEglContext"
@@ -201,9 +202,58 @@ 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;