直播模块重构及demo更新

This commit is contained in:
2026-01-07 11:06:35 +08:00
parent aca9365c5b
commit 5640d2446d
117 changed files with 3810 additions and 2050 deletions

3
.gitignore vendored
View File

@@ -36,3 +36,6 @@ google-services.json
#sdk files #sdk files
SellyCloudSDK/ SellyCloudSDK/
.gradle-user-home/
pic/

View File

@@ -7,6 +7,8 @@ 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() def hasLocalSdk = rootProject.file("SellyCloudSDK").exists()
def releaseStorePath = project.rootProject.file(findProperty("MY_STORE_FILE") ?: "release.keystore")
def hasReleaseKeystore = releaseStorePath != null && releaseStorePath.exists()
android { android {
namespace 'com.demo.SellyCloudSDK' namespace 'com.demo.SellyCloudSDK'
@@ -28,17 +30,14 @@ android {
signingConfigs { signingConfigs {
release { release {
def storePath = project.rootProject.file(findProperty("MY_STORE_FILE") ?: "") if (hasReleaseKeystore) {
if (storePath != null && storePath.exists()) { storeFile releaseStorePath
storeFile storePath storePassword findProperty("MY_STORE_PASSWORD") ?: ""
} else { keyAlias findProperty("MY_KEY_ALIAS") ?: ""
storeFile project.rootProject.file(findProperty("MY_STORE_FILE") ?: "release.keystore") keyPassword findProperty("MY_KEY_PASSWORD") ?: ""
v1SigningEnabled true
v2SigningEnabled true
} }
storePassword findProperty("MY_STORE_PASSWORD") ?: ""
keyAlias findProperty("MY_KEY_ALIAS") ?: ""
keyPassword findProperty("MY_KEY_PASSWORD") ?: ""
v1SigningEnabled true
v2SigningEnabled true
} }
} }
@@ -47,7 +46,12 @@ android {
shrinkResources false shrinkResources false
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release if (hasReleaseKeystore) {
signingConfig signingConfigs.release
} else {
// Allow local CI/dev builds without a private keystore.
signingConfig signingConfigs.debug
}
} }
} }
compileOptions { compileOptions {
@@ -74,7 +78,6 @@ dependencies {
) )
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 fileTree( implementation fileTree(
@@ -92,10 +95,18 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.activity:activity-ktx:1.9.2' implementation 'androidx.activity:activity-ktx:1.9.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
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'
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation "io.coil-kt:coil:2.6.0"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:runner:1.5.2'
} }

Binary file not shown.

View File

@@ -19,15 +19,19 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:label="SellyCloudRTC Demo" android:label="SellyCloudRTC Demo"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".FeatureHubActivity" android:name=".login.LoginActivity"
android:exported="true" android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullSensor"> android:theme="@style/Theme.AVDemo.NoActionBar"
android:screenOrientation="fullSensor"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -35,10 +39,26 @@
</activity> </activity>
<activity <activity
android:name=".live.MainActivity" android:name=".FeatureHubActivity"
android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
android:theme="@style/Theme.AVDemo.NoActionBar"
android:screenOrientation="fullSensor" />
<activity
android:name=".live.LivePushActivity"
android:exported="false" android:exported="false"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullSensor" android:screenOrientation="fullSensor"
android:theme="@style/Theme.AVDemo.NoActionBar"
android:parentActivityName=".FeatureHubActivity" />
<activity
android:name=".live.LivePlayActivity"
android:exported="false"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
android:screenOrientation="fullSensor"
android:theme="@style/Theme.AVDemo.NoActionBar"
android:parentActivityName=".FeatureHubActivity" /> android:parentActivityName=".FeatureHubActivity" />
<activity <activity
@@ -48,11 +68,6 @@
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:parentActivityName=".FeatureHubActivity" /> android:parentActivityName=".FeatureHubActivity" />
<!-- 新增:多路播放页面 -->
<activity
android:name=".live.MultiPlayActivity"
android:exported="false" />
<service <service
android:name=".interactive.InteractiveForegroundService" android:name=".interactive.InteractiveForegroundService"
android:exported="false" android:exported="false"

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

View File

@@ -1,11 +1,44 @@
package com.demo.SellyCloudSDK package com.demo.SellyCloudSDK
import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.demo.SellyCloudSDK.avdemo.AvDemoSettings
import com.demo.SellyCloudSDK.avdemo.AvDemoSettingsStore
import com.demo.SellyCloudSDK.databinding.ActivityFeatureHubBinding import com.demo.SellyCloudSDK.databinding.ActivityFeatureHubBinding
import com.demo.SellyCloudSDK.databinding.DialogLivePresetSettingsBinding
import com.demo.SellyCloudSDK.databinding.DialogPlayConfigOverlayBinding
import com.demo.SellyCloudSDK.databinding.DialogPushProtocolSheetBinding
import com.demo.SellyCloudSDK.interactive.InteractiveLiveActivity import com.demo.SellyCloudSDK.interactive.InteractiveLiveActivity
import com.demo.SellyCloudSDK.live.MainActivity import com.demo.SellyCloudSDK.live.LivePlayActivity
import com.demo.SellyCloudSDK.live.LivePushActivity
import com.demo.SellyCloudSDK.live.env.LiveEnvSettings
import com.demo.SellyCloudSDK.live.env.LiveEnvSettingsStore
import com.demo.SellyCloudSDK.live.env.normalizedAppName
import com.demo.SellyCloudSDK.live.env.normalizedVhost
import com.demo.SellyCloudSDK.live.square.AliveListRepository
import com.demo.SellyCloudSDK.live.square.AliveListResult
import com.demo.SellyCloudSDK.live.square.AliveStreamAdapter
import com.demo.SellyCloudSDK.live.square.AliveStreamItem
import com.demo.SellyCloudSDK.login.DemoLoginStore
import com.demo.SellyCloudSDK.login.LoginActivity
import com.sellycloud.sellycloudsdk.SellyLiveMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.random.Random
/** /**
* Entry screen displaying available demo experiences. * Entry screen displaying available demo experiences.
@@ -13,20 +46,471 @@ import com.demo.SellyCloudSDK.live.MainActivity
class FeatureHubActivity : AppCompatActivity() { class FeatureHubActivity : AppCompatActivity() {
private lateinit var binding: ActivityFeatureHubBinding private lateinit var binding: ActivityFeatureHubBinding
private lateinit var settingsStore: AvDemoSettingsStore
private lateinit var envStore: LiveEnvSettingsStore
private lateinit var loginStore: DemoLoginStore
private val uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private lateinit var aliveAdapter: AliveStreamAdapter
private val allAliveItems: MutableList<AliveStreamItem> = mutableListOf()
private var currentPage = 0
private var isPaging = false
private var selectedTab: Tab = Tab.HOME
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
loginStore = DemoLoginStore(this)
if (!loginStore.isLoggedIn()) {
redirectToLogin()
return
}
binding = ActivityFeatureHubBinding.inflate(layoutInflater) binding = ActivityFeatureHubBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.title = "SellyCloud SDK DEMO" supportActionBar?.hide()
settingsStore = AvDemoSettingsStore(this)
envStore = LiveEnvSettingsStore(this)
selectedTab = savedInstanceState?.getString(KEY_SELECTED_TAB)
?.let { runCatching { Tab.valueOf(it) }.getOrNull() }
?: Tab.HOME
binding.cardLiveStreaming.setOnClickListener { restoreSettingsToUi()
startActivity(Intent(this, MainActivity::class.java)) binding.etCallChannelId.setText(getString(R.string.default_call_id))
}
binding.cardInteractiveLive.setOnClickListener { setupTabs()
startActivity(Intent(this, InteractiveLiveActivity::class.java)) setupActions()
setupSettingsSave()
setupLogout()
setupHomeList()
refreshAliveList()
}
override fun onStart() {
super.onStart()
if (!loginStore.isLoggedIn() && !isFinishing) {
redirectToLogin()
} }
} }
override fun onDestroy() {
super.onDestroy()
uiScope.cancel()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_SELECTED_TAB, selectedTab.name)
}
private fun setupTabs() {
binding.tabHome.setOnClickListener { selectTab(Tab.HOME) }
binding.tabCall.setOnClickListener { selectTab(Tab.CALL) }
binding.tabSettings.setOnClickListener { selectTab(Tab.SETTINGS) }
selectTab(selectedTab)
}
private fun setupActions() {
binding.btnHomeLivePush.setOnClickListener {
showPushSettingsDialog { showPushProtocolSheet() }
}
binding.btnHomeLivePull.setOnClickListener {
showPlayConfigDialog()
}
binding.btnCallSingleChat.setOnClickListener {
startInteractive(InteractiveLiveActivity.DEFAULT_CALL_TYPE_P2P)
}
binding.btnCallConference.setOnClickListener {
startInteractive(InteractiveLiveActivity.DEFAULT_CALL_TYPE_GROUP)
}
}
private fun setupHomeList() {
aliveAdapter = AliveStreamAdapter { item -> handleAliveItemClick(item) }
val layoutManager = GridLayoutManager(this, 2)
binding.rvLiveSquare.layoutManager = layoutManager
binding.rvLiveSquare.adapter = aliveAdapter
binding.rvLiveSquare.addItemDecoration(GridSpacingItemDecoration(2, dp(6)))
binding.rvLiveSquare.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy <= 0) return
val lastVisible = layoutManager.findLastVisibleItemPosition()
if (lastVisible >= aliveAdapter.itemCount - 2) {
appendNextPage()
}
}
})
binding.swipeHome.setOnRefreshListener { refreshAliveList() }
}
private fun refreshAliveList() {
binding.swipeHome.isRefreshing = true
uiScope.launch {
when (val result = AliveListRepository.fetchAliveList()) {
is AliveListResult.Success -> {
binding.swipeHome.isRefreshing = false
val response = result.response
if (!response.success) {
showHomeError(response.message ?: getString(R.string.home_live_square_error))
return@launch
}
resetPaging(response.list)
}
is AliveListResult.Error -> {
binding.swipeHome.isRefreshing = false
showHomeError(result.message)
}
}
}
}
private fun resetPaging(items: List<AliveStreamItem>) {
allAliveItems.clear()
allAliveItems.addAll(items)
currentPage = 0
aliveAdapter.replaceAll(emptyList())
appendNextPage()
updateHomeEmptyState()
}
private fun appendNextPage() {
if (isPaging) return
val start = currentPage * PAGE_SIZE
if (start >= allAliveItems.size) return
isPaging = true
val end = min(start + PAGE_SIZE, allAliveItems.size)
aliveAdapter.appendItems(allAliveItems.subList(start, end))
currentPage += 1
isPaging = false
}
private fun updateHomeEmptyState() {
val empty = allAliveItems.isEmpty()
binding.tvHomeEmpty.text = getString(R.string.home_live_square_empty)
binding.tvHomeEmpty.visibility = if (empty) View.VISIBLE else View.GONE
binding.rvLiveSquare.visibility = if (empty) View.INVISIBLE else View.VISIBLE
}
private fun showHomeError(message: String) {
allAliveItems.clear()
aliveAdapter.replaceAll(emptyList())
binding.tvHomeEmpty.text = message
binding.tvHomeEmpty.visibility = View.VISIBLE
binding.rvLiveSquare.visibility = View.INVISIBLE
}
private fun handleAliveItemClick(item: AliveStreamItem) {
val liveMode = resolvePlayMode(item.playProtocol)
val params = buildPlayParams(item)
if (params == null) {
Toast.makeText(this, "播放地址缺失", Toast.LENGTH_SHORT).show()
return
}
startActivity(
LivePlayActivity.createIntentWithParams(
this,
liveMode,
params.vhost,
params.appName,
params.streamName,
autoStart = true
)
)
}
private fun resolvePlayMode(raw: String?): SellyLiveMode {
val normalized = raw?.trim()?.uppercase() ?: ""
return if (normalized == "WHEP" || normalized == "WHIP" || normalized == "RTC") {
SellyLiveMode.RTC
} else {
SellyLiveMode.RTMP
}
}
private fun buildPlayParams(item: AliveStreamItem): PlayParams? {
val env = envStore.read()
val vhost = item.vhost?.trim().orEmpty().ifBlank { env.normalizedVhost() }
val app = item.app?.trim().orEmpty().ifBlank { env.normalizedAppName() }
val stream = item.stream?.trim().orEmpty()
if (vhost.isBlank() || app.isBlank() || stream.isBlank()) return null
return PlayParams(vhost = vhost, appName = app, streamName = stream)
}
private data class PlayParams(
val vhost: String,
val appName: String,
val streamName: String
)
private fun startInteractive(defaultCallType: String) {
val callId = binding.etCallChannelId.text?.toString()?.trim().orEmpty()
if (callId.isEmpty()) {
Toast.makeText(this, getString(R.string.call_id_required), Toast.LENGTH_SHORT).show()
return
}
startActivity(
Intent(this, InteractiveLiveActivity::class.java)
.putExtra(InteractiveLiveActivity.EXTRA_DEFAULT_CALL_TYPE, defaultCallType)
.putExtra(InteractiveLiveActivity.EXTRA_CALL_ID, callId)
)
}
private fun showPushProtocolSheet() {
val dialog = Dialog(this, R.style.Theme_AVDemo_Dialog_BottomSheet)
val sheetBinding = DialogPushProtocolSheetBinding.inflate(layoutInflater)
dialog.setContentView(sheetBinding.root)
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialog.window?.setGravity(Gravity.BOTTOM)
sheetBinding.btnRtc.setOnClickListener {
dialog.dismiss()
startActivity(LivePushActivity.createIntent(this, SellyLiveMode.RTC))
}
sheetBinding.btnRtmp.setOnClickListener {
dialog.dismiss()
startActivity(LivePushActivity.createIntent(this, SellyLiveMode.RTMP))
}
sheetBinding.btnCancel.setOnClickListener { dialog.dismiss() }
dialog.show()
}
private fun showPushSettingsDialog(onApplied: () -> Unit) {
val dialog = Dialog(this)
val dialogBinding = DialogLivePresetSettingsBinding.inflate(layoutInflater)
dialog.setContentView(dialogBinding.root)
dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT))
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
dialog.window?.decorView?.setPadding(0, 0, 0, 0)
val current = settingsStore.read()
dialogBinding.etStreamId.setText(generateRandomStreamId())
dialogBinding.etFps.setText(current.fps.toString())
dialogBinding.etMaxBitrate.setText(current.maxBitrateKbps.toString())
dialogBinding.etMinBitrate.setText(current.minBitrateKbps.toString())
dialogBinding.rgResolution.check(
when (current.resolution) {
AvDemoSettings.Resolution.P360 -> R.id.rbRes360p
AvDemoSettings.Resolution.P480 -> R.id.rbRes480p
AvDemoSettings.Resolution.P540 -> R.id.rbRes540p
AvDemoSettings.Resolution.P720 -> R.id.rbRes720p
}
)
dialogBinding.btnClose.setOnClickListener { dialog.dismiss() }
dialogBinding.btnApply.setOnClickListener {
val streamId = dialogBinding.etStreamId.text?.toString()?.trim().orEmpty()
val fps = dialogBinding.etFps.text?.toString()?.trim()?.toIntOrNull()
val maxKbps = dialogBinding.etMaxBitrate.text?.toString()?.trim()?.toIntOrNull()
val minKbps = dialogBinding.etMinBitrate.text?.toString()?.trim()?.toIntOrNull()
if (streamId.isEmpty()) {
Toast.makeText(this, "请输入 Stream ID", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (fps == null || fps <= 0) {
Toast.makeText(this, "请输入正确的 FPS", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (maxKbps == null || maxKbps <= 0) {
Toast.makeText(this, "请输入正确的最大码率", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (minKbps == null || minKbps <= 0) {
Toast.makeText(this, "请输入正确的最小码率", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val res = when (dialogBinding.rgResolution.checkedRadioButtonId) {
R.id.rbRes360p -> AvDemoSettings.Resolution.P360
R.id.rbRes480p -> AvDemoSettings.Resolution.P480
R.id.rbRes540p -> AvDemoSettings.Resolution.P540
else -> AvDemoSettings.Resolution.P720
}
val updated = current.copy(
streamId = streamId,
resolution = res,
fps = fps,
maxBitrateKbps = maxKbps,
minBitrateKbps = minKbps
)
settingsStore.write(updated)
dialog.dismiss()
onApplied()
}
dialog.show()
}
private fun showPlayConfigDialog() {
val dialog = Dialog(this, R.style.Theme_AVDemo_Dialog_FullscreenOverlay)
val dialogBinding = DialogPlayConfigOverlayBinding.inflate(layoutInflater)
dialog.setContentView(dialogBinding.root)
dialog.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
dialog.setCancelable(true)
dialogBinding.btnClose.setOnClickListener { dialog.dismiss() }
dialogBinding.btnStartPlay.setOnClickListener {
val input = dialogBinding.etStreamIdOrUrl.text?.toString()?.trim().orEmpty()
if (input.isEmpty()) {
Toast.makeText(this, "请输入 Stream ID 或 URL", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val liveMode = if (dialogBinding.rbRtc.isChecked) {
SellyLiveMode.RTC
} else {
SellyLiveMode.RTMP
}
dialog.dismiss()
startActivity(LivePlayActivity.createIntent(this, liveMode, input, autoStart = true))
}
dialog.show()
}
private fun setupSettingsSave() {
binding.btnSaveSettings.setOnClickListener {
val settings = uiToSettingsOrNull() ?: return@setOnClickListener
settingsStore.write(settings)
val envSettings = uiToEnvSettings(envStore.read(), settings.streamId)
envStore.write(envSettings)
Toast.makeText(this, "已保存", Toast.LENGTH_SHORT).show()
}
}
private fun setupLogout() {
binding.btnLogout.setOnClickListener {
loginStore.clear()
redirectToLogin()
}
}
private fun redirectToLogin() {
startActivity(LoginActivity.createIntent(this, clearTask = true))
finish()
}
private fun restoreSettingsToUi() {
val settings = settingsStore.read()
binding.etSettingsStreamId.setText(settings.streamId)
binding.etSettingsFps.setText(settings.fps.toString())
binding.etSettingsMaxBitrate.setText(settings.maxBitrateKbps.toString())
binding.etSettingsMinBitrate.setText(settings.minBitrateKbps.toString())
when (settings.resolution) {
AvDemoSettings.Resolution.P360 -> binding.rgSettingsResolution.check(R.id.rbSettingsRes360p)
AvDemoSettings.Resolution.P480 -> binding.rgSettingsResolution.check(R.id.rbSettingsRes480p)
AvDemoSettings.Resolution.P540 -> binding.rgSettingsResolution.check(R.id.rbSettingsRes540p)
AvDemoSettings.Resolution.P720 -> binding.rgSettingsResolution.check(R.id.rbSettingsRes720p)
}
restoreEnvSettingsToUi()
}
private fun restoreEnvSettingsToUi() {
val env = envStore.read()
binding.etSettingsVhost.setText(env.vhost)
binding.etSettingsVhostKey.setText(env.vhostKey)
binding.etSettingsAppId.setText(env.appId)
}
private fun uiToSettingsOrNull(): AvDemoSettings? {
val streamId = binding.etSettingsStreamId.text?.toString()?.trim().orEmpty()
val fps = binding.etSettingsFps.text?.toString()?.trim()?.toIntOrNull()
val maxKbps = binding.etSettingsMaxBitrate.text?.toString()?.trim()?.toIntOrNull()
val minKbps = binding.etSettingsMinBitrate.text?.toString()?.trim()?.toIntOrNull()
if (streamId.isEmpty()) {
Toast.makeText(this, "请输入 Stream ID", Toast.LENGTH_SHORT).show()
return null
}
if (fps == null || fps <= 0) {
Toast.makeText(this, "请输入正确的 FPS", Toast.LENGTH_SHORT).show()
return null
}
if (maxKbps == null || maxKbps <= 0) {
Toast.makeText(this, "请输入正确的最大码率", Toast.LENGTH_SHORT).show()
return null
}
if (minKbps == null || minKbps <= 0) {
Toast.makeText(this, "请输入正确的最小码率", Toast.LENGTH_SHORT).show()
return null
}
val res = when (binding.rgSettingsResolution.checkedRadioButtonId) {
R.id.rbSettingsRes360p -> AvDemoSettings.Resolution.P360
R.id.rbSettingsRes480p -> AvDemoSettings.Resolution.P480
R.id.rbSettingsRes540p -> AvDemoSettings.Resolution.P540
else -> AvDemoSettings.Resolution.P720
}
val current = settingsStore.read()
return current.copy(
streamId = streamId,
resolution = res,
fps = fps,
maxBitrateKbps = maxKbps,
minBitrateKbps = minKbps
)
}
private fun uiToEnvSettings(current: LiveEnvSettings, streamId: String): LiveEnvSettings {
val vhost = binding.etSettingsVhost.text?.toString()?.trim().orEmpty()
val vhostKey = binding.etSettingsVhostKey.text?.toString()?.trim().orEmpty()
val appId = binding.etSettingsAppId.text?.toString()?.trim().orEmpty()
return current.copy(
vhost = vhost,
vhostKey = vhostKey,
appId = appId,
defaultStreamId = streamId
)
}
private fun selectTab(tab: Tab) {
selectedTab = tab
binding.tabHome.isSelected = tab == Tab.HOME
binding.tabCall.isSelected = tab == Tab.CALL
binding.tabSettings.isSelected = tab == Tab.SETTINGS
binding.pageHome.visibility = if (tab == Tab.HOME) View.VISIBLE else View.GONE
binding.pageCall.visibility = if (tab == Tab.CALL) View.VISIBLE else View.GONE
binding.pageSettings.visibility = if (tab == Tab.SETTINGS) View.VISIBLE else View.GONE
val titleRes = when (tab) {
Tab.HOME -> R.string.tab_home
Tab.CALL -> R.string.tab_call
Tab.SETTINGS -> R.string.tab_settings
}
binding.tvTitle.setText(titleRes)
}
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).roundToInt()
private fun generateRandomStreamId(): String = Random.nextInt(100, 1000).toString()
private class GridSpacingItemDecoration(
private val spanCount: Int,
private val spacing: Int
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
if (position == RecyclerView.NO_POSITION) return
val column = position % spanCount
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
outRect.top = if (position < spanCount) spacing else spacing / 2
outRect.bottom = spacing
}
}
private enum class Tab { HOME, CALL, SETTINGS }
private companion object {
private const val KEY_SELECTED_TAB = "selected_tab"
private const val PAGE_SIZE = 4
}
} }

View File

@@ -67,11 +67,17 @@ class FUBeautyFilterRender(
previewWidth: Int, previewWidth: Int,
previewHeight: Int previewHeight: Int
) { ) {
// GL 上下文可能重建:确保滤镜和 FaceUnity 资源重新初始化
isInitialized = false
program = -1
// 先保存 ApplicationContext避免 super.initGl 内部触发 initGlFilter 时为空
this.appContext = context.applicationContext
super.initGl(width, height, context, previewWidth, previewHeight) super.initGl(width, height, context, previewWidth, previewHeight)
// 确保使用 ApplicationContext避免Activity依赖 // 确保使用 ApplicationContext避免Activity依赖
this.appContext = context.applicationContext
frameW = width frameW = width
frameH = height frameH = height
// 刷新 FaceUnity GL 资源绑定到新的上下文
fuRenderer.reinitializeGlContextBlocking()
Log.d(TAG, "initGl: width=$width, height=$height, context=${context.javaClass.simpleName}") Log.d(TAG, "initGl: width=$width, height=$height, context=${context.javaClass.simpleName}")
} }
@@ -234,6 +240,7 @@ class FUBeautyFilterRender(
} }
override fun release() { override fun release() {
isInitialized = false
if (program != -1) { if (program != -1) {
GLES20.glDeleteProgram(program) GLES20.glDeleteProgram(program)
program = -1 program = -1

View File

@@ -19,7 +19,9 @@ import com.faceunity.wrapper.faceunity
import com.pedro.encoder.input.video.CameraHelper import com.pedro.encoder.input.video.CameraHelper
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/** /**
@@ -50,7 +52,11 @@ class FURenderer(private val context: Context) {
private val BUNDLE_AI_HUMAN = "model" + File.separator + "ai_human_processor.bundle" private val BUNDLE_AI_HUMAN = "model" + File.separator + "ai_human_processor.bundle"
private val BUNDLE_FACE_BEAUTY = "graphics" + File.separator + "face_beautification.bundle" private val BUNDLE_FACE_BEAUTY = "graphics" + File.separator + "face_beautification.bundle"
private val workerThread = Executors.newSingleThreadExecutor() @Volatile
private var workerThreadRef: Thread? = null
private val workerThread = Executors.newSingleThreadExecutor { task ->
Thread(task, "FURenderer-Worker").also { workerThreadRef = it }
}
// 添加摄像头朝向管理 // 添加摄像头朝向管理
private var currentCameraFacing: CameraHelper.Facing = CameraHelper.Facing.BACK private var currentCameraFacing: CameraHelper.Facing = CameraHelper.Facing.BACK
@@ -155,11 +161,6 @@ class FURenderer(private val context: Context) {
// 检查 SDK 和 GL 是否就绪 // 检查 SDK 和 GL 是否就绪
if (!isAuthSuccess || !isGlInitialized || fuRenderKit == null) { if (!isAuthSuccess || !isGlInitialized || fuRenderKit == null) {
// 如果认证成功但 GL 未初始化,尝试初始化
if (isAuthSuccess && !isGlInitialized) {
Log.w(TAG, "GL not initialized, attempting to initialize")
reinitializeGlContext()
}
return inputTex return inputTex
} }
@@ -255,30 +256,58 @@ class FURenderer(private val context: Context) {
*/ */
fun reinitializeGlContext() { fun reinitializeGlContext() {
if (!isAuthSuccess) return if (!isAuthSuccess) return
workerThread.execute { doReinitializeGlContext() }
}
/**
* 重新初始化 GL 上下文(同步等待完成,用于避免美颜空窗)
*/
fun reinitializeGlContextBlocking(timeoutMs: Long = 2000L) {
if (!isAuthSuccess) return
if (Thread.currentThread() === workerThreadRef) {
doReinitializeGlContext()
return
}
val latch = CountDownLatch(1)
workerThread.execute { workerThread.execute {
try { try {
Log.d(TAG, "Reinitializing GL context after protocol switch") doReinitializeGlContext()
} finally {
// 重新获取 FURenderKit 实例(绑定到新的 GL 上下文) latch.countDown()
fuRenderKit = FURenderKit.getInstance()
// 重新设置异步纹理模式
faceunity.fuSetUseTexAsync(1)
// 如果之前有美颜配置,重新应用
if (faceBeauty != null) {
fuRenderKit?.faceBeauty = faceBeauty
Log.d(TAG, "Beauty configuration reapplied")
}
isGlInitialized = true
Log.d(TAG, "GL context reinitialized successfully")
} catch (e: Exception) {
Log.e(TAG, "Error reinitializing GL context", e)
isGlInitialized = false
} }
} }
try {
if (!latch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
Log.w(TAG, "GL context reinit timeout: ${timeoutMs}ms")
}
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
Log.w(TAG, "GL context reinit interrupted")
}
}
private fun doReinitializeGlContext() {
try {
Log.d(TAG, "Reinitializing GL context after protocol switch")
// 重新获取 FURenderKit 实例(绑定到新的 GL 上下文)
fuRenderKit = FURenderKit.getInstance()
// 重新设置异步纹理模式
faceunity.fuSetUseTexAsync(1)
// 如果之前有美颜配置,重新应用
if (faceBeauty != null) {
fuRenderKit?.faceBeauty = faceBeauty
Log.d(TAG, "Beauty configuration reapplied")
}
isGlInitialized = true
Log.d(TAG, "GL context reinitialized successfully")
} catch (e: Exception) {
Log.e(TAG, "Error reinitializing GL context", e)
isGlInitialized = false
}
} }
/** /**

View File

@@ -1,12 +1,12 @@
package com.demo.SellyCloudSDK.beauty package com.demo.SellyCloudSDK.beauty
import android.util.Log import android.util.Log
import com.sellycloud.sellycloudsdk.VideoFrameInterceptor
import com.faceunity.core.entity.FURenderInputData import com.faceunity.core.entity.FURenderInputData
import com.faceunity.core.enumeration.CameraFacingEnum import com.faceunity.core.enumeration.CameraFacingEnum
import com.faceunity.core.enumeration.FUExternalInputEnum import com.faceunity.core.enumeration.FUExternalInputEnum
import com.faceunity.core.enumeration.FUInputBufferEnum import com.faceunity.core.enumeration.FUInputBufferEnum
import com.faceunity.core.enumeration.FUTransformMatrixEnum import com.faceunity.core.enumeration.FUTransformMatrixEnum
import com.sellycloud.sellycloudsdk.VideoFrameInterceptor
import org.webrtc.JavaI420Buffer import org.webrtc.JavaI420Buffer
import org.webrtc.VideoFrame import org.webrtc.VideoFrame

View File

@@ -8,8 +8,8 @@ import android.content.Context
import android.content.Intent 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.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import com.demo.SellyCloudSDK.R import com.demo.SellyCloudSDK.R
/** /**

View File

@@ -24,8 +24,8 @@ import com.sellycloud.sellycloudsdk.interactive.InteractiveChannelMediaOptions
import com.sellycloud.sellycloudsdk.interactive.InteractiveConnectionState import com.sellycloud.sellycloudsdk.interactive.InteractiveConnectionState
import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngine import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngine
import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngine.ConnectionReason import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngine.ConnectionReason
import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngineEventHandler
import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngineConfig import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngineConfig
import com.sellycloud.sellycloudsdk.interactive.InteractiveRtcEngineEventHandler
import com.sellycloud.sellycloudsdk.interactive.InteractiveStreamStats import com.sellycloud.sellycloudsdk.interactive.InteractiveStreamStats
import com.sellycloud.sellycloudsdk.interactive.InteractiveVideoCanvas import com.sellycloud.sellycloudsdk.interactive.InteractiveVideoCanvas
import com.sellycloud.sellycloudsdk.interactive.InteractiveVideoEncoderConfig import com.sellycloud.sellycloudsdk.interactive.InteractiveVideoEncoderConfig
@@ -37,6 +37,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
private lateinit var binding: ActivityInteractiveLiveBinding private lateinit var binding: ActivityInteractiveLiveBinding
private var rtcEngine: InteractiveRtcEngine? = null private var rtcEngine: InteractiveRtcEngine? = null
private var lockedCallType: CallType? = null
private var localRenderer: SurfaceViewRenderer? = null private var localRenderer: SurfaceViewRenderer? = null
private lateinit var localSlot: VideoSlot private lateinit var localSlot: VideoSlot
private lateinit var remoteSlots: List<VideoSlot> private lateinit var remoteSlots: List<VideoSlot>
@@ -106,6 +107,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
setupVideoSlots() setupVideoSlots()
initRtcEngine() initRtcEngine()
setupUiDefaults() setupUiDefaults()
applyDefaultCallTypeFromIntent()
setupControlButtons() setupControlButtons()
binding.btnJoin.setOnClickListener { binding.btnJoin.setOnClickListener {
@@ -128,6 +130,24 @@ class InteractiveLiveActivity : AppCompatActivity() {
} }
} }
private fun applyDefaultCallTypeFromIntent() {
lockedCallType = when (intent.getStringExtra(EXTRA_DEFAULT_CALL_TYPE)) {
DEFAULT_CALL_TYPE_GROUP -> CallType.GROUP
DEFAULT_CALL_TYPE_P2P -> CallType.ONE_TO_ONE
else -> null
}
when (lockedCallType) {
CallType.GROUP -> binding.rbCallTypeGroup.isChecked = true
CallType.ONE_TO_ONE -> binding.rbCallTypeP2p.isChecked = true
null -> Unit
}
// Home 已经确定单聊/多方通话时,这里不再让用户二次选择。
val locked = lockedCallType != null
binding.callTypeGroup.isVisible = !locked
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
rtcEngine?.setCaptureVideoFrameInterceptor(null) rtcEngine?.setCaptureVideoFrameInterceptor(null)
@@ -151,6 +171,7 @@ class InteractiveLiveActivity : AppCompatActivity() {
private fun initRtcEngine() { private fun initRtcEngine() {
val appId = getString(R.string.signaling_app_id) val appId = getString(R.string.signaling_app_id)
val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() } val token = getString(R.string.signaling_token).takeIf { it.isNotBlank() }
val kiwiRsName = getString(R.string.signaling_kiwi_rsname).trim()
beautyRenderer = FURenderer(this).also { it.setup() } beautyRenderer = FURenderer(this).also { it.setup() }
fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply { fuFrameInterceptor = beautyRenderer?.let { FuVideoFrameInterceptor(it).apply {
setFrontCamera(isFrontCamera) setFrontCamera(isFrontCamera)
@@ -160,12 +181,13 @@ class InteractiveLiveActivity : AppCompatActivity() {
InteractiveRtcEngineConfig( InteractiveRtcEngineConfig(
context = applicationContext, context = applicationContext,
appId = appId, appId = appId,
defaultToken = token defaultToken = token,
kiwiRsName = kiwiRsName
) )
).apply { ).apply {
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 = 850)) setVideoEncoderConfiguration(InteractiveVideoEncoderConfig(640, 480 , fps = 20, minBitrateKbps = 150, maxBitrateKbps = 850))
setDefaultAudioRoutetoSpeakerphone(true) setDefaultAudioRoutetoSpeakerphone(true)
setCaptureVideoFrameInterceptor { frame -> setCaptureVideoFrameInterceptor { frame ->
@@ -318,7 +340,12 @@ class InteractiveLiveActivity : AppCompatActivity() {
} }
private fun setupUiDefaults() { private fun setupUiDefaults() {
binding.etCallId.setText(getString(R.string.default_call_id)) val presetCallId = intent.getStringExtra(EXTRA_CALL_ID)
if (!presetCallId.isNullOrBlank()) {
binding.etCallId.setText(presetCallId)
} else {
binding.etCallId.setText(getString(R.string.default_call_id))
}
val defaultUser = String.format( val defaultUser = String.format(
getString(R.string.default_user_id), getString(R.string.default_user_id),
System.currentTimeMillis().toString().takeLast(4) System.currentTimeMillis().toString().takeLast(4)
@@ -494,9 +521,9 @@ class InteractiveLiveActivity : AppCompatActivity() {
Toast.makeText(this, R.string.signaling_app_id_missing, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.signaling_app_id_missing, Toast.LENGTH_LONG).show()
return return
} }
val options = InteractiveChannelMediaOptions( val options = InteractiveChannelMediaOptions(callType = lockedCallType ?: run {
callType = if (binding.rbCallTypeP2p.isChecked) CallType.ONE_TO_ONE else CallType.GROUP if (binding.rbCallTypeP2p.isChecked) CallType.ONE_TO_ONE else CallType.GROUP
) })
val tokenBundle = buildToken(appId, callId, userInput) ?: return val tokenBundle = buildToken(appId, callId, userInput) ?: return
pendingJoinRequest = JoinRequest( pendingJoinRequest = JoinRequest(
token = tokenBundle.token, token = tokenBundle.token,
@@ -851,6 +878,11 @@ class InteractiveLiveActivity : AppCompatActivity() {
} }
companion object { companion object {
const val EXTRA_DEFAULT_CALL_TYPE = "default_call_type"
const val EXTRA_CALL_ID = "call_id"
const val DEFAULT_CALL_TYPE_P2P = "p2p"
const val DEFAULT_CALL_TYPE_GROUP = "group"
private const val TAG = "InteractiveLiveActivity" private const val TAG = "InteractiveLiveActivity"
} }

View File

@@ -1,887 +0,0 @@
package com.demo.SellyCloudSDK.live
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.PixelFormat
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.SurfaceHolder
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.demo.SellyCloudSDK.R
import com.demo.SellyCloudSDK.beauty.FaceUnityBeautyEngine
import com.demo.SellyCloudSDK.databinding.ActivityMainBinding
import com.sellycloud.sellycloudsdk.*
import com.sellycloud.sellycloudsdk.PlayerConfig
import com.sellycloud.sellycloudsdk.RtmpPlayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import org.webrtc.SurfaceViewRenderer
import com.sellycloud.sellycloudsdk.Protocol
import android.content.res.Configuration
import org.webrtc.RendererCommon
import kotlin.text.clear
class MainActivity : AppCompatActivity(),
SurfaceHolder.Callback {
private lateinit var binding: ActivityMainBinding
// 单一 StreamingManager按协议初始化
private var streamingManager: StreamingManager? = null
private val faceUnityBeautyEngine: FaceUnityBeautyEngine by lazy { FaceUnityBeautyEngine() }
// UI 状态助手
private lateinit var uiState: UiStateManager
// 播放 Surface 管理器
private lateinit var playSurfaceManager: PlaySurfaceManager
// WHEP 相关
private var whepClient: WhepClient? = null
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var isWhepPlaying = false
private var whepSurfaceView: SurfaceViewRenderer? = null
private var webrtcEglBase: org.webrtc.EglBase? = null
// 预览 Surface 就绪标志RTMP 预览视图)
private var isPushSurfaceReady = false
// 协议选择
private var selectedProtocol: Protocol = Protocol.RTMP
// 播放类型枚举
private enum class PlayType { NONE, RTMP, WHEP }
private var currentPlayType = PlayType.NONE
// 播放器
private var player: RtmpPlayer? = null
private var playerConfig: PlayerConfig? = null
private var isPlaySurfaceValid = false
private var lastPlayUrl: String? = null
private var shouldResumePlayback = false
private var needRecreatePlayer = false
// 状态变量
private var idelStatus = "待启动"
private val permissions =
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) else arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
// 防止重复启动预览导致多次 GL / 美颜初始化
private var hasStartedPushPreview = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// 初始化 UI 与管理器
uiState = UiStateManager(binding)
playSurfaceManager = PlaySurfaceManager(binding.surfaceViewPlay)
uiState.setRtmpButtonText(false)
updateWhepButtonText()
// 屏幕常亮
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// 初始化 StreamingManager 与监听
streamingManager = StreamingManager(this).also { mgr ->
mgr.setBeautyEngine(faceUnityBeautyEngine)
mgr.setStreamingListener(object : StreamingListener {
override fun onStateUpdate(state: StreamingState, message: String?, extras: Bundle?) {
runOnUiThread {
val text = message ?: when (state) {
StreamingState.IDLE -> "待启动"
StreamingState.CONNECTING -> "连接中..."
StreamingState.STREAMING -> "推流中"
StreamingState.RECONNECTING -> "重连中..."
StreamingState.STOPPED -> "已停止"
StreamingState.FAILED -> "推流错误"
}
val logMap = mapOf(
"state" to state.name,
"message" to message,
"extras" to bundleToMap(extras)
)
Log.d("MainActivity111111", logMap.toString())
uiState.setPushStatusText(text, idelStatus)
uiState.setPushButtonsEnabled(state == StreamingState.STREAMING)
setProtocolSelectionEnabled(state != StreamingState.STREAMING && state != StreamingState.CONNECTING && state != StreamingState.RECONNECTING)
}
}
override fun onError(error: StreamingError) {
runOnUiThread { Toast.makeText(this@MainActivity, error.message, Toast.LENGTH_SHORT).show() }
}
})
}
// 默认 RTMP 预览标题
val defaultId = if (selectedProtocol == Protocol.WHIP) R.id.rbProtocolWhip else R.id.rbProtocolRtmp
if (binding.protocolGroup.checkedRadioButtonId != defaultId) {
binding.protocolGroup.check(defaultId)
}
setPushPreviewHeader(selectedProtocol.name)
// 绑定 UI 监听
setupListeners()
// 权限
checkAndRequestPermissions()
}
// 接管屏幕方向变化,避免 Activity 重建导致两个预览销毁
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// RTMP 推流预览OpenGlView保持像素格式与叠放层不变仅请求重新布局
try {
binding.surfaceViewPlay.setZOrderMediaOverlay(false)
binding.surfaceViewPlay.holder.setFormat(PixelFormat.OPAQUE)
binding.surfaceViewPlay.requestLayout()
} catch (_: Exception) {}
// WHIP 推流预览SurfaceViewRenderer只调整缩放并请求布局
try {
binding.whipPreview.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
binding.whipPreview.setEnableHardwareScaler(true)
binding.whipPreview.requestLayout()
} catch (_: Exception) {}
// 若当前是 WHEP 播放,动态渲染器同样更新缩放并请求布局
try {
whepSurfaceView?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
whepSurfaceView?.setEnableHardwareScaler(true)
whepSurfaceView?.requestLayout()
} catch (_: Exception) {}
// 播放 Surface 保持 RGBA_8888 与覆盖层,确保颜色/叠放正确
try {
binding.surfaceViewPlay.setZOrderMediaOverlay(true)
ensurePlaySurfaceFormat()
binding.surfaceViewPlay.requestLayout()
} catch (_: Exception) {}
}
override fun onResume() {
super.onResume()
// 恢复美颜/GL 管线
// streamingManager?.onResume()
// 恢复预览RTMP/WHIP
if (isPushSurfaceReady) {
try {
streamingManager?.resumePreview()
} catch (_: Exception) {
}
}
// 播放器复位
if (needRecreatePlayer && isPlaySurfaceValid) {
recreatePlayerAndMaybeResume()
} else if (shouldResumePlayback && !lastPlayUrl.isNullOrEmpty()) {
val holder = binding.surfaceViewPlay.holder
if (holder.surface != null && holder.surface.isValid) {
ensurePlaySurfaceFormat()
player?.setSurface(holder.surface)
player?.prepareAsync(lastPlayUrl!!)
updateStatus(playStatus = "正在连接")
updatePlayButtonStates(false)
shouldResumePlayback = false
}
}
}
override fun onPause() {
super.onPause()
// 暂停美颜/GL 管线
// streamingManager?.onPause()
// 暂停预览RTMP/WHIP
if (selectedProtocol == Protocol.RTMP) {
try { streamingManager?.pausePreview() } catch (_: Exception) {}
} else if (selectedProtocol == Protocol.WHIP) {
try { streamingManager?.stopWhipPreview() } catch (_: Exception) {}
}
// 播放侧资源处理
shouldResumePlayback = player?.isPlaying() == true || player?.isPrepared() == true
try { player?.setSurface(null) } catch (_: Exception) {}
player?.release(); player = null
needRecreatePlayer = true
shouldResumePlayback = true
}
private fun setupListeners() {
// 开始推流(根据协议)
binding.btnStartPush.setOnClickListener {
// 获取各个配置字段
// val host = binding.etHost.text.toString().trim()
val appName = binding.etAppName.text.toString().trim()
val streamName = binding.etStreamName.text.toString().trim()
// val streamKey = binding.etStreamKey.text.toString().trim()
// 验证必填字段
// if (host.isEmpty()) {
// Toast.makeText(this, "请输入Host地址", Toast.LENGTH_SHORT).show()
// return@setOnClickListener
// }
if (appName.isEmpty()) {
Toast.makeText(this, "请输入App Name", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (streamName.isEmpty()) {
Toast.makeText(this, "请输入Stream Name", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
streamingManager?.updateStreamConfig(
host = "rtmp.sellycloud.push",
appName = appName,
streamName = streamName,
streamKey = ""
)
streamingManager?.startStreaming()
// 同步美颜
streamingManager?.setBeautyEnabled(binding.switchBeauty.isChecked)
}
// 停止推流
binding.btnStopPush.setOnClickListener { streamingManager?.stopStreaming() }
// 协议选择监听
binding.protocolGroup.setOnCheckedChangeListener { _, checkedId ->
val newProtocol = if (checkedId == R.id.rbProtocolWhip) Protocol.WHIP else Protocol.RTMP
if (newProtocol != selectedProtocol) {
switchProtocol(newProtocol)
}
}
// 切换摄像头
binding.btnSwitchCamera.setOnClickListener { streamingManager?.switchCamera() }
// 切换方向
binding.btnSwitchOrientation.setOnClickListener { streamingManager?.switchOrientation() }
// 镜像
binding.cbPreviewHFlip.setOnCheckedChangeListener { _, h ->
streamingManager?.setMirror(horizontal = h, vertical = binding.cbPreviewVFlip.isChecked)
}
binding.cbPreviewVFlip.setOnCheckedChangeListener { _, v ->
streamingManager?.setMirror(horizontal = binding.cbPreviewHFlip.isChecked, vertical = v)
}
// 美颜
binding.switchBeauty.setOnCheckedChangeListener { _, on ->
streamingManager?.setBeautyEnabled(on)
Toast.makeText(this, "美颜功能${if (on) "开启" else "关闭"}", Toast.LENGTH_SHORT).show()
}
binding.switchBeauty.setOnLongClickListener { Toast.makeText(this, "高级美颜面板暂未开放", Toast.LENGTH_SHORT).show(); true }
// 分辨率
binding.resolutionGroup.setOnCheckedChangeListener { _, checkedId ->
val (w, h) = when (checkedId) {
R.id.res360p -> 360 to 640
R.id.res540p -> 540 to 960
R.id.res720p -> 720 to 1280
R.id.res1080p -> 1080 to 1920
else -> 720 to 1280
}
streamingManager?.changeResolution(w, h)
}
// 选择图片作为视频源
val pickImage = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
if (uri == null) { Toast.makeText(this, "未选择图片", Toast.LENGTH_SHORT).show(); return@registerForActivityResult }
try {
val bmp = decodeBitmapFromUri(uri)
if (bmp != null) {
val ok = streamingManager?.setBitmapAsVideoSource(bmp)
Toast.makeText(this, if (ok == true) "已切换为图片源" else "暂不支持该分辨率/失败", Toast.LENGTH_SHORT).show()
} else Toast.makeText(this, "图片解码失败", Toast.LENGTH_SHORT).show()
} catch (e: Exception) { Toast.makeText(this, "设置图片源失败: ${e.message}", Toast.LENGTH_LONG).show() }
}
binding.btnChooseImageSource.setOnClickListener { pickImage.launch("image/*") }
// 恢复摄像头视频源
binding.btnRestoreCamera.setOnClickListener { streamingManager?.restoreCameraVideoSource() }
// RTMP 播放:单按钮切换 开始/停止
binding.btnPlay.setOnClickListener {
if (currentPlayType == PlayType.RTMP) {
stopCurrentPlayback()
return@setOnClickListener
}
val playAppName = binding.etPlayAppName.text.toString().trim()
val playStreamName = binding.etPlayStreamName.text.toString().trim()
if (playAppName.isEmpty()) { Toast.makeText(this, "请输入Play App Name", Toast.LENGTH_SHORT).show(); return@setOnClickListener }
if (playStreamName.isEmpty()) { Toast.makeText(this, "请输入Play Stream Name", Toast.LENGTH_SHORT).show(); return@setOnClickListener }
val url = buildPlayUrl("RTMP", playAppName, playStreamName)
Toast.makeText(this, "播放地址: $url", Toast.LENGTH_SHORT).show()
stopCurrentPlayback() // 停止其他播放如WHEP
if (isPlaySurfaceValid) {
ensurePlaySurfaceFormat()
lastPlayUrl = url
currentPlayType = PlayType.RTMP
player?.setSurface(binding.surfaceViewPlay.holder.surface)
player?.prepareAsync(url)
updatePlayButtonStates(false)
uiState.setRtmpButtonText(true)
updateStatus(playStatus = "正在连接(RTMP)")
} else Toast.makeText(this, "播放 Surface 未准备好", Toast.LENGTH_SHORT).show()
}
// 已移除独立的停止播放按钮
// WHEP 拉流:单按钮切换 开始/停止
binding.btnWhepPlay.setOnClickListener {
if (isWhepPlaying) {
stopWhepStreaming()
} else {
val playAppName = binding.etPlayAppName.text.toString().trim()
val playStreamName = binding.etPlayStreamName.text.toString().trim()
if (playAppName.isEmpty()) { Toast.makeText(this, "请输入Play App Name", Toast.LENGTH_SHORT).show(); return@setOnClickListener }
if (playStreamName.isEmpty()) { Toast.makeText(this, "请输入Play Stream Name", Toast.LENGTH_SHORT).show(); return@setOnClickListener }
val url = buildPlayUrl("WHEP", playAppName, playStreamName)
Toast.makeText(this, "播放地址: $url", Toast.LENGTH_SHORT).show()
startWhepStreaming(url)
}
}
// 截图:推流预览
binding.btnCapturePush.setOnClickListener {
val targetView: View? = if (selectedProtocol == Protocol.WHIP) binding.whipPreview else binding.surfaceViewPush
captureSurfaceViewAndSave(targetView, prefix = "push")
}
// 截图:播放
binding.btnCapturePlay.setOnClickListener {
val targetView: View? = if (isWhepPlaying) whepSurfaceView else binding.surfaceViewPlay
captureSurfaceViewAndSave(targetView, prefix = "play")
}
}
/* ---------- 协议切换 ---------- */
private fun switchProtocol(newProtocol: Protocol) {
if (newProtocol == Protocol.RTMP) {
binding.surfaceViewPush.visibility = View.VISIBLE
binding.whipPreview.visibility = View.GONE
setPushPreviewHeader("RTMP")
} else {
binding.surfaceViewPush.visibility = View.GONE
binding.whipPreview.visibility = View.VISIBLE
setPushPreviewHeader("WHIP")
}
// 根据协议选择对应视图
val targetView = if (newProtocol == Protocol.RTMP) {
binding.surfaceViewPush
} else {
binding.whipPreview
}
selectedProtocol = newProtocol
streamingManager?.switchProtocol(newProtocol, targetView)
}
private fun setProtocolSelectionEnabled(enabled: Boolean) {
binding.protocolGroup.isEnabled = enabled
binding.rbProtocolRtmp.isEnabled = enabled
binding.rbProtocolWhip.isEnabled = enabled
}
/* ---------- SurfaceHolder.Callback ---------- */
override fun surfaceCreated(holder: SurfaceHolder) {
when (holder.surface) {
binding.surfaceViewPlay.holder.surface -> {
isPlaySurfaceValid = true
ensurePlaySurfaceFormat()
if (needRecreatePlayer) {
recreatePlayerAndMaybeResume()
} else {
player?.setSurface(holder.surface)
if (shouldResumePlayback && !lastPlayUrl.isNullOrEmpty()) {
player?.prepareAsync(lastPlayUrl!!)
updateStatus(playStatus = "正在连接")
updatePlayButtonStates(false)
shouldResumePlayback = false
}
}
}
binding.surfaceViewPush.holder.surface -> {
isPushSurfaceReady = true
//打印日志
Log.d("MainActivity", "Push surface created")
Log.d("MainActivity" , hasStartedPushPreview.toString())
// 仅首次或重建后启动预览,避免重复 startPreview + 触发多次美颜加载
if (!hasStartedPushPreview) {
try {
streamingManager?.startPreview(); hasStartedPushPreview = true
streamingManager?.setBeautyEnabled(binding.switchBeauty.isChecked)
} catch (_: Exception) {
}
}
}
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
if (holder.surface == binding.surfaceViewPlay.holder.surface) ensurePlaySurfaceFormat()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
when (holder.surface) {
binding.surfaceViewPlay.holder.surface -> {
isPlaySurfaceValid = false
player?.setSurface(null)
}
binding.surfaceViewPush.holder.surface -> {
Log.d("MainActivity", "Push surface destroyed")
isPushSurfaceReady = false
hasStartedPushPreview = false // 下次重建允许重新 startPreview
// 释放摄像头/预览由流程统一处理
}
}
}
/** 权限检测与申请 */
private fun checkAndRequestPermissions() {
if (permissions.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }) {
setupAll()
} else {
permissionLauncher.launch(permissions)
}
}
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { results ->
if (results.values.all { it }) setupAll() else Toast.makeText(this, "需要相机和录音权限才能使用此功能", Toast.LENGTH_LONG).show()
}
/** 初始化(推流 + 播放) */
private fun setupAll() {
// 默认显示 RTMP 预览视图
binding.surfaceViewPush.visibility = if (selectedProtocol == Protocol.RTMP) View.VISIBLE else View.GONE
binding.whipPreview.visibility = if (selectedProtocol == Protocol.WHIP) View.VISIBLE else View.GONE
// RTMP 预览器配置
binding.surfaceViewPlay.setZOrderMediaOverlay(false)
binding.surfaceViewPlay.holder.setFormat(PixelFormat.OPAQUE)
// 由 Surface 回调驱动 RTMP 预览生命周期
binding.surfaceViewPush.holder.addCallback(this)
binding.whipPreview.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL )
binding.whipPreview.setEnableHardwareScaler(true)
val (w, h) = currentResolution()
//配置参数
streamingManager?.updateStreamConfig(
protocol = selectedProtocol,
width = 1080,
height = 1920,
fps = 40,
videoBitrate = 2_500_000,
audioBitrate = 128_000,
iFrameInterval = 1,
maxRetryCount = 5,
retryDelayMs = 3000,
facing = "front"
)
// 初始化 manager 与预览
try {
if (selectedProtocol == Protocol.RTMP) {
streamingManager?.initialize(binding.surfaceViewPush)
} else {
streamingManager?.initialize(binding.whipPreview)
}
} catch (_: Exception) {}
// 播放器与回调维持原逻辑
playerConfig = PlayerConfig.forRtmpLive(enableKiwi = true, rsname = "123")
player = RtmpPlayer(context = this, playerConfig = playerConfig!!)
attachRtmpPlayerStateListener()
binding.surfaceViewPlay.setZOrderMediaOverlay(false)
binding.surfaceViewPlay.holder.setFormat(PixelFormat.OPAQUE)
binding.surfaceViewPlay.holder.addCallback(this)
if (binding.surfaceViewPlay.holder.surface.isValid) {
surfaceCreated(binding.surfaceViewPlay.holder)
}
}
/** 将Uri解码成合适大小的Bitmap避免OOM */
private fun decodeBitmapFromUri(uri: Uri): Bitmap? {
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
contentResolver.openInputStream(uri)?.use { input -> BitmapFactory.decodeStream(input, null, options) }
val reqMax = 1280
var inSample = 1
val w = options.outWidth; val h = options.outHeight
if (w > reqMax || h > reqMax) {
val halfW = w / 2; val halfH = h / 2
while ((halfW / inSample) >= reqMax || (halfH / inSample) >= reqMax) { inSample *= 2 }
}
val decodeOpts = BitmapFactory.Options().apply { inSampleSize = inSample }
contentResolver.openInputStream(uri)?.use { input -> return BitmapFactory.decodeStream(input, null, decodeOpts) }
return null
}
/** 更新状态文本 */
private fun updateStatus(pushStatus: String? = null, playStatus: String? = null) {
runOnUiThread {
val currentPushStatus = binding.tvStatus.text.split("|")[0].split(":").getOrNull(1)?.trim() ?: "待启动"
val newPushStatus = pushStatus ?: currentPushStatus
if (playStatus != null) this.idelStatus = playStatus
uiState.setPushStatusText(newPushStatus, this.idelStatus)
}
}
private fun updatePlayButtonStates(enabled: Boolean) { runOnUiThread { uiState.setPlayButtonEnabled(enabled) } }
private fun setPushPreviewHeader(mode: String) { try { uiState.setPushPreviewHeader(mode) } catch (_: Exception) {} }
private fun updateWhepButtonText() { uiState.setWhepButtonText(isWhepPlaying) }
private fun stopCurrentPlayback() {
when (currentPlayType) {
PlayType.RTMP -> {
try { player?.setSurface(null) } catch (_: Exception) {}
try { player?.stop() } catch (_: Exception) {}
try { player?.release() } catch (_: Exception) {}
try { player?.destroy() } catch (_: Exception) {}
val cfg = playerConfig ?: PlayerConfig.forRtmpLive(enableKiwi = true, rsname = "123")
player = RtmpPlayer(context = this, playerConfig = cfg)
attachRtmpPlayerStateListener()
lastPlayUrl = null
shouldResumePlayback = false
currentPlayType = PlayType.NONE
updatePlayButtonStates(true)
uiState.setRtmpButtonText(false)
updateStatus(playStatus = "已停止播放")
forceRecreatePlaySurface()
}
PlayType.WHEP -> stopWhepStreaming()
PlayType.NONE -> {}
}
}
// 强制销毁并重建播放 Surface通过可见性切换触发 surfaceDestroyed/surfaceCreated
private fun forceRecreatePlaySurface() {
try {
binding.surfaceViewPlay.visibility = View.GONE
binding.surfaceViewPlay.post {
ensurePlaySurfaceFormat()
binding.surfaceViewPlay.visibility = View.VISIBLE
binding.surfaceViewPlay.requestLayout()
}
} catch (_: Exception) {}
}
/* ---------- WHEP 功能(保留) ---------- */
private fun startWhepStreaming(url: String) {
stopCurrentPlayback()
val whepUrl = url
try {
// 初始化 WHEP SurfaceViewRenderer
if (whepSurfaceView == null) {
whepSurfaceView = SurfaceViewRenderer(this)
runOnUiThread {
whepSurfaceView?.let { surfaceView ->
try {
if (webrtcEglBase == null) webrtcEglBase = org.webrtc.EglBase.create()
surfaceView.init(webrtcEglBase!!.eglBaseContext, null)
surfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
surfaceView.setEnableHardwareScaler(true)
val playContainer = binding.surfaceViewPlay.parent as android.view.ViewGroup
val layoutParams = android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT
)
surfaceView.layoutParams = layoutParams
surfaceView.setZOrderOnTop(false)
surfaceView.setZOrderMediaOverlay(true)
playContainer.addView(surfaceView)
binding.surfaceViewPlay.visibility = View.GONE
surfaceView.visibility = View.VISIBLE
playContainer.requestLayout(); surfaceView.requestLayout()
} catch (e: Exception) { Log.e("MainActivity", "Error initializing WHEP view", e); throw e }
}
}
}
// 启动播放
coroutineScope.launch(Dispatchers.Main) {
try {
delay(300)
whepClient = WhepClient(this@MainActivity, coroutineScope, whepSurfaceView!!, webrtcEglBase!!.eglBaseContext)
attachWhepPlayerStateListener()
whepClient?.play(whepUrl)
runOnUiThread {
isWhepPlaying = true
currentPlayType = PlayType.WHEP
updateWhepButtonText()
updatePlayButtonStates(false)
Toast.makeText(this@MainActivity, "WHEP拉流已启动", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
runOnUiThread {
isWhepPlaying = false; whepClient = null; currentPlayType = PlayType.NONE
updateWhepButtonText(); updatePlayButtonStates(true)
updateStatus(playStatus = "WHEP播放失败: ${e.message}")
Toast.makeText(this@MainActivity, "WHEP拉流启动失败: ${e.message}", Toast.LENGTH_LONG).show()
whepSurfaceView?.let { surfaceView ->
try { val parent = surfaceView.parent as? android.view.ViewGroup; parent?.removeView(surfaceView) } catch (_: Exception) {}
}
whepSurfaceView = null
binding.surfaceViewPlay.visibility = View.VISIBLE
}
}
}
} catch (e: Exception) { Toast.makeText(this, "WHEP拉流初始化失败: ${e.message}", Toast.LENGTH_LONG).show(); updateStatus(playStatus = "WHEP初始化失败") }
}
private fun stopWhepStreaming() {
try { whepClient?.stop(); whepClient = null; runOnUiThread {
whepSurfaceView?.let { surfaceView ->
try { surfaceView.release() } catch (_: Exception) {}
val parent = surfaceView.parent as? android.view.ViewGroup; parent?.removeView(surfaceView)
}
whepSurfaceView = null
binding.surfaceViewPlay.visibility = View.VISIBLE
ensurePlaySurfaceFormat()
} } catch (_: Exception) {}
try { webrtcEglBase?.release() } catch (_: Exception) {}
webrtcEglBase = null
isWhepPlaying = false; currentPlayType = PlayType.NONE
updateWhepButtonText(); updatePlayButtonStates(true)
updateStatus(playStatus = "WHEP播放已停止")
Toast.makeText(this, "WHEP拉流已停止", Toast.LENGTH_SHORT).show()
}
override fun onDestroy() {
super.onDestroy()
// 停止 WHEP
if (isWhepPlaying) stopWhepStreaming()
try { webrtcEglBase?.release(); webrtcEglBase = null } catch (_: Exception) {}
// 释放 StreamingManager
streamingManager?.release(); streamingManager = null
// 完整销毁播放器(包含协程作用域和 native profile
try { player?.destroy() } catch (_: Exception) { try { player?.release() } catch (_: Exception) {} }
player = null
coroutineScope.cancel()
try { binding.surfaceViewPlay.holder.removeCallback(this) } catch (_: Exception) {}
try { binding.surfaceViewPush.holder.removeCallback(this) } catch (_: Exception) {}
try { coroutineScope.cancel() } catch (_: Exception) {}
}
private fun recreatePlayerAndMaybeResume() {
val cfg = playerConfig ?: PlayerConfig.forRtmpLive(enableKiwi = true, rsname = "123")
player = RtmpPlayer(context = this, playerConfig = cfg)
attachRtmpPlayerStateListener()
ensurePlaySurfaceFormat()
val holder = binding.surfaceViewPlay.holder
if (holder.surface != null && holder.surface.isValid) player?.setSurface(holder.surface)
if (shouldResumePlayback && !lastPlayUrl.isNullOrEmpty()) {
player?.prepareAsync(lastPlayUrl!!)
updatePlayButtonStates(false)
uiState.setRtmpButtonText(true)
shouldResumePlayback = false
}
needRecreatePlayer = false
}
private fun attachRtmpPlayerStateListener() {
player?.setSCPlayerStateListener { state, detail ->
Log.d("MainActivity", "Player State: $state, Detail: $detail")
when (state) {
SCPlayerState.SCPlayerStateConnecting -> runOnUiThread {
val reconnect = detail?.contains("reconnecting") == true
updateStatus(playStatus = if (reconnect) "正在重连(RTMP)" else "正在连接(RTMP)")
updatePlayButtonStates(false)
uiState.setRtmpButtonText(true)
}
SCPlayerState.SCPlayerStatePlaying -> runOnUiThread { updateStatus(playStatus = "播放中(RTMP)"); updatePlayButtonStates(false); uiState.setRtmpButtonText(true) }
SCPlayerState.SCPlayerStatePaused -> runOnUiThread { updateStatus(playStatus = "暂停播放(RTMP)") }
SCPlayerState.SCPlayerStateStoppedOrEnded -> runOnUiThread {
val text = if (detail == "completed") "播放完成(RTMP)" else "已结束播放(RTMP)"
updateStatus(playStatus = text); updatePlayButtonStates(true); uiState.setRtmpButtonText(false)
try { player?.setSurface(null) } catch (_: Exception) {}
playSurfaceManager.clear()
}
SCPlayerState.SCPlayerStateFailed -> runOnUiThread {
updateStatus(playStatus = "播放错误(RTMP)"); updatePlayButtonStates(true); uiState.setRtmpButtonText(false)
try { player?.setSurface(null) } catch (_: Exception) {}
playSurfaceManager.clear()
}
SCPlayerState.SCPlayerStateIdle -> { }
}
}
}
private fun attachWhepPlayerStateListener() {
whepClient?.setSCPlayerStateListener { state, detail ->
Log.d("MainActivity", "WHEP Player State: $state, Detail: $detail")
when (state) {
SCPlayerState.SCPlayerStateConnecting -> runOnUiThread {
val statusText = if (detail == "ICE connected") "已连接(WHEP)" else "正在连接(WHEP)"
updateStatus(playStatus = statusText); updatePlayButtonStates(false)
}
SCPlayerState.SCPlayerStatePlaying -> runOnUiThread { updateStatus(playStatus = "播放中(WHEP)"); updatePlayButtonStates(false) }
SCPlayerState.SCPlayerStateStoppedOrEnded -> runOnUiThread {
isWhepPlaying = false
updateStatus(playStatus = "WHEP播放已停止"); updatePlayButtonStates(true)
updateWhepButtonText()
}
SCPlayerState.SCPlayerStateFailed -> runOnUiThread {
isWhepPlaying = false
updateStatus(playStatus = "WHEP失败: ${detail ?: "未知错误"}"); updatePlayButtonStates(true)
updateWhepButtonText()
}
else -> { }
}
}
}
/** 计算当前选中分辨率,统一竖屏(宽<高) */
private fun currentResolution(): Pair<Int, Int> {
var (w, h) = when (binding.resolutionGroup.checkedRadioButtonId) {
R.id.res360p -> 360 to 640
R.id.res540p -> 540 to 960
R.id.res720p -> 720 to 1280
else -> 720 to 1280
}
if (w > h) { val t = w; w = h; h = t }
return w to h
}
/** 根据不同协议组装播放 URL */
private fun buildPlayUrl(protocolType: String, appName: String, streamName: String): String {
return when (protocolType) {
"RTMP" -> {
// RTMP 播放格式: rtmp://rtmp.sellycloud.pull/appName/streamName
"rtmp://rtmp.sellycloud.pull/$appName/$streamName"
}
"WHEP" -> {
// WHEP 播放格式 (WHIP推流对应WHEP拉流): https://rtmp.sellycloud.pull/whep/appName/streamName
"http://rtmp.sellycloud.pull/$appName/$streamName"
}
else -> {
// 默认使用 RTMP
""
}
}
}
/** 确保播放 Surface 的像素格式与叠放层设置正确(防止再次播放偏蓝) */
private fun ensurePlaySurfaceFormat() {
playSurfaceManager.ensureOpaqueFormat()
}
/** 使用 PixelCopy 截取 Surface 内容并保存到相册Android 8.0+)。更低版本给出提示。 */
private fun captureSurfaceViewAndSave(view: View?, prefix: String) {
if (view == null) { Toast.makeText(this, "当前没有可用的视图进行截图", Toast.LENGTH_SHORT).show(); return }
if (view.width <= 0 || view.height <= 0) { Toast.makeText(this, "视图尚未布局完成,稍后再试", Toast.LENGTH_SHORT).show(); return }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Toast.makeText(this, "当前系统版本不支持该截图方式(需Android 8.0+)", Toast.LENGTH_LONG).show(); return
}
// 仅支持 SurfaceView/其子类
if (view !is android.view.SurfaceView) {
Toast.makeText(this, "当前视图不支持截图", Toast.LENGTH_SHORT).show(); return
}
val bmp = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
try {
val handler = android.os.Handler(mainLooper)
android.view.PixelCopy.request(view, bmp, { result ->
if (result == android.view.PixelCopy.SUCCESS) {
coroutineScope.launch(Dispatchers.IO) {
val ok = saveBitmapToGallery(bmp, prefix)
launch(Dispatchers.Main) {
Toast.makeText(this@MainActivity, if (ok) "截图已保存到相册" else "保存失败", Toast.LENGTH_SHORT).show()
}
}
} else {
Toast.makeText(this, "截图失败,错误码: $result", Toast.LENGTH_SHORT).show()
}
}, handler)
} catch (e: Exception) {
Toast.makeText(this, "截图异常: ${e.message}", Toast.LENGTH_LONG).show()
}
}
/** 保存位图到系统相册按API等级分别处理 */
private fun saveBitmapToGallery(bitmap: Bitmap, prefix: String): Boolean {
val filename = "${prefix}_${java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date())}.png"
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val values = android.content.ContentValues().apply {
put(android.provider.MediaStore.Images.Media.DISPLAY_NAME, filename)
put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/png")
put(android.provider.MediaStore.Images.Media.RELATIVE_PATH, "Pictures/")
put(android.provider.MediaStore.Images.Media.IS_PENDING, 1)
}
val resolver = contentResolver
val uri = resolver.insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
resolver.openOutputStream(uri)?.use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}
values.clear()
values.put(android.provider.MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(uri, values, null, null)
true
} else false
} else {
// API 29 以下保存到公共图片目录需要WRITE_EXTERNAL_STORAGE权限已在Manifest按maxSdk申明
val picturesDir = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_PICTURES)
val targetDir = java.io.File(picturesDir, "RTMPDemo").apply { if (!exists()) mkdirs() }
val file = java.io.File(targetDir, filename)
java.io.FileOutputStream(file).use { out -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) }
// 通知相册扫描
val values = android.content.ContentValues().apply {
put(android.provider.MediaStore.Images.Media.DATA, file.absolutePath)
put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/png")
put(android.provider.MediaStore.Images.Media.DISPLAY_NAME, filename)
}
contentResolver.insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
true
}
} catch (e: Exception) {
Log.e("MainActivity", "saveBitmapToGallery error", e)
false
}
}
private fun bundleToMap(bundle: Bundle?): Map<String, Any?> {
if (bundle == null) return emptyMap()
val map = mutableMapOf<String, Any?>()
for (key in bundle.keySet()) {
val value = bundle.get(key)
map[key] = when (value) {
is Bundle -> bundleToMap(value)
is IntArray -> value.toList()
is LongArray -> value.toList()
is FloatArray -> value.toList()
is DoubleArray -> value.toList()
is BooleanArray -> value.toList()
is ByteArray -> value.joinToString(prefix = "[", postfix = "]")
is Array<*> -> value.toList()
else -> value
}
}
return map
}
override fun onSupportNavigateUp(): Boolean {
onBackPressedDispatcher.onBackPressed()
return true
}
}

