屏幕共享美颜更新
This commit is contained in:
parent
e09271a60e
commit
6a1f3db5eb
|
|
@ -33,3 +33,6 @@ google-services.json
|
|||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
#sdk files
|
||||
SellyCloudSDK/
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ plugins {
|
|||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
def usePublishedSdk = (findProperty("usePublishedSdk")?.toString()?.toBoolean() ?: false)
|
||||
def sdkGroupId = rootProject.findProperty("sellySdkGroupId") ?: "com.sellycloud"
|
||||
def sdkArtifactId = rootProject.findProperty("sellySdkArtifactId") ?: "sellycloudsdk"
|
||||
def sdkVersion = rootProject.findProperty("sellySdkVersion") ?: "1.0.0"
|
||||
def hasLocalSdk = rootProject.file("SellyCloudSDK").exists()
|
||||
|
||||
android {
|
||||
namespace 'com.demo.SellyCloudSDK'
|
||||
|
|
@ -63,13 +63,30 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// SellyCloudSDK 需要的依赖(需要手动添加)
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'com.github.pedroSG94.RootEncoder:library:2.6.6'
|
||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||
|
||||
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.github.pedroSG94.RootEncoder:library:2.6.6'
|
||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
}
|
||||
|
||||
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.constraintlayout:constraintlayout:2.2.0-alpha13'
|
||||
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.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -11,6 +11,7 @@
|
|||
<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_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.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
|
@ -55,7 +56,7 @@
|
|||
<service
|
||||
android:name=".interactive.InteractiveForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="camera|microphone" />
|
||||
android:foregroundServiceType="camera|microphone|mediaProjection" />
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ import android.content.Intent
|
|||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import com.demo.SellyCloudSDK.R
|
||||
|
||||
/**
|
||||
|
|
@ -18,7 +19,27 @@ import com.demo.SellyCloudSDK.R
|
|||
class InteractiveForegroundService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
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())
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
|
|
@ -61,9 +82,11 @@ class InteractiveForegroundService : Service() {
|
|||
companion object {
|
||||
private const val CHANNEL_ID = "interactive_call_foreground"
|
||||
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)
|
||||
intent.putExtra(EXTRA_MEDIA_PROJECTION, includeMediaProjection)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
package com.demo.SellyCloudSDK.interactive
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.util.Log
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
|
@ -63,6 +66,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
private var pendingJoinRequest: JoinRequest? = null
|
||||
private var currentCallId: String? = null
|
||||
@Volatile private var selfUserId: String? = null
|
||||
private var isScreenSharing: Boolean = false
|
||||
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
|
|
@ -78,6 +82,17 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityInteractiveLiveBinding.inflate(layoutInflater)
|
||||
|
|
@ -151,7 +166,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
setEventHandler(rtcEventHandler)
|
||||
setClientRole(InteractiveRtcEngine.ClientRole.BROADCASTER)
|
||||
// setVideoEncoderConfiguration(InteractiveVideoEncoderConfig()) 使用默认值
|
||||
setVideoEncoderConfiguration(InteractiveVideoEncoderConfig(640, 480 , fps = 20, minBitrateKbps = 150, maxBitrateKbps = 350))
|
||||
setVideoEncoderConfiguration(InteractiveVideoEncoderConfig(640, 480 , fps = 20, minBitrateKbps = 150, maxBitrateKbps = 850))
|
||||
setDefaultAudioRoutetoSpeakerphone(true)
|
||||
setCaptureVideoFrameInterceptor { frame ->
|
||||
if (!beautyEnabled) return@setCaptureVideoFrameInterceptor frame
|
||||
|
|
@ -277,6 +292,10 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
val tip = "onStreamStateChanged[$peerId] state=$state code=$code ${message ?: ""}"
|
||||
Log.d(TAG, tip)
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
@ -390,6 +427,14 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
} else {
|
||||
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() {
|
||||
|
|
@ -402,6 +447,36 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
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() {
|
||||
hideKeyboard()
|
||||
val callId = binding.etCallId.text.toString().trim()
|
||||
|
|
@ -643,6 +718,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
|
|||
callDurationSeconds = 0
|
||||
lastMessage = null
|
||||
binding.tvMessageLog.text = getString(R.string.message_none)
|
||||
isScreenSharing = false
|
||||
updateControlButtons()
|
||||
updateLocalStatsLabel()
|
||||
updateCallInfo()
|
||||
|
|
|
|||
|
|
@ -222,6 +222,13 @@
|
|||
android:layout_weight="1"
|
||||
android:text="@string/ctrl_mic_off" />
|
||||
</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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@
|
|||
<string name="ctrl_mic_on">开启麦克风</string>
|
||||
<string name="ctrl_camera_off">关闭摄像头</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="send_message">发送</string>
|
||||
<string name="ctrl_beauty_on">美颜开启</string>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ pluginManagement {
|
|||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
def sdkDir = file('SellyCloudSDK')
|
||||
def hasLocalSdk = sdkDir.exists()
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||
repositories {
|
||||
|
|
@ -12,11 +16,19 @@ dependencyResolutionManagement {
|
|||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
// Local AARs for SellyCloudSDK
|
||||
flatDir { dirs file('SellyCloudSDK/libs') }
|
||||
// Local AARs for the demo or the SDK when present
|
||||
flatDir {
|
||||
dirs file('example/libs')
|
||||
if (hasLocalSdk) {
|
||||
dirs sdkDir.toPath().resolve('libs').toFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "SellyCLoudSDKExample"
|
||||
include ':example'
|
||||
include ':SellyCloudSDK'
|
||||
// Use the SDK module only when it exists locally; otherwise rely on the published/prebuilt AAR.
|
||||
if (hasLocalSdk) {
|
||||
include ':SellyCloudSDK'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue