屏幕共享美颜更新

This commit is contained in:
shou 2025-12-04 15:31:05 +08:00
parent e09271a60e
commit 6a1f3db5eb
11 changed files with 157 additions and 18 deletions

3
.gitignore vendored
View File

@ -33,3 +33,6 @@ google-services.json
# Android Profiling # Android Profiling
*.hprof *.hprof
#sdk files
SellyCloudSDK/

View File

@ -3,10 +3,10 @@ plugins {
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
} }
def usePublishedSdk = (findProperty("usePublishedSdk")?.toString()?.toBoolean() ?: false)
def sdkGroupId = rootProject.findProperty("sellySdkGroupId") ?: "com.sellycloud" def sdkGroupId = rootProject.findProperty("sellySdkGroupId") ?: "com.sellycloud"
def sdkArtifactId = rootProject.findProperty("sellySdkArtifactId") ?: "sellycloudsdk" def sdkArtifactId = rootProject.findProperty("sellySdkArtifactId") ?: "sellycloudsdk"
def sdkVersion = rootProject.findProperty("sellySdkVersion") ?: "1.0.0" def sdkVersion = rootProject.findProperty("sellySdkVersion") ?: "1.0.0"
def hasLocalSdk = rootProject.file("SellyCloudSDK").exists()
android { android {
namespace 'com.demo.SellyCloudSDK' namespace 'com.demo.SellyCloudSDK'
@ -63,13 +63,30 @@ android {
} }
dependencies { dependencies {
// SellyCloudSDK if (hasLocalSdk) {
implementation project(':SellyCloudSDK')
} else {
implementation files(
"libs/${sdkArtifactId}-${sdkVersion}.aar",
"libs/ijkplayer-cmake-release.aar",
"libs/Kiwi.aar",
"libs/libwebrtc.aar"
)
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.pedroSG94.RootEncoder:library:2.6.6' implementation 'com.github.pedroSG94.RootEncoder:library:2.6.6'
implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"]) }
implementation fileTree(
dir: "libs",
include: ["*.jar", "*.aar"],
exclude: [
"${sdkArtifactId}-${sdkVersion}.aar",
"ijkplayer-cmake-release.aar",
"Kiwi.aar",
"libwebrtc.aar"
]
)
implementation 'androidx.appcompat:appcompat:1.7.0-alpha03' implementation 'androidx.appcompat:appcompat:1.7.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0-alpha13' implementation 'androidx.constraintlayout:constraintlayout:2.2.0-alpha13'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
@ -81,6 +98,4 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
} }

Binary file not shown.

View File

@ -11,6 +11,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
@ -55,7 +56,7 @@
<service <service
android:name=".interactive.InteractiveForegroundService" android:name=".interactive.InteractiveForegroundService"
android:exported="false" android:exported="false"
android:foregroundServiceType="camera|microphone" /> android:foregroundServiceType="camera|microphone|mediaProjection" />
</application> </application>

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.app.ServiceCompat
import com.demo.SellyCloudSDK.R import com.demo.SellyCloudSDK.R
/** /**
@ -18,7 +19,27 @@ import com.demo.SellyCloudSDK.R
class InteractiveForegroundService : Service() { class InteractiveForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val includeMediaProjection = intent?.getBooleanExtra(EXTRA_MEDIA_PROJECTION, false) == true
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val base = android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA or
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
if (includeMediaProjection) {
base or android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
} else {
base
}
} else 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
buildNotification(),
serviceType
)
} else {
startForeground(NOTIFICATION_ID, buildNotification()) startForeground(NOTIFICATION_ID, buildNotification())
}
return START_STICKY return START_STICKY
} }
@ -61,9 +82,11 @@ class InteractiveForegroundService : Service() {
companion object { companion object {
private const val CHANNEL_ID = "interactive_call_foreground" private const val CHANNEL_ID = "interactive_call_foreground"
private const val NOTIFICATION_ID = 0x101 private const val NOTIFICATION_ID = 0x101
private const val EXTRA_MEDIA_PROJECTION = "extra_media_projection"
fun start(context: Context) { fun start(context: Context, includeMediaProjection: Boolean = false) {
val intent = Intent(context, InteractiveForegroundService::class.java) val intent = Intent(context, InteractiveForegroundService::class.java)
intent.putExtra(EXTRA_MEDIA_PROJECTION, includeMediaProjection)
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }

View File

@ -1,11 +1,14 @@
package com.demo.SellyCloudSDK.interactive package com.demo.SellyCloudSDK.interactive
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.projection.MediaProjectionManager
import android.os.Bundle import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import android.util.Log import android.util.Log
import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -63,6 +66,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
private var pendingJoinRequest: JoinRequest? = null private var pendingJoinRequest: JoinRequest? = null
private var currentCallId: String? = null private var currentCallId: String? = null
@Volatile private var selfUserId: String? = null @Volatile private var selfUserId: String? = null
private var isScreenSharing: Boolean = false
private val permissionLauncher = registerForActivityResult( private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() ActivityResultContracts.RequestMultiplePermissions()
@ -78,6 +82,17 @@ class InteractiveLiveActivity : AppCompatActivity() {
if (!granted) setJoinButtonEnabled(true) if (!granted) setJoinButtonEnabled(true)
} }
private val screenShareLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
startScreenShareInternal(result.resultCode, result.data!!)
} else {
Toast.makeText(this, "已取消屏幕共享授权", Toast.LENGTH_SHORT).show()
updateControlButtons()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityInteractiveLiveBinding.inflate(layoutInflater) binding = ActivityInteractiveLiveBinding.inflate(layoutInflater)
@ -151,7 +166,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
setEventHandler(rtcEventHandler) setEventHandler(rtcEventHandler)
setClientRole(InteractiveRtcEngine.ClientRole.BROADCASTER) setClientRole(InteractiveRtcEngine.ClientRole.BROADCASTER)
// setVideoEncoderConfiguration(InteractiveVideoEncoderConfig()) 使用默认值 // setVideoEncoderConfiguration(InteractiveVideoEncoderConfig()) 使用默认值
setVideoEncoderConfiguration(InteractiveVideoEncoderConfig(640, 480 , fps = 20, minBitrateKbps = 150, maxBitrateKbps = 350)) setVideoEncoderConfiguration(InteractiveVideoEncoderConfig(640, 480 , fps = 20, minBitrateKbps = 150, maxBitrateKbps = 850))
setDefaultAudioRoutetoSpeakerphone(true) setDefaultAudioRoutetoSpeakerphone(true)
setCaptureVideoFrameInterceptor { frame -> setCaptureVideoFrameInterceptor { frame ->
if (!beautyEnabled) return@setCaptureVideoFrameInterceptor frame if (!beautyEnabled) return@setCaptureVideoFrameInterceptor frame
@ -277,6 +292,10 @@ class InteractiveLiveActivity : AppCompatActivity() {
val tip = "onStreamStateChanged[$peerId] state=$state code=$code ${message ?: ""}" val tip = "onStreamStateChanged[$peerId] state=$state code=$code ${message ?: ""}"
Log.d(TAG, tip) Log.d(TAG, tip)
Toast.makeText(this@InteractiveLiveActivity, tip, Toast.LENGTH_SHORT).show() Toast.makeText(this@InteractiveLiveActivity, tip, Toast.LENGTH_SHORT).show()
if (peerId == currentUserId && message?.contains("screen_share_stopped") == true) {
isScreenSharing = false
updateControlButtons()
}
} }
} }
} }
@ -360,6 +379,24 @@ class InteractiveLiveActivity : AppCompatActivity() {
} }
} }
} }
binding.btnScreenShare.setOnClickListener {
if (currentCallId == null) {
Toast.makeText(this, "请先加入频道", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (isScreenSharing) {
stopScreenShareInternal(true)
} else {
// Android 14+ 屏幕捕获需要先开启包含 mediaProjection 类型的前台服务
InteractiveForegroundService.start(this, includeMediaProjection = true)
val mgr = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager
if (mgr == null) {
Toast.makeText(this, "当前设备不支持屏幕捕获", Toast.LENGTH_SHORT).show()
} else {
screenShareLauncher.launch(mgr.createScreenCaptureIntent())
}
}
}
updateControlButtons() updateControlButtons()
} }
@ -390,6 +427,14 @@ class InteractiveLiveActivity : AppCompatActivity() {
} else { } else {
getString(R.string.ctrl_camera_on) getString(R.string.ctrl_camera_on)
} }
binding.btnScreenShare.text = if (isScreenSharing) {
getString(R.string.stop_screen_share)
} else {
getString(R.string.start_screen_share)
}
binding.btnSwitchCamera.isEnabled = !isScreenSharing
binding.btnToggleBeauty.isEnabled = !isScreenSharing
} }
private fun applyLocalPreviewVisibility() { private fun applyLocalPreviewVisibility() {
@ -402,6 +447,36 @@ class InteractiveLiveActivity : AppCompatActivity() {
updateLocalStatsLabel() updateLocalStatsLabel()
} }
private fun startScreenShareInternal(resultCode: Int, data: Intent) {
// Android 14+ 需开启包含 mediaProjection 类型的前台服务
InteractiveForegroundService.start(this, includeMediaProjection = true)
val started = rtcEngine?.startScreenShare(
resultCode,
data,
width = 720,
height = 1280,
fps = 15
) == true
if (started) {
isScreenSharing = true
} else {
Toast.makeText(this, "屏幕共享启动失败", Toast.LENGTH_LONG).show()
}
updateControlButtons()
}
private fun stopScreenShareInternal(showToast: Boolean = false) {
val stopped = rtcEngine?.stopScreenShare() == true
if (stopped) {
isScreenSharing = false
ensureBeautySessionReady()
fuFrameInterceptor?.setEnabled(beautyEnabled)
} else if (showToast) {
Toast.makeText(this, "停止屏幕共享失败", Toast.LENGTH_SHORT).show()
}
updateControlButtons()
}
private fun attemptJoin() { private fun attemptJoin() {
hideKeyboard() hideKeyboard()
val callId = binding.etCallId.text.toString().trim() val callId = binding.etCallId.text.toString().trim()
@ -643,6 +718,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
callDurationSeconds = 0 callDurationSeconds = 0
lastMessage = null lastMessage = null
binding.tvMessageLog.text = getString(R.string.message_none) binding.tvMessageLog.text = getString(R.string.message_none)
isScreenSharing = false
updateControlButtons() updateControlButtons()
updateLocalStatsLabel() updateLocalStatsLabel()
updateCallInfo() updateCallInfo()

View File

@ -222,6 +222,13 @@
android:layout_weight="1" android:layout_weight="1"
android:text="@string/ctrl_mic_off" /> android:text="@string/ctrl_mic_off" />
</LinearLayout> </LinearLayout>
<Button
android:id="@+id/btn_screen_share"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/start_screen_share" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout

View File

@ -39,6 +39,8 @@
<string name="ctrl_mic_on">开启麦克风</string> <string name="ctrl_mic_on">开启麦克风</string>
<string name="ctrl_camera_off">关闭摄像头</string> <string name="ctrl_camera_off">关闭摄像头</string>
<string name="ctrl_camera_on">开启摄像头</string> <string name="ctrl_camera_on">开启摄像头</string>
<string name="start_screen_share">开始屏幕共享</string>
<string name="stop_screen_share">停止屏幕共享</string>
<string name="message_hint">发送频道广播消息</string> <string name="message_hint">发送频道广播消息</string>
<string name="send_message">发送</string> <string name="send_message">发送</string>
<string name="ctrl_beauty_on">美颜开启</string> <string name="ctrl_beauty_on">美颜开启</string>

View File

@ -5,6 +5,10 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
def sdkDir = file('SellyCloudSDK')
def hasLocalSdk = sdkDir.exists()
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories { repositories {
@ -12,11 +16,19 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
// Local AARs for SellyCloudSDK // Local AARs for the demo or the SDK when present
flatDir { dirs file('SellyCloudSDK/libs') } flatDir {
dirs file('example/libs')
if (hasLocalSdk) {
dirs sdkDir.toPath().resolve('libs').toFile()
}
}
} }
} }
rootProject.name = "SellyCLoudSDKExample" rootProject.name = "SellyCLoudSDKExample"
include ':example' include ':example'
// Use the SDK module only when it exists locally; otherwise rely on the published/prebuilt AAR.
if (hasLocalSdk) {
include ':SellyCloudSDK' include ':SellyCloudSDK'
}