View File

@@ -1,155 +0,0 @@
package com.demo.SellyCloudSDK.live
import android.os.Bundle
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.demo.SellyCloudSDK.R
import com.sellycloud.sellycloudsdk.MultiRtmpPlayer
import com.sellycloud.sellycloudsdk.PlayerConfig
class MultiPlayActivity : AppCompatActivity(), MultiRtmpPlayer.MultiRtmpPlayerListener {
private lateinit var etNewUrl: EditText
private lateinit var btnAddStream: Button
private lateinit var btnStartAll: Button
private lateinit var btnStopAll: Button
private lateinit var streamsContainer: LinearLayout
private lateinit var multiPlayer: MultiRtmpPlayer
private var streamCounter = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_play)
etNewUrl = findViewById(R.id.etNewUrl)
btnAddStream = findViewById(R.id.btnAddStream)
btnStartAll = findViewById(R.id.btnStartAll)
btnStopAll = findViewById(R.id.btnStopAll)
streamsContainer = findViewById(R.id.streamsContainer)
multiPlayer = MultiRtmpPlayer(this, this, PlayerConfig.forRtmpLive())
btnAddStream.setOnClickListener {
val urlInput = etNewUrl.text.toString().trim()
val id = "stream_${streamCounter++}"
val config = if (urlInput.isEmpty()) {
// 未输入 URL 时,示例启用 Kiwi 使用默认 rs 标识,可按需替换
PlayerConfig.forRtmpLive(enableKiwi = true, rsname = "123")
} else {
PlayerConfig.forRtmpLive()
}
val ok = multiPlayer.addStream(id, urlInput.ifEmpty { "rtmp://placeholder/kiwi" }, config)
if (!ok) {
Toast.makeText(this, "添加失败ID重复", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
addStreamItemView(id)
}
btnStartAll.setOnClickListener {
multiPlayer.currentStreams().forEach { id ->
if (multiPlayer.isPrepared(id)) multiPlayer.start(id) else multiPlayer.prepareAsync(id)
}
}
btnStopAll.setOnClickListener {
multiPlayer.currentStreams().forEach { id -> multiPlayer.stop(id) }
}
}
private fun addStreamItemView(streamId: String) {
val item = LayoutInflater.from(this).inflate(R.layout.item_stream_player, streamsContainer, false)
val tvTitle = item.findViewById<TextView>(R.id.tvTitle)
val tvStatus = item.findViewById<TextView>(R.id.tvStatus)
val surfaceView = item.findViewById<SurfaceView>(R.id.surfaceView)
val btnPrepare = item.findViewById<Button>(R.id.btnPrepare)
val btnStart = item.findViewById<Button>(R.id.btnStart)
val btnStop = item.findViewById<Button>(R.id.btnStop)
val btnRemove = item.findViewById<Button>(R.id.btnRemove)
tvTitle.text = "流: $streamId"
tvStatus.text = "状态: 已添加,待准备"
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
multiPlayer.setSurface(streamId, holder.surface)
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {
multiPlayer.setSurface(streamId, null)
}
})
btnPrepare.setOnClickListener {
tvStatus.text = "状态: 准备中"
multiPlayer.prepareAsync(streamId)
}
btnStart.setOnClickListener {
if (multiPlayer.isPrepared(streamId)) {
multiPlayer.start(streamId)
} else {
Toast.makeText(this, "请先准备该流", Toast.LENGTH_SHORT).show()
}
}
btnStop.setOnClickListener { multiPlayer.stop(streamId) }
btnRemove.setOnClickListener {
multiPlayer.release(streamId)
streamsContainer.removeView(item)
}
streamsContainer.addView(item)
}
override fun onDestroy() {
super.onDestroy()
multiPlayer.releaseAll()
}
// MultiRtmpPlayerListener 实现
override fun onPlayerPrepared(streamId: String) {
updateStatus(streamId, "准备完成")
}
override fun onPlayerStarted(streamId: String) {
updateStatus(streamId, "播放中")
}
override fun onPlayerError(streamId: String, error: String) {
updateStatus(streamId, "错误: $error")
}
override fun onPlayerCompleted(streamId: String) {
updateStatus(streamId, "播放完成")
}
override fun onPlayerBuffering(streamId: String, percent: Int) {
updateStatus(streamId, "缓冲中...$percent%")
}
override fun onPlayerInfo(streamId: String, what: Int, extra: Int) {
// 可按需处理更多 info
}
private fun updateStatus(streamId: String, status: String) {
runOnUiThread {
for (i in 0 until streamsContainer.childCount) {
val item = streamsContainer.getChildAt(i)
val title = item.findViewById<TextView>(R.id.tvTitle)
if (title.text.endsWith(streamId)) {
val tvStatus = item.findViewById<TextView>(R.id.tvStatus)
tvStatus.text = "状态: $status"
break
}
}
}
}
}

