feat: upload latest android source changes

This commit is contained in:
sue
2026-03-03 22:16:30 +08:00
parent c0a7109816
commit 0bf4f72141
56 changed files with 14949 additions and 2152 deletions

View File

@@ -0,0 +1,417 @@
package com.hikoncont.manager
import android.media.projection.MediaProjection
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import com.hikoncont.MediaProjectionHolder
import com.hikoncont.service.AccessibilityRemoteService
import com.pedro.common.ConnectChecker
import com.pedro.common.VideoCodec
import com.pedro.encoder.input.sources.audio.NoAudioSource
import com.pedro.encoder.input.sources.video.ScreenSource
import com.pedro.library.srt.SrtStream
import org.json.JSONObject
import kotlin.math.roundToInt
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class SrtStreamManager(
private val service: AccessibilityRemoteService,
private val listener: Listener
) {
interface Listener {
fun onStateChanged(state: String, message: String, extra: JSONObject? = null)
}
data class StartResult(
val success: Boolean,
val message: String,
val extra: JSONObject? = null
)
companion object {
private const val TAG = "SrtStreamManager"
private const val MIN_FPS = 20
private const val MAX_FPS = 60
private const val MIN_LONG_EDGE = 360
private const val MAX_LONG_EDGE = 1920
private const val MIN_BITRATE_BPS = 500_000
private const val MAX_BITRATE_BPS = 12_000_000
}
private enum class StreamPriority(val wireValue: String) {
SMOOTH("smooth"),
BALANCED("balanced"),
QUALITY("quality");
companion object {
fun fromRaw(raw: String?): StreamPriority {
return when (raw?.trim()?.lowercase()) {
QUALITY.wireValue -> QUALITY
BALANCED.wireValue -> BALANCED
else -> SMOOTH
}
}
}
}
private data class Session(
val deviceId: String,
val clientId: String,
val ingestUrl: String,
val width: Int,
val height: Int,
val fps: Int,
val priority: String,
val maxLongEdge: Int,
val bitrateBps: Int,
val startedAt: Long
)
private val stateLock = Any()
private var stream: SrtStream? = null
private var session: Session? = null
private var lastBitrateBps = 0L
fun isRuntimeSupported(): Boolean {
// Dependency exists at compile-time; runtime check is kept explicit for clarity.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
}
fun isStreaming(): Boolean = synchronized(stateLock) {
stream?.isStreaming == true
}
fun start(
deviceId: String,
clientId: String,
ingestUrl: String,
requestedFps: Int? = null,
requestedMaxLongEdge: Int? = null,
requestedBitrateKbps: Int? = null,
priorityRaw: String? = null
): StartResult {
if (!isRuntimeSupported()) {
return StartResult(false, "srt_runtime_not_supported")
}
if (ingestUrl.isBlank() || !ingestUrl.startsWith("srt://", ignoreCase = true)) {
return StartResult(false, "invalid_srt_ingest_url")
}
val projection = MediaProjectionHolder.getMediaProjection()
?: return StartResult(false, "media_projection_not_ready")
val screenConfig = buildScreenConfig(
requestedFps = requestedFps,
requestedMaxLongEdge = requestedMaxLongEdge,
requestedBitrateKbps = requestedBitrateKbps,
priorityRaw = priorityRaw
)
val preparedExtra = JSONObject().apply {
put("width", screenConfig.width)
put("height", screenConfig.height)
put("fps", screenConfig.fps)
put("bitrateBps", screenConfig.bitrateBps)
put("priority", screenConfig.priority)
put("maxLongEdge", screenConfig.maxLongEdge)
if (requestedMaxLongEdge != null) put("requestedMaxLongEdge", requestedMaxLongEdge)
if (requestedBitrateKbps != null) put("requestedBitrateKbps", requestedBitrateKbps)
put("ingestUrl", ingestUrl)
}
synchronized(stateLock) {
stopInternalLocked()
val checker = createConnectChecker()
val newStream = try {
createStream(projection, checker)
} catch (e: Exception) {
Log.e(TAG, "Create SRT stream failed", e)
return StartResult(false, "srt_stream_create_failed:${e.message ?: "unknown"}")
}
val videoPrepared = try {
newStream.prepareVideo(
screenConfig.width,
screenConfig.height,
screenConfig.bitrateBps,
screenConfig.fps,
1,
0
)
} catch (e: Exception) {
Log.e(TAG, "Prepare SRT video failed", e)
false
}
if (!videoPrepared) {
try {
newStream.release()
} catch (_: Exception) {
}
return StartResult(false, "srt_prepare_video_failed", preparedExtra)
}
// Keep encoder pipeline initialized even when sending video-only.
val audioPrepared = try {
newStream.prepareAudio(32_000, false, 32 * 1000)
} catch (e: Exception) {
Log.e(TAG, "Prepare SRT audio failed", e)
false
}
if (!audioPrepared) {
try {
newStream.release()
} catch (_: Exception) {
}
return StartResult(false, "srt_prepare_audio_failed", preparedExtra)
}
stream = newStream
session = Session(
deviceId = deviceId,
clientId = clientId,
ingestUrl = ingestUrl,
width = screenConfig.width,
height = screenConfig.height,
fps = screenConfig.fps,
priority = screenConfig.priority,
maxLongEdge = screenConfig.maxLongEdge,
bitrateBps = screenConfig.bitrateBps,
startedAt = System.currentTimeMillis()
)
lastBitrateBps = 0L
listener.onStateChanged("starting", "srt_connecting", preparedExtra)
try {
newStream.startStream(ingestUrl)
} catch (e: Exception) {
Log.e(TAG, "Start SRT stream failed", e)
stopInternalLocked()
return StartResult(
success = false,
message = "srt_start_failed:${e.message ?: "unknown"}",
extra = preparedExtra
)
}
}
return StartResult(true, "srt_starting", preparedExtra)
}
fun stop(reason: String = "remote_stop"): Boolean {
val hasSession = synchronized(stateLock) {
val exists = stream != null || session != null
stopInternalLocked()
exists
}
if (hasSession) {
listener.onStateChanged(
state = "stopped",
message = "srt_stopped",
extra = JSONObject().apply { put("reason", reason) }
)
}
return hasSession
}
fun release() {
synchronized(stateLock) {
stopInternalLocked()
}
}
private data class ScreenConfig(
val width: Int,
val height: Int,
val fps: Int,
val priority: String,
val maxLongEdge: Int,
val bitrateBps: Int
)
private fun buildScreenConfig(
requestedFps: Int?,
requestedMaxLongEdge: Int?,
requestedBitrateKbps: Int?,
priorityRaw: String?
): ScreenConfig {
val metrics = service.resources.displayMetrics
val rawWidth = metrics.widthPixels.coerceAtLeast(2)
val rawHeight = metrics.heightPixels.coerceAtLeast(2)
val priority = StreamPriority.fromRaw(priorityRaw)
val defaultFps = when (priority) {
StreamPriority.SMOOTH -> 40
StreamPriority.BALANCED -> 45
StreamPriority.QUALITY -> 50
}
val defaultLongEdge = when (priority) {
StreamPriority.SMOOTH -> 960
StreamPriority.BALANCED -> 1152
StreamPriority.QUALITY -> 1280
}
val bitrateFactor = when (priority) {
StreamPriority.SMOOTH -> 0.055
StreamPriority.BALANCED -> 0.075
StreamPriority.QUALITY -> 0.10
}
val profileMinBitrate = when (priority) {
StreamPriority.SMOOTH -> 700_000
StreamPriority.BALANCED -> 1_000_000
StreamPriority.QUALITY -> 1_300_000
}
val profileMaxBitrate = when (priority) {
StreamPriority.SMOOTH -> 4_000_000
StreamPriority.BALANCED -> 7_000_000
StreamPriority.QUALITY -> MAX_BITRATE_BPS
}
val fps = (requestedFps ?: defaultFps).coerceIn(MIN_FPS, MAX_FPS)
val maxLongEdge = (requestedMaxLongEdge ?: defaultLongEdge).coerceIn(MIN_LONG_EDGE, MAX_LONG_EDGE)
val sourceLong = maxOf(rawWidth, rawHeight)
val sourceShort = minOf(rawWidth, rawHeight)
val scale = if (sourceLong > maxLongEdge) {
maxLongEdge.toFloat() / sourceLong.toFloat()
} else {
1f
}
val targetLong = ((sourceLong * scale).roundToInt()).coerceAtLeast(2)
val targetShort = ((sourceShort * scale).roundToInt()).coerceAtLeast(2)
val width = if (rawWidth >= rawHeight) toEven(targetLong) else toEven(targetShort)
val height = if (rawWidth >= rawHeight) toEven(targetShort) else toEven(targetLong)
val requestedBitrateBps = requestedBitrateKbps
?.takeIf { it > 0 }
?.let { it * 1000 }
val bitrateEstimate = (width.toLong() * height.toLong() * fps.toLong() * bitrateFactor).toLong()
.coerceAtLeast(MIN_BITRATE_BPS.toLong())
.coerceAtMost(MAX_BITRATE_BPS.toLong())
.toInt()
val bitrateBps = (requestedBitrateBps ?: bitrateEstimate)
.coerceIn(profileMinBitrate, profileMaxBitrate)
.coerceIn(MIN_BITRATE_BPS, MAX_BITRATE_BPS)
return ScreenConfig(
width = width,
height = height,
fps = fps,
priority = priority.wireValue,
maxLongEdge = maxLongEdge,
bitrateBps = bitrateBps
)
}
private fun toEven(value: Int): Int {
val safe = value.coerceAtLeast(2)
return if (safe % 2 == 0) safe else safe - 1
}
private fun createStream(projection: MediaProjection, checker: ConnectChecker): SrtStream {
val screenSource = ScreenSource(service.applicationContext, projection)
val audioSource = NoAudioSource()
return SrtStream(service.applicationContext, checker, screenSource, audioSource).apply {
setVideoCodec(VideoCodec.H264)
val client = getStreamClient()
client.setReTries(2)
client.setOnlyVideo(true)
client.setLogs(false)
}
}
private fun createConnectChecker(): ConnectChecker {
return object : ConnectChecker {
override fun onConnectionStarted(url: String) {
listener.onStateChanged(
state = "starting",
message = "srt_connection_started",
extra = JSONObject().apply { put("url", url) }
)
}
override fun onConnectionSuccess() {
val current = synchronized(stateLock) { session }
listener.onStateChanged(
state = "running",
message = "srt_connection_success",
extra = JSONObject().apply {
put("bitrateBps", lastBitrateBps)
put("width", current?.width ?: 0)
put("height", current?.height ?: 0)
put("fps", current?.fps ?: 0)
put("priority", current?.priority ?: StreamPriority.SMOOTH.wireValue)
put("maxLongEdge", current?.maxLongEdge ?: 0)
put("startedAt", current?.startedAt ?: 0)
}
)
}
override fun onConnectionFailed(reason: String) {
Log.w(TAG, "SRT connection failed: $reason")
synchronized(stateLock) {
stopInternalLocked()
}
listener.onStateChanged(
state = "failed",
message = "srt_connection_failed:$reason"
)
}
override fun onDisconnect() {
val hadSession = synchronized(stateLock) {
val active = stream != null || session != null
stopInternalLocked()
active
}
if (hadSession) {
listener.onStateChanged(
state = "stopped",
message = "srt_disconnected"
)
}
}
override fun onAuthError() {
listener.onStateChanged("failed", "srt_auth_error")
}
override fun onAuthSuccess() {
listener.onStateChanged("running", "srt_auth_success")
}
override fun onNewBitrate(bitrate: Long) {
lastBitrateBps = bitrate
}
}
}
private fun stopInternalLocked() {
val old = stream
stream = null
session = null
if (old != null) {
try {
if (old.isStreaming) {
old.stopStream()
}
} catch (e: Exception) {
Log.w(TAG, "Stop SRT stream failed", e)
}
try {
old.release()
} catch (e: Exception) {
Log.w(TAG, "Release SRT stream failed", e)
}
}
}
}