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 } } } }