View File

@@ -1,30 +0,0 @@
package com.demo.SellyCloudSDK.live
import android.graphics.Color
import android.graphics.PixelFormat
import android.view.SurfaceHolder
import android.view.SurfaceView
/**
* Minimal manager for the playback SurfaceView to keep MainActivity lean.
* Encapsulates pixel format setup and clearing the surface to black.
*/
class PlaySurfaceManager(private val surfaceView: SurfaceView) {
// 用于视频播放,使用 OPAQUE 格式避免颜色问题
fun ensureOpaqueFormat() {
surfaceView.setZOrderMediaOverlay(false)
surfaceView.setZOrderOnTop(false)
surfaceView.holder.setFormat(PixelFormat.OPAQUE)
}
fun clear() {
val holder: SurfaceHolder = surfaceView.holder
try {
val canvas = holder.lockCanvas()
if (canvas != null) {
canvas.drawColor(Color.BLACK)
holder.unlockCanvasAndPost(canvas)
}
} catch (_: Exception) {}
}
}

View File

@@ -1,53 +0,0 @@
package com.demo.SellyCloudSDK.live
import android.graphics.Color
import android.view.SurfaceHolder
import com.demo.SellyCloudSDK.databinding.ActivityMainBinding
/**
* Thin UI state helper to centralize status text and button states.
* MainActivity delegates to this manager to reduce duplication.
* No business logic is changed.
*/
class UiStateManager(private val binding: ActivityMainBinding) {
fun setPushStatusText(text: String, currentPlayStatus: String) {
binding.tvStatus.text = "推流状态: $text | 播放状态: $currentPlayStatus"
}
fun setPlayStatusText(currentPushStatus: String, playStatus: String) {
binding.tvStatus.text = "推流状态: $currentPushStatus | 播放状态: $playStatus"
}
fun setPushButtonsEnabled(isPushing: Boolean) {
binding.btnStartPush.isEnabled = !isPushing
binding.btnStopPush.isEnabled = isPushing
}
// Keep the RTMP play button enabled so it can serve as a Start/Stop toggle
fun setPlayButtonEnabled(@Suppress("UNUSED_PARAMETER") enabled: Boolean) {
binding.btnPlay.isEnabled = true
}
fun setRtmpButtonText(isPlaying: Boolean) {
binding.btnPlay.text = if (isPlaying) "停止播放(RTMP)" else "开始播放(RTMP)"
}
fun setWhepButtonText(isWhepPlaying: Boolean) {
binding.btnWhepPlay.text = if (isWhepPlaying) "停止播放(WHEP)" else "开始播放(WHEP)"
}
fun setPushPreviewHeader(mode: String) {
binding.tvPushPreviewHeader.text = "📹 推流预览($mode"
}
fun clearSurface(holder: SurfaceHolder) {
try {
val canvas = holder.lockCanvas()
if (canvas != null) {
canvas.drawColor(Color.BLACK)
holder.unlockCanvasAndPost(canvas)
}
} catch (_: Exception) {}
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="220"
android:fromYDelta="100%p"
android:toYDelta="0%p"
android:interpolator="@android:interpolator/decelerate_quad" />
</set>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="180"
android:fromYDelta="0%p"
android:toYDelta="100%p"
android:interpolator="@android:interpolator/accelerate_quad" />
</set>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/av_text_primary" android:state_checked="true" />
<item android:color="@color/av_text_secondary" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/brand_primary" android:state_selected="true" />
<item android:color="@color/av_tab_inactive" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_stats_bg" />
<corners android:radius="@dimen/av_corner_large" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_card_fill" />
<corners android:radius="@dimen/av_corner_large" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_modal_fill_gray" />
<corners android:radius="@dimen/av_corner_large" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/brand_primary" />
<corners android:radius="@dimen/av_corner_large" />
</shape>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/av_card_fill" />
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_card_fill" />
<stroke
android:width="1dp"
android:color="@color/av_input_border" />
<corners android:radius="@dimen/av_corner_small" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_primary_disabled" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/brand_primary" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_primary_pressed" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_button_red_disabled" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_button_red" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_button_red_pressed" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_card_fill" />
<stroke
android:width="1dp"
android:color="@color/av_input_border" />
<corners
android:topLeftRadius="@dimen/av_corner_small"
android:bottomLeftRadius="@dimen/av_corner_small"
android:topRightRadius="0dp"
android:bottomRightRadius="0dp" />
</shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_card_fill" />
<stroke
android:width="1dp"
android:color="@color/av_input_border" />
</shape>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_card_fill" />
<stroke
android:width="1dp"
android:color="@color/av_input_border" />
<corners
android:topLeftRadius="0dp"
android:bottomLeftRadius="0dp"
android:topRightRadius="@dimen/av_corner_small"
android:bottomRightRadius="@dimen/av_corner_small" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_segment_bg" />
<corners android:radius="@dimen/av_corner_small" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_card_fill" />
<corners
android:topLeftRadius="18dp"
android:topRightRadius="18dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_stats_bg" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/av_action_circle_bg" />
</shape>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/av_stats_red" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#80000000" />
<corners android:radius="10dp" />
</shape>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#80FFFFFF" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="16dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/brand_primary" />
<corners android:radius="10dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/av_segment_bg" />
<corners android:radius="@dimen/av_corner_medium" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/brand_primary" />
<corners android:radius="@dimen/av_corner_large" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M20,5h-3.17l-1.84,-2H9.01L7.17,5H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2zM20,19H4V7h4.05l1.83,-2h4.25l1.83,2H20v12zM12,9c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,15c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19,6.41 17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5V19h14v-2.5C15,14.17 10.33,13 8,13zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45V19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M21,3H3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM21,19H3V5h18v14zM11,16l5,-3 -5,-3v6z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,11c-1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2zM12,2C7.03,2 3,6.03 3,11c0,3.87 2.45,7.19 5.88,8.54l1.5,-2.6C7.82,15.76 6,13.54 6,11c0,-3.31 2.69,-6 6,-6s6,2.69 6,6c0,2.54 -1.82,4.76 -4.38,5.94l1.5,2.6C18.55,18.19 21,14.87 21,11c0,-4.97 -4.03,-9 -9,-9z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M8,5v14l11,-7z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.96 -0.7,2.8l1.46,1.46C19.54,16.46 20,14.79 20,13c0,-4.42 -3.58,-8 -8,-8zM11,11v5H9v-3H7v3c0,1.1 0.9,2 2,2h2c1.1,0 2,-0.9 2,-2v-5c0,-1.1 -0.9,-2 -2,-2H9c-1.1,0 -2,0.9 -2,2v1h2v-1h2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17,10.5V7c0,-1.1 -0.9,-2 -2,-2H5C3.9,5 3,5.9 3,7v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-3.5l4,4v-11l-4,4z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.56,15.3 18.78,15.5 20,15.5C20.55,15.5 21,15.95 21,16.5V20C21,20.55 20.55,21 20,21C10.61,21 3,13.39 3,4C3,3.45 3.45,3 4,3H7.5C8.05,3 8.5,3.45 8.5,4C8.5,5.22 8.7,6.44 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94 0,-0.32 -0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.11,-0.2 -0.36,-0.28 -0.57,-0.2l-2.39,0.96c-0.5,-0.38 -1.04,-0.7 -1.62,-0.94L14.96,2.5c-0.03,-0.21 -0.21,-0.36 -0.43,-0.36h-3.84c-0.22,0 -0.4,0.15 -0.43,0.36l-0.36,2.54c-0.58,0.24 -1.12,0.56 -1.62,0.94l-2.39,-0.96c-0.21,-0.08 -0.46,0 -0.57,0.2l-1.92,3.32c-0.11,0.2 -0.06,0.47 0.12,0.61l2.03,1.58c-0.04,0.31 -0.06,0.62 -0.06,0.94 0,0.31 0.02,0.63 0.06,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.11,0.2 0.36,0.28 0.57,0.2l2.39,-0.96c0.5,0.38 1.04,0.7 1.62,0.94l0.36,2.54c0.03,0.21 0.21,0.36 0.43,0.36h3.84c0.22,0 0.4,-0.15 0.43,-0.36l0.36,-2.54c0.58,-0.24 1.12,-0.56 1.62,-0.94l2.39,0.96c0.21,0.08 0.46,0 0.57,-0.2l1.92,-3.32c0.11,-0.2 0.06,-0.47 -0.12,-0.61l-2.03,-1.58zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M3,9v6h4l5,5V4L7,9H3z" />
</vector>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2l1.7,5.1L19,9l-5.3,1.7L12,16l-1.7-5.3L5,9l5.3-1.9L12,2z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M18.5,3l0.7,2.1L21.5,6l-2.3,0.7L18.5,9l-0.7-2.3L15.5,6l2.3-0.9L18.5,3z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M7.41,15.41 12,10.83 16.59,15.41 18,14 12,8 6,14z" />
</vector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M4,5c0,-1.105 0.895,-2 2,-2h12c1.105,0 2,0.895 2,2v14c0,1.105 -0.895,2 -2,2H6c-1.105,0 -2,-0.895 -2,-2V5zM6,5h12v14H6V5z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M8.5,11.5l2.5,3 1.5,-2 3.5,4H8z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M10,9a1.25,1.25 0,1 0,0.01 0z" />
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,14c1.66,0 3,-1.34 3,-3V5c0,-1.66 -1.34,-3 -3,-3s-3,1.34 -3,3v6c0,1.66 1.34,3 3,3z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M17,11h-2c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3H7c0,2.76 2.24,5 5,5v3h2v-3c2.76,0 5,-2.24 5,-5z" />
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M16.5,11c0,0.36 -0.06,0.71 -0.16,1.04l-1.48,-1.48c0.09,-0.18 0.14,-0.37 0.14,-0.56V5c0,-1.66 -1.34,-3 -3,-3 -0.64,0 -1.23,0.2 -1.72,0.54l1.37,1.37C11.78,3.73 11.89,3.5 12,3.5c0.83,0 1.5,0.67 1.5,1.5v6c0,0.11 -0.02,0.22 -0.05,0.32L16.5,11z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M4,3l17,17 -1.41,1.41 -5.09,-5.09V19h-2v-3c-2.76,0 -5,-2.24 -5,-5h2c0,1.66 1.34,3 3,3 0.49,0 0.95,-0.12 1.35,-0.33L5.41,4.41 4,3z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M7,2h10a2,2 0 0 1 2,2v16a2,2 0 0 1 -2,2H7a2,2 0 0 1 -2,-2V4a2,2 0 0 1 2,-2zM9,4h6v16H9z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M4,20h16v-2H4zM6,17h3V10H6zM11,17h3V6h-3zM16,17h3V13h-3z" />
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M9,9h6v6H9z" />
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M7,7h10v10H7zM5,5v2H3v2h2v6H3v2h2v2h2v-2h10v2h2v-2h2v-2h-2V9h2V7h-2V5h-2v2H7V5H5z" />
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M4,6c0,-1.105 0.895,-2 2,-2h12c1.105,0 2,0.895 2,2v12c0,1.105 -0.895,2 -2,2H6c-1.105,0 -2,-0.895 -2,-2V6zM6,6h12v12H6V6z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M8,9h8v2H8zM8,13h5v2H8z" />
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9 9,-4.03 9,-9 -4.03,-9 -9,-9zM12,19c-3.86,0 -7,-3.14 -7,-7s3.14,-7 7,-7 7,3.14 7,7 -3.14,7 -7,7z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M11,12l6,-3 -3,6z" />
</vector>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M12,2C6.477,2 2,6.477 2,12s4.477,10 10,10 10,-4.477 10,-10S17.523,2 12,2zM4,12c0,-4.418 3.582,-8 8,-8s8,3.582 8,8 -3.582,8 -8,8 -8,-3.582 -8,-8z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,4c2.21,0 4,3.582 4,8s-1.79,8 -4,8 -4,-3.582 -4,-8 1.79,-8 4,-8z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M4.5,12h15v1.5h-15z" />
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2c-5.52,0 -10,4.48 -10,10s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,6h-1.2v6l5.2,3.1 0.6,-1 -4.6,-2.7z" />
</vector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,6h-3.17L15,4H9L7.17,6H4c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2zM12,19c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M12,9.5c1.93,0 3.5,1.57 3.5,3.5S13.93,16.5 12,16.5 8.5,14.93 8.5,13 10.07,9.5 12,9.5z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M7,12h2l-2.5,-2.5L4,12h2c0,2.21 1.79,4 4,4v-2c-1.1,0 -2,-0.9 -2,-2z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M17,12c0,-2.21 -1.79,-4 -4,-4v2c1.1,0 2,0.9 2,2h-2l2.5,2.5L20,12z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M17,10.5V7c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2v-3.5l4,4v-11l-4,4z" />
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M4,3l17,17 -1.41,1.41 -2.59,-2.59H5c-1.1,0 -2,-0.9 -2,-2V7c0,-0.9 0.6,-1.67 1.42,-1.91L2.59,3.41 4,3z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M21,6.5v11l-4,-4v3.5c0,0.22 -0.04,0.43 -0.1,0.63L6.37,7.1C6.57,7.04 6.78,7 7,7h8c1.1,0 2,0.9 2,2v3.5l4,-4z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,5C7,5 3.2,8.1 2,12c1.2,3.9 5,7 10,7s8.8,-3.1 10,-7c-1.2,-3.9 -5,-7 -10,-7zm0,12c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zm0,-8a3,3 0 1,0 0,6 3,3 0 0,0 0,-6z" />
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,5C7,5 3.2,8.1 2,12c1.2,3.9 5,7 10,7s8.8,-3.1 10,-7c-1.2,-3.9 -5,-7 -10,-7zm0,12c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zm0,-8a3,3 0 1,0 0,6 3,3 0 0,0 0,-6z" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M4,4 L20,20"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeWidth="2" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,17a2,2 0 0,0 2,-2v-2a2,2 0 0,0 -4,0v2a2,2 0 0,0 2,2zm6,-8h-1V7a5,5 0 0,0 -10,0v2H6a2,2 0 0,0 -2,2v8a2,2 0 0,0 2,2h12a2,2 0 0,0 2,-2v-8a2,2 0 0,0 -2,-2zm-3,0H9V7a3,3 0 0,1 6,0v2z" />
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/brand_primary"
android:fillAlpha="0.75"
android:pathData="M6,2 H14 A3,3 0 0,1 17,5 V11 A3,3 0 0,1 14,14 H6 A3,3 0 0,1 3,11 V5 A3,3 0 0,1 6,2 Z" />
<path
android:fillColor="@color/brand_primary"
android:pathData="M10,6 H18 A3,3 0 0,1 21,9 V15 A3,3 0 0,1 18,18 H10 A3,3 0 0,1 7,15 V9 A3,3 0 0,1 10,6 Z" />
<path
android:fillColor="@color/brand_primary_text_on"
android:pathData="M13,10 L13,14 L16.5,12 Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zm0,2c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_av_primary_button_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/bg_av_primary_button_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/bg_av_primary_button_normal" />
</selector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_av_red_button_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/bg_av_red_button_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/bg_av_red_button_normal" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_av_segment_checked_left" android:state_checked="true" />
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_av_segment_checked_middle" android:state_checked="true" />
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg_av_segment_checked_right" android:state_checked="true" />
<item android:drawable="@android:color/transparent" />
</selector>

View File

@@ -3,150 +3,664 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#FFFFFF" android:background="@color/av_background">
android:padding="16dp">
<TextView <TextView
android:id="@+id/tvHubSubtitle" android:id="@+id/tvTitle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/hub_subtitle_basic" android:layout_marginTop="12dp"
android:textAllCaps="false" android:gravity="center"
android:textColor="#101215" android:text="@string/tab_home"
android:textColor="@color/av_text_primary"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
<ScrollView android:id="@+id/pageContainer"
android:id="@+id/scrollCards"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="16dp" app:layout_constraintBottom_toTopOf="@id/vTabDivider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTitle">
<!-- Home -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pageHome"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/av_screen_padding_home"
android:paddingEnd="@dimen/av_screen_padding_home"
android:paddingTop="18dp"
android:paddingBottom="12dp">
<LinearLayout
android:id="@+id/homeActionRow"
android:layout_width="0dp"
android:layout_height="@dimen/av_home_button_height"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/btnHomeLivePush"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_av_home_button"
android:clickable="true"
android:clipToOutline="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:contentDescription="@string/home_live_push"
android:src="@drawable/ic_av_live_push"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/home_live_push"
android:textColor="@color/brand_primary_text_on"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="12dp"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/btnHomeLivePull"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/bg_av_home_button"
android:clickable="true"
android:clipToOutline="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:contentDescription="@string/home_live_pull"
android:src="@drawable/ic_av_live_pull"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/home_live_pull"
android:textColor="@color/brand_primary_text_on"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tvHomeSquareTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/home_live_square_title"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/homeActionRow" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeHome"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHomeSquareTitle">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvLiveSquare"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="12dp"
android:overScrollMode="never" />
<TextView
android:id="@+id/tvHomeEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="@string/home_live_square_empty"
android:textColor="@color/av_text_hint"
android:textSize="14sp"
android:visibility="gone" />
</FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Call -->
<ScrollView
android:id="@+id/pageCall"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/av_screen_padding"
android:paddingTop="28dp"
android:paddingEnd="@dimen/av_screen_padding"
android:paddingBottom="28dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/call_channel_id"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etCallChannelId"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:hint="@string/default_call_id"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<LinearLayout
android:id="@+id/btnCallSingleChat"
android:layout_width="match_parent"
android:layout_height="@dimen/av_home_button_height"
android:layout_marginTop="20dp"
android:background="@drawable/bg_av_home_button"
android:clickable="true"
android:clipToOutline="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:contentDescription="@string/home_single_chat"
android:src="@drawable/ic_av_single_chat"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/home_single_chat"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnCallConference"
android:layout_width="match_parent"
android:layout_height="@dimen/av_home_button_height"
android:layout_marginTop="14dp"
android:background="@drawable/bg_av_home_button"
android:clickable="true"
android:clipToOutline="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:contentDescription="@string/home_conference"
android:src="@drawable/ic_av_conference"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/home_conference"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- Settings -->
<ScrollView
android:id="@+id/pageSettings"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/av_screen_padding"
android:paddingTop="18dp"
android:paddingEnd="@dimen/av_screen_padding"
android:paddingBottom="28dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/push_settings_stream_id"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsStreamId"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings_resolution"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgSettingsResolution"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_segment_container"
android:checkedButton="@+id/rbSettingsRes720p"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbSettingsRes360p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_left"
android:button="@null"
android:gravity="center"
android:text="360p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbSettingsRes480p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_middle"
android:button="@null"
android:gravity="center"
android:text="480p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbSettingsRes540p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_middle"
android:button="@null"
android:gravity="center"
android:text="540p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbSettingsRes720p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_right"
android:button="@null"
android:gravity="center"
android:text="720p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings_fps"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsFps"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="30"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings_max_bitrate"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsMaxBitrate"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="2000"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/settings_min_bitrate"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsMinBitrate"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="500"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/settings_env_title"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/settings_vhost"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsVhost"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="textUri"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_vhost_key"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsVhostKey"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="textPassword"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_app_id"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etSettingsAppId"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<Button
android:id="@+id/btnSaveSettings"
android:layout_width="match_parent"
android:layout_height="@dimen/av_primary_button_height"
android:layout_marginTop="24dp"
android:background="@drawable/selector_av_primary_button"
android:text="@string/settings_save"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnLogout"
android:layout_width="match_parent"
android:layout_height="@dimen/av_primary_button_height"
android:layout_marginTop="12dp"
android:background="@drawable/selector_av_red_button"
android:text="@string/logout_action"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</ScrollView>
</FrameLayout>
<View
android:id="@+id/vTabDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/av_divider"
app:layout_constraintBottom_toTopOf="@id/tabBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<LinearLayout
android:id="@+id/tabBar"
android:layout_width="0dp"
android:layout_height="@dimen/av_tab_height"
android:background="@color/av_background"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent">
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHubSubtitle"
app:layout_constraintVertical_bias="0.0">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/tabHome"
android:layout_height="wrap_content" android:layout_width="0dp"
android:orientation="vertical"> android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="10dp">
<androidx.cardview.widget.CardView <ImageView
android:id="@+id/cardLiveStreaming" android:id="@+id/ivTabHome"
android:layout_width="match_parent" android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/tab_home"
android:duplicateParentState="true"
android:src="@drawable/ic_av_tab_home"
app:tint="@color/av_tab_text" />
<TextView
android:id="@+id/tvTabHome"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="12dp" android:layout_marginTop="2dp"
android:clickable="true" android:duplicateParentState="true"
android:focusable="true" android:text="@string/tab_home"
android:foreground="?attr/selectableItemBackground" android:textColor="@color/av_tab_text"
app:cardBackgroundColor="@color/brand_primary" android:textSize="12sp" />
app:cardCornerRadius="10dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="64dp"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/live_streaming_title"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/live_streaming_subtitle"
android:textColor="@color/brand_primary_text_sub"
android:textSize="12sp" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_next"
android:contentDescription="@null"
app:tint="#E6FFFFFF" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardInteractiveLive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="@color/brand_primary"
app:cardCornerRadius="10dp"
app:cardElevation="2dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="64dp"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/interactive_live_title"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="@string/interactive_live_subtitle"
android:textColor="@color/brand_primary_text_sub"
android:textSize="12sp" />
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_media_next"
android:contentDescription="@null"
app:tint="#E6FFFFFF" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout> </LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/tabCall"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="10dp">
<ImageView
android:id="@+id/ivTabCall"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/tab_call"
android:duplicateParentState="true"
android:src="@drawable/ic_av_tab_call"
app:tint="@color/av_tab_text" />
<TextView
android:id="@+id/tvTabCall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:duplicateParentState="true"
android:text="@string/tab_call"
android:textColor="@color/av_tab_text"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/tabSettings"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="10dp">
<ImageView
android:id="@+id/ivTabSettings"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/tab_settings"
android:duplicateParentState="true"
android:src="@drawable/ic_av_tab_settings"
app:tint="@color/av_tab_text" />
<TextView
android:id="@+id/tvTabSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:duplicateParentState="true"
android:text="@string/tab_settings"
android:textColor="@color/av_tab_text"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/av_surface_black">
<FrameLayout
android:id="@+id/renderContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_av_icon_circle"
android:contentDescription="@string/close"
android:src="@drawable/ic_av_close"
app:tint="@color/av_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/controlBar"
android:layout_width="0dp"
android:layout_height="@dimen/av_control_bar_height"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_av_control_bar"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:id="@+id/actionPlay"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/play_ctrl_play"
android:src="@drawable/ic_av_play"
app:tint="@color/brand_primary_text_on" />
<TextView
android:id="@+id/tvPlayLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/play_ctrl_play"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionMute"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/play_ctrl_mute"
android:src="@drawable/ic_av_volume"
app:tint="@color/brand_primary_text_on" />
<TextView
android:id="@+id/tvMuteLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/play_ctrl_mute"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionScreenshot"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/play_ctrl_screenshot"
android:src="@drawable/ic_av_camera"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/play_ctrl_screenshot"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionSeek10"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/play_ctrl_seek_10"
android:src="@drawable/ic_av_replay_10"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/play_ctrl_seek_10"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,523 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/av_surface_black">
<FrameLayout
android:id="@+id/previewContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/statsPanel"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:background="@drawable/bg_av_stats_panel"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingTop="10dp"
android:paddingEnd="12dp"
android:paddingBottom="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivStatsTitleIcon"
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats"
app:tint="@color/brand_primary_text_on" />
<TextView
android:id="@+id/tvStatsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="@string/live_stats_title"
android:textColor="@color/brand_primary_text_on"
android:textSize="14sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@+id/ivStatsCollapse"
android:layout_width="22dp"
android:layout_height="22dp"
android:background="@drawable/bg_live_action_circle"
android:contentDescription="@null"
android:src="@drawable/ic_live_chevron_up"
app:tint="@color/brand_primary_text_on" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:alpha="0.12"
android:background="@color/brand_primary_text_on" />
<LinearLayout
android:id="@+id/statsContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats_protocol"
app:tint="@color/brand_primary_text_sub" />
<TextView
android:id="@+id/tvStatsProtocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="rtmp"
android:textColor="@color/brand_primary_text_sub"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats_cpu"
app:tint="@color/brand_primary_text_sub" />
<TextView
android:id="@+id/tvStatsCpuApp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="App CPU: 0%"
android:textColor="@color/av_stats_green"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats_cpu"
app:tint="@color/brand_primary_text_sub" />
<TextView
android:id="@+id/tvStatsCpuSys"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Sys CPU: 0%"
android:textColor="@color/av_stats_green"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats_kbps"
app:tint="@color/brand_primary_text_sub" />
<TextView
android:id="@+id/tvStatsKbps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="0 kbps (A:0 V:0)"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats_fps"
app:tint="@color/brand_primary_text_sub" />
<TextView
android:id="@+id/tvStatsFps"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="FPS: 0"
android:textColor="@color/av_stats_red"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:src="@drawable/ic_live_stats_rtt"
app:tint="@color/brand_primary_text_sub" />
<TextView
android:id="@+id/tvStatsRtt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="RTT: 0 ms"
android:textColor="@color/av_stats_green"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/topRightActions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btnPushSettings"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/bg_live_action_circle"
android:contentDescription="@string/settings_title"
android:src="@drawable/ic_av_tab_settings"
app:tint="@color/brand_primary_text_on" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:background="@drawable/bg_live_action_circle"
android:contentDescription="@string/close"
android:src="@drawable/ic_av_close"
app:tint="@color/brand_primary_text_on" />
</LinearLayout>
<View
android:id="@+id/bottomAnchor"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<LinearLayout
android:id="@+id/controlBar"
android:layout_width="0dp"
android:layout_height="@dimen/av_control_bar_height"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:background="@drawable/bg_av_control_bar"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:id="@+id/actionFlip"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/push_tool_flip"
android:src="@drawable/ic_live_switch_camera"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/push_tool_flip"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionMute"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/ivToolMute"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/push_tool_mute"
android:src="@drawable/ic_live_mic"
app:tint="@color/brand_primary_text_on" />
<TextView
android:id="@+id/tvToolMuteLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/push_tool_mute"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionCamera"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/ivToolCamera"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/push_tool_camera_off"
android:src="@drawable/ic_live_video"
app:tint="@color/brand_primary_text_on" />
<TextView
android:id="@+id/tvToolCameraLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/push_tool_camera_off"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionBeauty"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/ivToolBeauty"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/push_tool_beauty_off"
android:src="@drawable/ic_live_beauty"
app:tint="@color/brand_primary_text_on" />
<TextView
android:id="@+id/tvToolBeautyLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/push_tool_beauty_off"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionScreenshot"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/push_tool_screenshot"
android:src="@drawable/ic_av_camera"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/push_tool_screenshot"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/actionBackground"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/ivToolBackground"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/push_tool_background"
android:src="@drawable/ic_live_image"
app:tint="@color/brand_primary_text_on" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/push_tool_background"
android:textColor="@color/brand_primary_text_on"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bottomBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierAllowsGoneWidgets="false"
app:barrierDirection="top"
app:constraint_referenced_ids="controlBar,bottomAnchor" />
<LinearLayout
android:id="@+id/bottomStartRow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="26dp"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/bottomBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/btnStartStopLive"
android:layout_width="220dp"
android:layout_height="52dp"
android:background="@drawable/selector_av_red_button"
android:text="@string/push_start_live"
android:textColor="@color/brand_primary_text_on"
android:textSize="18sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/quickActions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageButton
android:id="@+id/btnQuickFlip"
android:layout_width="52dp"
android:layout_height="52dp"
android:background="@drawable/bg_live_action_circle"
android:contentDescription="@string/push_tool_flip"
android:src="@drawable/ic_live_switch_camera"
app:tint="@color/brand_primary_text_on" />
<ImageButton
android:id="@+id/btnQuickOrientation"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginStart="10dp"
android:background="@drawable/bg_live_action_circle"
android:contentDescription="@string/push_stream_orientation"
android:src="@drawable/ic_live_orientation"
app:tint="@color/brand_primary_text_on" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/av_background">
<LinearLayout
android:id="@+id/loginContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="@dimen/av_screen_padding"
android:paddingEnd="@dimen/av_screen_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:layout_width="72dp"
android:layout_height="72dp"
android:gravity="center">
<ImageView
android:layout_width="83dp"
android:layout_height="72dp"
android:layout_gravity="center"
android:contentDescription="@string/login_logo_desc"
android:src="@drawable/ic_login_logo" />
</FrameLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/login_welcome"
android:textColor="@color/av_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="24dp"
android:background="@drawable/bg_login_input_field"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@drawable/ic_login_user"
app:tint="@color/av_text_hint" />
<EditText
android:id="@+id/etUsername"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@null"
android:hint="@string/login_username_hint"
android:importantForAutofill="no"
android:inputType="text"
android:maxLines="1"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="12dp"
android:background="@drawable/bg_login_input_field"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="8dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@drawable/ic_login_lock"
app:tint="@color/av_text_hint" />
<EditText
android:id="@+id/etPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@null"
android:hint="@string/login_password_hint"
android:importantForAutofill="no"
android:inputType="textPassword"
android:imeOptions="actionDone"
android:maxLines="1"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<ImageButton
android:id="@+id/btnTogglePassword"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/login_toggle_password"
android:padding="6dp"
android:src="@drawable/ic_login_eye_off"
app:tint="@color/av_text_hint" />
</LinearLayout>
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="@dimen/av_primary_button_height"
android:layout_marginTop="20dp"
android:background="@drawable/selector_av_primary_button"
android:text="@string/login_action"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,576 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<!-- 顶部:推流行(输入框 + 按钮列) -->
<LinearLayout
android:id="@+id/pushRow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- 推流配置输入区域 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:background="#333333"
android:padding="4dp">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#555555"
android:layout_marginVertical="1dp" />
<EditText
android:id="@+id/etAppName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="App Name (如: live)"
android:text="live"
android:textColor="#FFFFFF"
android:textColorHint="#CCCCCC"
android:textSize="11sp"
android:inputType="text"
android:importantForAutofill="no"
android:padding="2dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#555555"
android:layout_marginVertical="1dp" />
<EditText
android:id="@+id/etStreamName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="Stream Name (如: stream123)"
android:text="stream001"
android:textColor="#FFFFFF"
android:textColorHint="#CCCCCC"
android:textSize="11sp"
android:inputType="text"
android:importantForAutofill="no"
android:padding="2dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#555555"
android:layout_marginVertical="1dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/pushButtonContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="2dp">
<Button
android:id="@+id/btnStartPush"
android:layout_width="wrap_content"
android:minWidth="120dp"
android:layout_height="32dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#4CAF50"
android:text="开始推流"
android:textColor="#FFFFFF"
android:textSize="10sp" />
<Button
android:id="@+id/btnStopPush"
android:layout_width="wrap_content"
android:minWidth="120dp"
android:layout_height="32dp"
android:layout_marginTop="2dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#F44336"
android:text="停止推流"
android:textColor="#FFFFFF"
android:textSize="10sp" />
</LinearLayout>
</LinearLayout>
<!-- 新增协议选择RTMP / WHIP -->
<RadioGroup
android:id="@+id/protocolGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/pushRow">
<RadioButton
android:id="@+id/rbProtocolRtmp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RTMP"
android:checked="true"
android:textColor="#FFFFFF"
android:textSize="10sp" />
<RadioButton
android:id="@+id/rbProtocolWhip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="WHIP"
android:textColor="#FFFFFF"
android:textSize="10sp" />
</RadioGroup>
<!-- 推流控制:放在协议选择下方 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pushControlsContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/protocolGroup">
<HorizontalScrollView
android:id="@+id/resolutionScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnSwitchCamera"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RadioGroup
android:id="@+id/resolutionGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/res360p"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:text="360p"
android:textColor="#FFFFFF"
android:textSize="10sp" />
<RadioButton
android:id="@+id/res540p"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:text="540p"
android:textColor="#FFFFFF"
android:textSize="10sp" />
<RadioButton
android:id="@+id/res720p"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:checked="true"
android:text="720p"
android:textColor="#FFFFFF"
android:textSize="10sp" />
<RadioButton
android:id="@+id/res1080p"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:text="1080p"
android:textColor="#FFFFFF"
android:textSize="10sp" />
</RadioGroup>
</HorizontalScrollView>
<Button
android:id="@+id/btnSwitchCamera"
android:layout_width="wrap_content"
android:minWidth="110dp"
android:layout_height="32dp"
android:layout_marginEnd="2dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#FF9800"
android:text="切换摄像头"
android:textColor="#FFFFFF"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnSwitchOrientation"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap" />
<Button
android:id="@+id/btnSwitchOrientation"
android:layout_width="wrap_content"
android:minWidth="110dp"
android:layout_height="32dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#03A9F4"
android:text="切换方向"
android:textColor="#FFFFFF"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 镜像控制:精简为两个统一镜像(同时作用预览与推流) + 美颜 -->
<HorizontalScrollView
android:id="@+id/mirrorScrollView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
android:scrollbars="none"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/pushControlsContainer">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/cbPreviewHFlip"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:text="水平镜像"
android:textColor="#FFFFFF"
android:textSize="9sp" />
<CheckBox
android:id="@+id/cbPreviewVFlip"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginStart="6dp"
android:text="垂直镜像"
android:textColor="#FFFFFF"
android:textSize="9sp" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchBeauty"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_marginStart="6dp"
android:text="美颜"
android:textColor="#FFFFFF"
android:textSize="9sp"
android:checked="true"
app:thumbTint="#4CAF50"
app:trackTint="#81C784" />
</LinearLayout>
</HorizontalScrollView>
<!-- 播放行(输入框 + 按钮列):放在镜像控制下方 -->
<LinearLayout
android:id="@+id/playRow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/mirrorScrollView">
<!-- 播放配置输入区域 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:background="#333333"
android:padding="4dp">
<EditText
android:id="@+id/etPlayAppName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="Play App Name (如: live)"
android:text="live"
android:textColor="#FFFFFF"
android:textColorHint="#CCCCCC"
android:textSize="11sp"
android:inputType="text"
android:importantForAutofill="no"
android:padding="2dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#555555"
android:layout_marginVertical="1dp" />
<EditText
android:id="@+id/etPlayStreamName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="Play Stream Name (如: stream123)"
android:text="stream001"
android:textColor="#FFFFFF"
android:textColorHint="#CCCCCC"
android:textSize="11sp"
android:inputType="text"
android:importantForAutofill="no"
android:padding="2dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/playButtonContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="2dp">
<Button
android:id="@+id/btnPlay"
android:layout_width="wrap_content"
android:minWidth="140dp"
android:layout_height="32dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#2196F3"
android:text="开始播放(RTMP)"
android:textColor="#FFFFFF"
android:textSize="10sp" />
<Button
android:id="@+id/btnWhepPlay"
android:layout_width="wrap_content"
android:minWidth="140dp"
android:layout_height="32dp"
android:layout_marginTop="2dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#E91E63"
android:text="开始播放(WHEP)"
android:textColor="#FFFFFF"
android:textSize="10sp" />
</LinearLayout>
</LinearLayout>
<!-- 功能按钮区域 -->
<LinearLayout
android:id="@+id/functionButtonsLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
android:orientation="horizontal"
android:weightSum="2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/playRow">
<Button
android:id="@+id/btnChooseImageSource"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:layout_marginEnd="1dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:text="图片作为视频源"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:backgroundTint="#E91E63" />
<Button
android:id="@+id/btnRestoreCamera"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:layout_marginStart="1dp"
android:layout_marginEnd="1dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:backgroundTint="#607D8B"
android:text="恢复摄像头源"
android:textColor="#FFFFFF"
android:textSize="10sp" />
</LinearLayout>
<!-- 新增:截图按钮行 -->
<LinearLayout
android:id="@+id/screenshotButtonsLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
android:orientation="horizontal"
android:weightSum="2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/functionButtonsLayout">
<Button
android:id="@+id/btnCapturePush"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:layout_marginEnd="1dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:text="截图(推流预览)"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:backgroundTint="#607D8B" />
<Button
android:id="@+id/btnCapturePlay"
android:layout_width="0dp"
android:layout_height="32dp"
android:layout_weight="1"
android:layout_marginStart="1dp"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:maxLines="1"
android:text="截图(播放)"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:backgroundTint="#607D8B" />
</LinearLayout>
<!-- 状态信息 -->
<!-- 视频显示区域:剩余空间自适应;设置最小高度 -->
<TextView
android:id="@+id/tvStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="4dp"
android:background="#80000000"
android:padding="4dp"
android:text="推流状态: 待启动 | 播放状态: 待启动"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:singleLine="true"
android:ellipsize="end"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/screenshotButtonsLayout" />
<LinearLayout
android:id="@+id/videoContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="4dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="2dp"
android:orientation="horizontal"
android:weightSum="2"
android:baselineAligned="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvStatus">
<!-- 推流预览 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:background="#222222"
android:orientation="vertical">
<TextView
android:id="@+id/tvPushPreviewHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#555555"
android:gravity="center"
android:padding="3dp"
android:text="📹 推流预览"
android:textColor="#FFFFFF"
android:textSize="11sp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.pedro.library.view.OpenGlView
android:id="@+id/surfaceViewPush"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="160dp" />
<!-- 新增WHIP 预览渲染器,按需显示 -->
<org.webrtc.SurfaceViewRenderer
android:id="@+id/whipPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
<!-- 拉流播放 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="2dp"
android:layout_weight="1"
android:background="#222222"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#555555"
android:gravity="center"
android:padding="3dp"
android:text="📺 拉流播放"
android:textColor="#FFFFFF"
android:textSize="11sp" />
<SurfaceView
android:id="@+id/surfaceViewPlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="160dp" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<EditText
android:id="@+id/etNewUrl"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="输入RTMP地址或留空使用Kiwi"
android:padding="12dp"
android:textColor="#FFFFFF"
android:textColorHint="#CCCCCC"
android:background="#333333"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnAddStream"/>
<Button
android:id="@+id/btnAddStream"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="添加"
android:textColor="#FFFFFF"
android:backgroundTint="#4CAF50"
app:layout_constraintTop_toTopOf="@id/etNewUrl"
app:layout_constraintBottom_toBottomOf="@id/etNewUrl"
app:layout_constraintEnd_toStartOf="@+id/btnStartAll"/>
<Button
android:id="@+id/btnStartAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="全部开始"
android:textColor="#FFFFFF"
android:backgroundTint="#2196F3"
app:layout_constraintTop_toTopOf="@id/etNewUrl"
app:layout_constraintBottom_toBottomOf="@id/etNewUrl"
app:layout_constraintEnd_toStartOf="@+id/btnStopAll"/>
<Button
android:id="@+id/btnStopAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="全部停止"
android:textColor="#FFFFFF"
android:backgroundTint="#F44336"
app:layout_constraintTop_toTopOf="@id/etNewUrl"
app:layout_constraintBottom_toBottomOf="@id/etNewUrl"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"/>
<ScrollView
android:id="@+id/scrollStreams"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@id/etNewUrl"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/streamsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/av_screen_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_av_dialog_card"
android:orientation="vertical"
android:paddingStart="18dp"
android:paddingTop="16dp"
android:paddingEnd="18dp"
android:paddingBottom="18dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/settings_title"
android:textColor="@color/av_text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_alignParentEnd="true"
android:background="@android:color/transparent"
android:contentDescription="@string/push_settings_close"
android:src="@drawable/ic_av_close"
app:tint="@color/brand_primary" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/push_settings_stream_id"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etStreamId"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_resolution"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgResolution"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_segment_container"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbRes360p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_left"
android:button="@null"
android:gravity="center"
android:text="360p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRes480p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_middle"
android:button="@null"
android:gravity="center"
android:text="480p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRes540p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_middle"
android:button="@null"
android:gravity="center"
android:text="540p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRes720p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_right"
android:button="@null"
android:gravity="center"
android:text="720p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_fps"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etFps"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_max_bitrate"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etMaxBitrate"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_min_bitrate"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etMinBitrate"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<Button
android:id="@+id/btnApply"
android:layout_width="match_parent"
android:layout_height="@dimen/av_primary_button_height"
android:layout_marginTop="18dp"
android:background="@drawable/selector_av_primary_button"
android:text="@string/push_settings_apply"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,290 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/av_screen_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_av_dialog_card"
android:orientation="vertical"
android:paddingStart="18dp"
android:paddingTop="16dp"
android:paddingEnd="18dp"
android:paddingBottom="18dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/settings_title"
android:textColor="@color/av_text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<ImageButton
android:id="@+id/btnClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_alignParentEnd="true"
android:background="@android:color/transparent"
android:contentDescription="@string/push_settings_close"
android:src="@drawable/ic_av_close"
app:tint="@color/brand_primary" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/push_settings_stream_id"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etStreamId"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_resolution"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgResolution"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_segment_container"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbRes360p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_left"
android:button="@null"
android:gravity="center"
android:text="360p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRes480p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_middle"
android:button="@null"
android:gravity="center"
android:text="480p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRes540p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_middle"
android:button="@null"
android:gravity="center"
android:text="540p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRes720p"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_right"
android:button="@null"
android:gravity="center"
android:text="720p"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_fps"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etFps"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_max_bitrate"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etMaxBitrate"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/settings_min_bitrate"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etMinBitrate"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="number"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/settings_env_title"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/settings_vhost"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etEnvVhost"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="textUri"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/settings_vhost_key"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etEnvVhostKey"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="textPassword"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/settings_app_id"
android:textColor="@color/av_text_primary"
android:textSize="14sp"
android:textStyle="bold" />
<EditText
android:id="@+id/etEnvAppId"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<Button
android:id="@+id/btnApply"
android:layout_width="match_parent"
android:layout_height="@dimen/av_primary_button_height"
android:layout_marginTop="18dp"
android:background="@drawable/selector_av_primary_button"
android:text="@string/push_settings_apply"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/av_overlay_dim">
<ImageButton
android:id="@+id/btnClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="top|end"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_av_icon_circle"
android:contentDescription="@string/close"
android:src="@drawable/ic_av_close"
app:tint="@color/av_text_primary" />
<LinearLayout
android:id="@+id/card"
android:layout_width="320dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_av_dialog_card_gray"
android:orientation="vertical"
android:paddingStart="18dp"
android:paddingTop="16dp"
android:paddingEnd="18dp"
android:paddingBottom="18dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/play_config_title"
android:textColor="@color/av_text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/play_config_protocol_label"
android:textColor="@color/av_text_primary"
android:textSize="13sp"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rgProtocol"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_segment_container"
android:checkedButton="@+id/rbRtmp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rbRtmp"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_left"
android:button="@null"
android:gravity="center"
android:text="@string/protocol_rtmp"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbRtc"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/selector_av_segment_right"
android:button="@null"
android:gravity="center"
android:text="@string/protocol_rtc"
android:textColor="@color/av_segment_text"
android:textSize="14sp" />
</RadioGroup>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:text="@string/play_config_stream_hint"
android:textColor="@color/av_text_secondary"
android:textSize="12sp" />
<EditText
android:id="@+id/etStreamIdOrUrl"
android:layout_width="match_parent"
android:layout_height="@dimen/av_field_height"
android:layout_marginTop="8dp"
android:background="@drawable/bg_av_input_field"
android:importantForAutofill="no"
android:inputType="text"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="test"
android:textColor="@color/av_text_primary"
android:textColorHint="@color/av_text_hint"
android:textSize="14sp" />
<Button
android:id="@+id/btnStartPlay"
android:layout_width="match_parent"
android:layout_height="@dimen/av_primary_button_height"
android:layout_marginTop="18dp"
android:background="@drawable/selector_av_primary_button"
android:text="@string/play_start"
android:textColor="@color/brand_primary_text_on"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingTop="0dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_av_dialog_card"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:text="@string/push_protocol_sheet_title"
android:textColor="@color/av_text_primary"
android:textSize="13sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="12dp"
android:gravity="center"
android:text="@string/push_protocol_sheet_message"
android:textColor="@color/av_text_secondary"
android:textSize="12sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/av_divider" />
<TextView
android:id="@+id/btnRtc"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:text="@string/push_protocol_rtc"
android:textColor="@color/brand_primary"
android:textSize="18sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/av_divider" />
<TextView
android:id="@+id/btnRtmp"
android:layout_width="match_parent"
android:layout_height="56dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:text="@string/push_protocol_rtmp"
android:textColor="@color/brand_primary"
android:textSize="18sp" />
</LinearLayout>
<TextView
android:id="@+id/btnCancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="10dp"
android:background="@drawable/bg_av_dialog_card"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center"
android:text="@string/cancel"
android:textColor="@color/brand_primary"
android:textSize="18sp" />
</LinearLayout>

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/previewContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_live_preview_mask"
android:clipToOutline="true"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/ivPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/home_live_square"
android:scaleType="centerCrop" />
<ImageView
android:id="@+id/ivPlay"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:background="@drawable/bg_live_play_button"
android:contentDescription="@string/play_ctrl_play"
android:src="@drawable/ic_av_play"
app:tint="@color/brand_primary" />
<TextView
android:id="@+id/tvDuration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_margin="8dp"
android:background="@drawable/bg_live_duration_badge"
android:paddingStart="8dp"
android:paddingTop="2dp"
android:paddingEnd="8dp"
android:paddingBottom="2dp"
android:text="00:00"
android:textColor="@android:color/white"
android:textSize="12sp" />
</FrameLayout>
<LinearLayout
android:id="@+id/infoContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/previewContainer">
<TextView
android:id="@+id/tvStreamName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:text="stream001"
android:textColor="@color/av_text_primary"
android:textSize="13sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<View
android:layout_width="8dp"
android:layout_height="8dp"
android:background="@drawable/bg_live_dot" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="@string/live_status"
android:textColor="@color/av_stats_red"
android:textSize="12sp" />
<TextView
android:id="@+id/tvProtocol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/bg_live_protocol_tag"
android:paddingStart="8dp"
android:paddingTop="2dp"
android:paddingEnd="8dp"
android:paddingBottom="2dp"
android:text="RTMP"
android:textColor="@color/brand_primary_text_on"
android:textSize="11sp" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#222222"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:background="#444444"
android:text="流"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="#000000">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="200dp"/>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:padding="8dp">
<Button
android:id="@+id/btnPrepare"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="准备"
android:textColor="#FFFFFF"
android:backgroundTint="#03A9F4"/>
<Button
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="开始"
android:textColor="#FFFFFF"
android:backgroundTint="#2196F3"/>
<Button
android:id="@+id/btnStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="停止"
android:textColor="#FFFFFF"
android:backgroundTint="#F44336"/>
<Button
android:id="@+id/btnRemove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="移除"
android:textColor="#FFFFFF"
android:backgroundTint="#9E9E9E"/>
</LinearLayout>
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:textSize="12sp"
android:padding="8dp"
android:text="状态: 待添加"/>
</LinearLayout>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Some files were not shown because too many files have changed in this diff Show More