Improve streaming camera controls

This commit is contained in:
2026-06-09 23:07:42 +08:00
parent aea5efec56
commit 1d9c5f9f20
6 changed files with 563 additions and 94 deletions
@@ -0,0 +1,183 @@
package com.kimgo.posefit
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import com.kimgo.posefit.sender.CameraFacing
import com.kimgo.posefit.sender.WebRtcSenderClient
import timber.log.Timber
class PosefitStreamingService : Service() {
private var webRtcClient: WebRtcSenderClient? = null
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_STOP -> {
stopStreaming()
stopSelf()
return START_NOT_STICKY
}
ACTION_START -> {
val signalingUrl = intent.getStringExtra(EXTRA_SIGNALING_URL) ?: DEFAULT_SIGNALING_URL
val cameraFacing = intent.getStringExtra(EXTRA_CAMERA_FACING)
?.let { runCatching { CameraFacing.valueOf(it) }.getOrNull() }
?: CameraFacing.BACK
val preferredCameraName = intent.getStringExtra(EXTRA_CAMERA_NAME)
startForegroundNotification(signalingUrl, cameraFacing, preferredCameraName)
startStreaming(signalingUrl, cameraFacing, preferredCameraName)
return START_STICKY
}
else -> {
val signalingUrl = getSavedSignalingUrl()
val cameraFacing = getSavedCameraFacing()
val preferredCameraName = getSavedCameraName()
startForegroundNotification(signalingUrl, cameraFacing, preferredCameraName)
startStreaming(signalingUrl, cameraFacing, preferredCameraName)
return START_STICKY
}
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
stopStreaming()
super.onDestroy()
}
private fun startStreaming(signalingUrl: String, cameraFacing: CameraFacing, preferredCameraName: String?) {
Timber.i(
"Streaming service start: %s, cameraFacing=%s, preferredCameraName=%s, orientation=landscape",
signalingUrl,
cameraFacing,
preferredCameraName,
)
webRtcClient?.release()
webRtcClient = WebRtcSenderClient(
context = applicationContext,
signalingUrl = signalingUrl,
cameraFacing = cameraFacing,
preferredCameraName = preferredCameraName,
).also { it.start() }
isRunning = true
}
private fun stopStreaming() {
Timber.i("Streaming service stop")
webRtcClient?.release()
webRtcClient = null
isRunning = false
}
private fun startForegroundNotification(signalingUrl: String, cameraFacing: CameraFacing, preferredCameraName: String?) {
val notification = buildNotification(signalingUrl, cameraFacing, preferredCameraName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
private fun buildNotification(signalingUrl: String, cameraFacing: CameraFacing, preferredCameraName: String?): Notification {
val cameraText = when (cameraFacing) {
CameraFacing.FRONT -> "前置摄像头"
CameraFacing.BACK -> "后置摄像头"
}
val cameraModeText = preferredCameraName?.let { "$cameraText $it" } ?: "$cameraText 自动最广角"
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, CHANNEL_ID)
.setContentTitle("PoseFit 正在推流")
.setContentText("$cameraModeText 横屏720p -> $signalingUrl")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
} else {
Notification.Builder(this)
.setContentTitle("PoseFit 正在推流")
.setContentText("$cameraModeText 横屏720p -> $signalingUrl")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val channel = NotificationChannel(
CHANNEL_ID,
"PoseFit 推流",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
private fun getSavedSignalingUrl(): String {
val prefs = getSharedPreferences("posefit_prefs", Context.MODE_PRIVATE)
return prefs.getString("signaling_url", DEFAULT_SIGNALING_URL) ?: DEFAULT_SIGNALING_URL
}
private fun getSavedCameraFacing(): CameraFacing {
val prefs = getSharedPreferences("posefit_prefs", Context.MODE_PRIVATE)
val value = prefs.getString("camera_facing", CameraFacing.BACK.name)
return runCatching { CameraFacing.valueOf(value ?: CameraFacing.BACK.name) }
.getOrDefault(CameraFacing.BACK)
}
private fun getSavedCameraName(): String? {
val prefs = getSharedPreferences("posefit_prefs", Context.MODE_PRIVATE)
return prefs.getString("camera_name", null)?.takeIf { it.isNotBlank() }
}
companion object {
private const val CHANNEL_ID = "posefit_streaming"
private const val NOTIFICATION_ID = 1001
private const val ACTION_START = "com.kimgo.posefit.action.START_STREAMING"
private const val ACTION_STOP = "com.kimgo.posefit.action.STOP_STREAMING"
private const val EXTRA_SIGNALING_URL = "extra_signaling_url"
private const val EXTRA_CAMERA_FACING = "extra_camera_facing"
private const val EXTRA_CAMERA_NAME = "extra_camera_name"
private const val DEFAULT_SIGNALING_URL = "ws://192.168.2.6:8765"
@Volatile
var isRunning: Boolean = false
private set
fun startIntent(
context: Context,
signalingUrl: String,
cameraFacing: CameraFacing,
preferredCameraName: String?
): Intent {
return Intent(context, PosefitStreamingService::class.java).apply {
action = ACTION_START
putExtra(EXTRA_SIGNALING_URL, signalingUrl)
putExtra(EXTRA_CAMERA_FACING, cameraFacing.name)
putExtra(EXTRA_CAMERA_NAME, preferredCameraName)
}
}
fun stopIntent(context: Context): Intent {
return Intent(context, PosefitStreamingService::class.java).apply {
action = ACTION_STOP
}
}
}
}