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:
@@ -2,6 +2,7 @@ package com.omixlab.lckcontrol.streaming
|
||||
|
||||
import android.hardware.HardwareBuffer
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
|
||||
/**
|
||||
* Thin JNI wrapper around the C++ StreamingEngine.
|
||||
@@ -77,6 +78,52 @@ class NativeStreamingEngine {
|
||||
return nativeIsRunning(nativePtr)
|
||||
}
|
||||
|
||||
// Preview surface
|
||||
fun setPreviewSurface(surface: Surface) {
|
||||
if (nativePtr == 0L) return
|
||||
nativeSetPreviewSurface(nativePtr, surface)
|
||||
}
|
||||
|
||||
fun removePreviewSurface() {
|
||||
if (nativePtr == 0L) return
|
||||
nativeRemovePreviewSurface(nativePtr)
|
||||
}
|
||||
|
||||
// Composition layers
|
||||
fun addCompositionLayer(
|
||||
rgbaData: ByteArray, w: Int, h: Int,
|
||||
posX: Float, posY: Float, scaleX: Float, scaleY: Float,
|
||||
rotation: Float, opacity: Float, zOrder: Int, tag: String,
|
||||
): Int {
|
||||
if (nativePtr == 0L) return -1
|
||||
return nativeAddCompositionLayer(nativePtr, rgbaData, w, h,
|
||||
posX, posY, scaleX, scaleY, rotation, opacity, zOrder, tag)
|
||||
}
|
||||
|
||||
fun removeCompositionLayer(layerId: Int) {
|
||||
if (nativePtr == 0L) return
|
||||
nativeRemoveCompositionLayer(nativePtr, layerId)
|
||||
}
|
||||
|
||||
fun updateCompositionLayerTransform(
|
||||
layerId: Int, posX: Float, posY: Float,
|
||||
scaleX: Float, scaleY: Float, rotation: Float,
|
||||
) {
|
||||
if (nativePtr == 0L) return
|
||||
nativeUpdateCompositionLayerTransform(nativePtr, layerId,
|
||||
posX, posY, scaleX, scaleY, rotation)
|
||||
}
|
||||
|
||||
fun updateCompositionLayerOpacity(layerId: Int, opacity: Float) {
|
||||
if (nativePtr == 0L) return
|
||||
nativeUpdateCompositionLayerOpacity(nativePtr, layerId, opacity)
|
||||
}
|
||||
|
||||
fun setCompositionLayerEnabled(layerId: Int, enabled: Boolean) {
|
||||
if (nativePtr == 0L) return
|
||||
nativeSetCompositionLayerEnabled(nativePtr, layerId, enabled)
|
||||
}
|
||||
|
||||
// Called from native code (JNI callbacks)
|
||||
@Suppress("unused")
|
||||
private fun onNativeStats(videoBitrate: Long, audioBitrate: Long, fps: Int, droppedFrames: Int) {
|
||||
@@ -109,4 +156,22 @@ class NativeStreamingEngine {
|
||||
private external fun nativeStop(ptr: Long)
|
||||
private external fun nativeDestroy(ptr: Long)
|
||||
private external fun nativeIsRunning(ptr: Long): Boolean
|
||||
|
||||
// Preview surface
|
||||
private external fun nativeSetPreviewSurface(ptr: Long, surface: Surface)
|
||||
private external fun nativeRemovePreviewSurface(ptr: Long)
|
||||
|
||||
// Composition layers
|
||||
private external fun nativeAddCompositionLayer(
|
||||
ptr: Long, rgbaData: ByteArray, w: Int, h: Int,
|
||||
posX: Float, posY: Float, scaleX: Float, scaleY: Float,
|
||||
rotation: Float, opacity: Float, zOrder: Int, tag: String,
|
||||
): Int
|
||||
private external fun nativeRemoveCompositionLayer(ptr: Long, layerId: Int)
|
||||
private external fun nativeUpdateCompositionLayerTransform(
|
||||
ptr: Long, layerId: Int, posX: Float, posY: Float,
|
||||
scaleX: Float, scaleY: Float, rotation: Float,
|
||||
)
|
||||
private external fun nativeUpdateCompositionLayerOpacity(ptr: Long, layerId: Int, opacity: Float)
|
||||
private external fun nativeSetCompositionLayerEnabled(ptr: Long, layerId: Int, enabled: Boolean)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.omixlab.lckcontrol.streaming
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.hardware.HardwareBuffer
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import com.omixlab.lckcontrol.shared.StreamPlan
|
||||
import com.omixlab.lckcontrol.shared.StreamingConfig
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import java.nio.ByteBuffer
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -153,4 +156,61 @@ class StreamingManager @Inject constructor() {
|
||||
}
|
||||
|
||||
fun isStreaming(): Boolean = _state.value == StreamingState.LIVE
|
||||
|
||||
// --- Preview surface ---
|
||||
|
||||
fun setPreviewSurface(surface: Surface) {
|
||||
engine?.setPreviewSurface(surface)
|
||||
}
|
||||
|
||||
fun removePreviewSurface() {
|
||||
engine?.removePreviewSurface()
|
||||
}
|
||||
|
||||
// --- Composition layers ---
|
||||
|
||||
fun addCompositionLayer(
|
||||
bitmap: Bitmap,
|
||||
posX: Float, posY: Float,
|
||||
scaleX: Float, scaleY: Float,
|
||||
rotation: Float, opacity: Float,
|
||||
zOrder: Int, tag: String,
|
||||
): Int {
|
||||
val rgba = bitmapToRgba(bitmap)
|
||||
return engine?.addCompositionLayer(
|
||||
rgba, bitmap.width, bitmap.height,
|
||||
posX, posY, scaleX, scaleY, rotation, opacity, zOrder, tag,
|
||||
) ?: -1
|
||||
}
|
||||
|
||||
fun removeCompositionLayer(layerId: Int) {
|
||||
engine?.removeCompositionLayer(layerId)
|
||||
}
|
||||
|
||||
fun updateCompositionLayerTransform(
|
||||
layerId: Int, posX: Float, posY: Float,
|
||||
scaleX: Float, scaleY: Float, rotation: Float,
|
||||
) {
|
||||
engine?.updateCompositionLayerTransform(layerId, posX, posY, scaleX, scaleY, rotation)
|
||||
}
|
||||
|
||||
fun updateCompositionLayerOpacity(layerId: Int, opacity: Float) {
|
||||
engine?.updateCompositionLayerOpacity(layerId, opacity)
|
||||
}
|
||||
|
||||
fun setCompositionLayerEnabled(layerId: Int, enabled: Boolean) {
|
||||
engine?.setCompositionLayerEnabled(layerId, enabled)
|
||||
}
|
||||
|
||||
private fun bitmapToRgba(bitmap: Bitmap): ByteArray {
|
||||
val argbBitmap = if (bitmap.config != Bitmap.Config.ARGB_8888) {
|
||||
bitmap.copy(Bitmap.Config.ARGB_8888, false)
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
val buffer = ByteBuffer.allocate(argbBitmap.byteCount)
|
||||
argbBitmap.copyPixelsToBuffer(buffer)
|
||||
if (argbBitmap !== bitmap) argbBitmap.recycle()
|
||||
return buffer.array()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user