8c8239502e
- 在 CameraOption 中新增 supportedResolutions 属性以支持分辨率选择 - 实现 StreamOrientation 和 StreamResolution 数据类用于视频方向和分辨率管理 - 重构 MainActivity 为多页面应用,添加设置页面支持摄像头、分辨率、方向配置 - 集成下拉菜单选择摄像头和视频分辨率功能 - 更新 PosefitStreamingService 支持视频分辨率和方向参数传递 - 移除 AndroidManifest 中 MainActivity 的屏幕方向锁定设置 - 添加详细的视频捕获帧日志记录和可用摄像头格式输出 - 优化视频流启动流程,支持多种分辨率和方向设置
228 lines
9.0 KiB
Kotlin
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
|
|
}
|
|
}
|
|
}
|
|
}
|