feat(app): 添加摄像头配置和视频流设置功能

- 在 CameraOption 中新增 supportedResolutions 属性以支持分辨率选择
- 实现 StreamOrientation 和 StreamResolution 数据类用于视频方向和分辨率管理
- 重构 MainActivity 为多页面应用,添加设置页面支持摄像头、分辨率、方向配置
- 集成下拉菜单选择摄像头和视频分辨率功能
- 更新 PosefitStreamingService 支持视频分辨率和方向参数传递
- 移除 AndroidManifest 中 MainActivity 的屏幕方向锁定设置
- 添加详细的视频捕获帧日志记录和可用摄像头格式输出
- 优化视频流启动流程,支持多种分辨率和方向设置
This commit is contained in:
2026-06-10 11:24:47 +08:00
parent 1d9c5f9f20
commit 8c8239502e
8 changed files with 624 additions and 218 deletions
@@ -10,6 +10,8 @@ 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
@@ -29,23 +31,31 @@ class PosefitStreamingService : Service() {
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)
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 signalingUrl = getSavedSignalingUrl()
val cameraFacing = getSavedCameraFacing()
val preferredCameraName = getSavedCameraName()
startForegroundNotification(signalingUrl, cameraFacing, preferredCameraName)
startStreaming(signalingUrl, cameraFacing, preferredCameraName)
val settings = getSavedSettings()
startForegroundNotification(settings)
startStreaming(settings)
return START_STICKY
}
}
@@ -58,19 +68,24 @@ class PosefitStreamingService : Service() {
super.onDestroy()
}
private fun startStreaming(signalingUrl: String, cameraFacing: CameraFacing, preferredCameraName: String?) {
private fun startStreaming(settings: StreamSettings) {
Timber.i(
"Streaming service start: %s, cameraFacing=%s, preferredCameraName=%s, orientation=landscape",
signalingUrl,
cameraFacing,
preferredCameraName,
"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 = signalingUrl,
cameraFacing = cameraFacing,
preferredCameraName = preferredCameraName,
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
}
@@ -82,8 +97,8 @@ class PosefitStreamingService : Service() {
isRunning = false
}
private fun startForegroundNotification(signalingUrl: String, cameraFacing: CameraFacing, preferredCameraName: String?) {
val notification = buildNotification(signalingUrl, cameraFacing, preferredCameraName)
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 {
@@ -91,24 +106,27 @@ class PosefitStreamingService : Service() {
}
}
private fun buildNotification(signalingUrl: String, cameraFacing: CameraFacing, preferredCameraName: String?): Notification {
val cameraText = when (cameraFacing) {
private fun buildNotification(settings: StreamSettings): Notification {
val cameraText = when (settings.cameraFacing) {
CameraFacing.FRONT -> "前置摄像头"
CameraFacing.BACK -> "后置摄像头"
}
val cameraModeText = preferredCameraName?.let { "$cameraText $it" } ?: "$cameraText 自动最广角"
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 横屏720p -> $signalingUrl")
.setContentText("$cameraModeText $streamText -> ${settings.signalingUrl}")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
} else {
Notification.Builder(this)
.setContentTitle("PoseFit 正在推流")
.setContentText("$cameraModeText 横屏720p -> $signalingUrl")
.setContentText("$cameraModeText $streamText -> ${settings.signalingUrl}")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
@@ -123,28 +141,38 @@ class PosefitStreamingService : Service() {
val channel = NotificationChannel(
CHANNEL_ID,
"PoseFit 推流",
NotificationManager.IMPORTANCE_LOW
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 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 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() }
}
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"
@@ -154,8 +182,19 @@ class PosefitStreamingService : Service() {
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
@@ -164,13 +203,18 @@ class PosefitStreamingService : Service() {
context: Context,
signalingUrl: String,
cameraFacing: CameraFacing,
preferredCameraName: String?
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)
}
}