Files
posefit-andrid/app/src/main/kotlin/com/kimgo/posefit/PosefitStreamingService.kt
T
wsy182 8c8239502e feat(app): 添加摄像头配置和视频流设置功能
- 在 CameraOption 中新增 supportedResolutions 属性以支持分辨率选择
- 实现 StreamOrientation 和 StreamResolution 数据类用于视频方向和分辨率管理
- 重构 MainActivity 为多页面应用,添加设置页面支持摄像头、分辨率、方向配置
- 集成下拉菜单选择摄像头和视频分辨率功能
- 更新 PosefitStreamingService 支持视频分辨率和方向参数传递
- 移除 AndroidManifest 中 MainActivity 的屏幕方向锁定设置
- 添加详细的视频捕获帧日志记录和可用摄像头格式输出
- 优化视频流启动流程,支持多种分辨率和方向设置
2026-06-10 11:24:47 +08:00

228 lines
9.0 KiB
Kotlin

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.StreamOrientation
import com.kimgo.posefit.sender.StreamResolution
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 settings = StreamSettings(
signalingUrl = intent.getStringExtra(EXTRA_SIGNALING_URL) ?: DEFAULT_SIGNALING_URL,
cameraFacing = intent.getStringExtra(EXTRA_CAMERA_FACING)
?.let { runCatching { CameraFacing.valueOf(it) }.getOrNull() }
?: CameraFacing.BACK,
preferredCameraName = intent.getStringExtra(EXTRA_CAMERA_NAME),
resolution = StreamResolution.from(
intent.getIntExtra(EXTRA_VIDEO_WIDTH, StreamResolution.DEFAULT.width),
intent.getIntExtra(EXTRA_VIDEO_HEIGHT, StreamResolution.DEFAULT.height),
),
orientation = intent.getStringExtra(EXTRA_STREAM_ORIENTATION)
?.let { runCatching { StreamOrientation.valueOf(it) }.getOrNull() }
?: StreamOrientation.DEFAULT,
)
startForegroundNotification(settings)
startStreaming(settings)
return START_STICKY
}
else -> {
val settings = getSavedSettings()
startForegroundNotification(settings)
startStreaming(settings)
return START_STICKY
}
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
stopStreaming()
super.onDestroy()
}
private fun startStreaming(settings: StreamSettings) {
Timber.i(
"Streaming service start: %s, cameraFacing=%s, preferredCameraName=%s, resolution=%s, orientation=%s",
settings.signalingUrl,
settings.cameraFacing,
settings.preferredCameraName,
settings.resolution.label,
settings.orientation,
)
webRtcClient?.release()
webRtcClient = WebRtcSenderClient(
context = applicationContext,
signalingUrl = settings.signalingUrl,
cameraFacing = settings.cameraFacing,
preferredCameraName = settings.preferredCameraName,
videoWidth = settings.resolution.width,
videoHeight = settings.resolution.height,
streamOrientation = settings.orientation,
).also { it.start() }
isRunning = true
}
private fun stopStreaming() {
Timber.i("Streaming service stop")
webRtcClient?.release()
webRtcClient = null
isRunning = false
}
private fun startForegroundNotification(settings: StreamSettings) {
val notification = buildNotification(settings)
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(settings: StreamSettings): Notification {
val cameraText = when (settings.cameraFacing) {
CameraFacing.FRONT -> "前置摄像头"
CameraFacing.BACK -> "后置摄像头"
}
val cameraModeText = settings.preferredCameraName
?.let { "$cameraText $it" }
?: "$cameraText 自动最广角"
val streamText = "${settings.resolution.label} ${settings.orientation.label}"
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, CHANNEL_ID)
.setContentTitle("PoseFit 正在推流")
.setContentText("$cameraModeText $streamText -> ${settings.signalingUrl}")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
} else {
Notification.Builder(this)
.setContentTitle("PoseFit 正在推流")
.setContentText("$cameraModeText $streamText -> ${settings.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 getSavedSettings(): StreamSettings {
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val cameraFacingValue = prefs.getString(KEY_CAMERA_FACING, CameraFacing.BACK.name)
val orientationValue = prefs.getString(KEY_STREAM_ORIENTATION, StreamOrientation.DEFAULT.name)
return StreamSettings(
signalingUrl = prefs.getString(KEY_SIGNALING_URL, DEFAULT_SIGNALING_URL) ?: DEFAULT_SIGNALING_URL,
cameraFacing = runCatching { CameraFacing.valueOf(cameraFacingValue ?: CameraFacing.BACK.name) }
.getOrDefault(CameraFacing.BACK),
preferredCameraName = prefs.getString(KEY_CAMERA_NAME, null)?.takeIf { it.isNotBlank() },
resolution = StreamResolution.from(
prefs.getInt(KEY_VIDEO_WIDTH, StreamResolution.DEFAULT.width),
prefs.getInt(KEY_VIDEO_HEIGHT, StreamResolution.DEFAULT.height),
),
orientation = runCatching { StreamOrientation.valueOf(orientationValue ?: StreamOrientation.DEFAULT.name) }
.getOrDefault(StreamOrientation.DEFAULT),
)
}
private data class StreamSettings(
val signalingUrl: String,
val cameraFacing: CameraFacing,
val preferredCameraName: String?,
val resolution: StreamResolution,
val orientation: StreamOrientation,
)
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 EXTRA_VIDEO_WIDTH = "extra_video_width"
private const val EXTRA_VIDEO_HEIGHT = "extra_video_height"
private const val EXTRA_STREAM_ORIENTATION = "extra_stream_orientation"
private const val DEFAULT_SIGNALING_URL = "ws://192.168.2.6:8765"
const val PREFS_NAME = "posefit_prefs"
const val KEY_SIGNALING_URL = "signaling_url"
const val KEY_CAMERA_FACING = "camera_facing"
const val KEY_CAMERA_NAME = "camera_name"
const val KEY_VIDEO_WIDTH = "video_width"
const val KEY_VIDEO_HEIGHT = "video_height"
const val KEY_STREAM_ORIENTATION = "stream_orientation"
@Volatile
var isRunning: Boolean = false
private set
fun startIntent(
context: Context,
signalingUrl: String,
cameraFacing: CameraFacing,
preferredCameraName: String?,
resolution: StreamResolution,
orientation: StreamOrientation,
): 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)
putExtra(EXTRA_VIDEO_WIDTH, resolution.width)
putExtra(EXTRA_VIDEO_HEIGHT, resolution.height)
putExtra(EXTRA_STREAM_ORIENTATION, orientation.name)
}
}
fun stopIntent(context: Context): Intent {
return Intent(context, PosefitStreamingService::class.java).apply {
action = ACTION_STOP
}
}
}
}