Android频道播放系统架构设计文档

Android频道播放系统架构设计文档

1. 系统架构全景图

1.1 模块化分层架构

┌───────────────────────────────────────┐
│                app模块                │
│  • 应用入口点                         │
│  • 全局导航控制                       │
├───────────────────────────────────────┤
│             feature-player模块        │
│  • 播放界面(Compose)                  │
│  • 播放控制逻辑                       │
├───────────────────────────────────────┤
│           feature-menu模块            │
│  • 动态菜单系统                       │
│  • 用户偏好界面                       │
├───────────────────────────────────────┤
│             core-ui模块               │
│  • 共享Compose组件                    │
│  • 主题/样式系统                      │
├───────────────────────────────────────┤
│            core-data模块              │
│  • 数据仓库(Repository)               │
│  • 本地持久化(Room/DataStore)         │
├───────────────────────────────────────┤
│           core-player模块             │
│  • ExoPlayer扩展                     │
│  • WebView播放器封装                  │
└───────────────────────────────────────┘

1.2 技术栈矩阵

技术领域核心技术组件
UI框架Jetpack Compose, Material3, Leanback (TV)
异步处理Kotlin Coroutines, Flow, Channel
依赖注入Hilt
本地存储Room, DataStore, SharedPreferences
网络Retrofit, OkHttp, Glide
播放引擎ExoPlayer, Android WebView, Media3
调试工具LeakCanary, Chucker, Timber
测试JUnit5, MockK, Espresso, Compose Testing

2. UI层深度设计

2.1 播放界面组件树(Compose实现)

@Composable
fun PlayerScreen(
    viewModel: PlayerViewModel = hiltViewModel()
) {
    val playerState by viewModel.playerState.collectAsState()

    Box(modifier = Modifier.fillMaxSize()) {
        // 视频渲染层
        VideoSurface(
            modifier = Modifier.fillMaxSize(),
            player = viewModel.player
        )

        // 叠加控制层
        AnimatedVisibility(
            visible = playerState.controlsVisible,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            PlayerControls(
                state = playerState,
                onPlayPause = { viewModel.togglePlayPause() },
                onSeek = { viewModel.seekTo(it) }
            )
        }

        // 悬浮菜单按钮
        FloatingMenuButton(
            onClick = { viewModel.toggleMenu() }
        )
    }
}

2.2 多端UI适配方案

移动端菜单实现:

@Composable
fun MobileMenu(
    menuState: MenuState,
    onAction: (MenuAction) -> Unit
) {
    ModalBottomSheet(
        onDismissRequest = { onAction(MenuAction.Dismiss) }
    ) {
        LazyColumn {
            items(menuState.items) { item ->
                when (item) {
                    is MenuItem.Expandable -> ExpandableMenuItem(item, onAction)
                    is MenuItem.Toggle -> ToggleMenuItem(item, onAction)
                    is MenuItem.Selectable -> SelectableMenuItem(item, onAction)
                }
            }
        }
    }
}

TV端菜单实现:

@Composable
fun TvMenu(
    menuState: MenuState,
    modifier: Modifier = Modifier
) {
    val focusRequester = remember { FocusRequester() }

    Box(modifier = modifier.background(MaterialTheme.colorScheme.surface)) {
        LazyRow(
            modifier = Modifier.focusRequester(focusRequester),
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            items(menuState.items) { item ->
                TvMenuItem(
                    item = item,
                    modifier = Modifier
                        .focusable()
                        .onFocusChanged { focus ->
                            if (focus.isFocused) {
                                viewModel.onMenuItemFocused(item.id)
                            }
                        }
                )
            }
        }
    }

    LaunchedEffect(Unit) {
        focusRequester.requestFocus()
    }
}

3. 业务逻辑层精要设计

3.1 状态管理核心

class PlayerViewModel @Inject constructor(
    private val playerAdapter: PlayerAdapter,
    private val prefRepository: PreferenceRepository
) : ViewModel() {

    private val _playerState = MutableStateFlow(PlayerState())
    val playerState: StateFlow<PlayerState> = _playerState.asStateFlow()

    init {
        observePlayerEvents()
    }

    private fun observePlayerEvents() {
        playerAdapter.addListener(object : PlayerListener {
            override fun onPositionChanged(position: Long) {
                _playerState.update { it.copy(currentPosition = position) }
            }

            override fun onPlaybackStateChanged(playing: Boolean) {
                _playerState.update { it.copy(isPlaying = playing) }
            }
        })
    }

    fun togglePlayPause() {
        if (playerState.value.isPlaying) {
            playerAdapter.pause()
        } else {
            playerAdapter.play()
        }
    }
}

3.2 依赖注入配置

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun providePlayerAdapter(
        context: Context,
        @ApplicationScope scope: CoroutineScope
    ): PlayerAdapter {
        return ExoPlayerAdapter(context, scope)
    }

    @Provides
    @Singleton
    fun provideChannelRepository(
        localDataSource: ChannelLocalDataSource,
        remoteDataSource: ChannelRemoteDataSource
    ): ChannelRepository {
        return ChannelRepositoryImpl(localDataSource, remoteDataSource)
    }
}

4. 数据层完整实现

4.1 数据流架构

graph TD
    A[UI层] -->|观察| B(ViewModel)
    B -->|调用| C[Repository]
    C -->|本地数据| D[(Room数据库)]
    C -->|远程数据| E[API Service]
    D -->|缓存策略| F[DataStore]
    E -->|网络请求| G[OkHttp]

4.2 数据库实体与DAO

@Entity(tableName = "channels")
data class ChannelEntity(
    @PrimaryKey val id: String,
    val name: String,
    val url: String,
    val type: String,
    val lastPlayed: Long = 0,
    val isFavorite: Boolean = false
)

@Dao
interface ChannelDao {
    @Query("SELECT * FROM channels WHERE isFavorite = 1 ORDER BY lastPlayed DESC")
    fun observeFavorites(): Flow<List<ChannelEntity>>

    @Upsert
    suspend fun upsert(channel: ChannelEntity)

    @Query("UPDATE channels SET lastPlayed = :timestamp WHERE id = :channelId")
    suspend fun updateLastPlayed(channelId: String, timestamp: Long)
}

4.3 仓库模式实现

class ChannelRepositoryImpl @Inject constructor(
    private val local: ChannelLocalDataSource,
    private val remote: ChannelRemoteDataSource
) : ChannelRepository {

    override fun getFeaturedChannels(): Flow<List<Channel>> {
        return local.getCachedChannels().combine(
            remote.fetchFeaturedChannels()
        ) { cached, remote ->
            if (cached.isEmpty()) remote else cached
        }.onEach { channels ->
            local.cacheChannels(channels)
        }
    }
}

5. 播放引擎层终极设计

5.1 ExoPlayer高级配置

class ExoPlayerAdapter @Inject constructor(
    context: Context,
    @ApplicationScope private val scope: CoroutineScope
) : PlayerAdapter {

    private val player: ExoPlayer by lazy {
        ExoPlayer.Builder(context)
            .setTrackSelector(DefaultTrackSelector(context).apply {
                parameters = buildUponParameters()
                    .setMaxVideoSizeSd()
                    .setPreferredAudioLanguage("zh")
                    .build()
            })
            .setLoadControl(DefaultLoadControl.Builder()
                .setBufferDurationsMs(
                    MIN_BUFFER_MS,
                    MAX_BUFFER_MS,
                    PLAYBACK_BUFFER_MS,
                    REBUFFER_BUFFER_MS
                )
                .build())
            .build()
    }

    private val bandwidthMeter = DefaultBandwidthMeter.Builder(context).build()

    override fun loadMedia(url: String) {
        val mediaItem = MediaItem.fromUri(url).buildUpon()
            .setMimeType(MimeTypes.APPLICATION_M3U8)
            .build()

        player.setMediaItem(mediaItem)
        player.prepare()
    }
}

5.2 WebView播放器封装

class WebPlayerAdapter @Inject constructor(
    context: Context
) : PlayerAdapter {

    private val webView: WebView = WebView(context).apply {
        settings.apply {
            javaScriptEnabled = true
            mediaPlaybackRequiresUserGesture = false
            domStorageEnabled = true
        }

        webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {
                injectPlayerScripts()
            }
        }
    }

    private fun injectPlayerScripts() {
        val autoPlayScript = """
            document.querySelector('video')?.play()
            document.querySelector('video')?.requestFullscreen()
        """.trimIndent()

        webView.evaluateJavascript(autoPlayScript, null)
    }
}

6. 性能优化全方案

6.1 内存优化策略

场景优化措施
后台播放启用音频焦点管理,释放视频解码器
低内存设备降低默认分辨率(480P),减少同时缓存的项目数
WebView实例使用对象池限制最大实例数(3个),超出时销毁最久未使用的实例
图片加载使用Glide with downsampling,启用硬件位图
播放器切换预初始化备用播放器,但保持暂停状态

6.2 播放预加载机制

class PreloadManager @Inject constructor(
    private val playerPool: PlayerPool,
    private val repo: ChannelRepository
) {
    private val preloadScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    fun schedulePreload(currentChannelId: String) {
        preloadScope.launch {
            val relatedChannels = repo.getRelatedChannels(currentChannelId)
            playerPool.preparePlayers(relatedChannels.take(2).map { it.url })
        }
    }

    fun cancelAll() {
        preloadScope.coroutineContext.cancelChildren()
    }
}

7. 安全与隐私保障

7.1 安全配置矩阵

安全领域实施措施
网络通信强制HTTPS,证书锁定(Certificate Pinning)
数据存储敏感信息使用AndroidKeyStore加密
WebView安全禁用文件访问,设置安全CSP策略
DRM支持Widevine Level1(支持硬件级内容保护)
用户隐私遵循GDPR,提供数据清除选项

7.2 安全代码示例

// 网络通信安全配置
fun createOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .sslSocketFactory(createSSLSocketFactory(), trustManager)
        .hostnameVerifier { hostname, session ->
            hostname == "api.trusted.com"
        }
        .addInterceptor(ChuckerInterceptor(context))
        .build()
}

// WebView安全配置
private fun setupWebViewSecurity(webView: WebView) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        webView.settings.allowFileAccess = false
    }
    WebSettings.setWebContentsDebuggingEnabled(false)
    webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
}

8. 测试体系完整方案

8.1 单元测试套件

class PlayerViewModelTest {
    @Test
    fun `togglePlayPause should invert play state`() = runTest {
        // Given
        val mockPlayer = mockk<PlayerAdapter>()
        val viewModel = PlayerViewModel(mockPlayer, mockk())

        // When
        viewModel.togglePlayPause()

        // Then
        verify { mockPlayer.play() }
    }
}

8.2 UI测试用例

@HiltAndroidTest
class PlayerScreenTest {
    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Test
    fun shouldShowControlsWhenVideoPlaying() {
        composeTestRule.setContent {
            PlayerScreen(viewModel = FakePlayerViewModel())
        }

        composeTestRule.onNodeWithTag("video_surface")
            .performClick()

        composeTestRule.onNodeWithTag("play_button")
            .assertIsDisplayed()
    }
}

9. 持续交付流水线

9.1 多环境配置

flavorDimensions "environment"
productFlavors {
    dev {
        dimension "environment"
        applicationIdSuffix ".dev"
        resValue "string", "app_name", "Player Dev"
    }
    staging {
        dimension "environment"
        applicationIdSuffix ".staging"
        resValue "string", "app_name", "Player Staging"
    }
    prod {
        dimension "environment"
        resValue "string", "app_name", "Player"
    }
}

9.2 CI/CD流程

name: CI Pipeline

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: ./gradlew assembleDebug lintDebug
      - run: ./gradlew testDebugUnitTest

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: ./gradlew assembleProdRelease
      - uses: fastlane/github-action@v1
        with:
          lane: 'deploy_to_firebase'

10. 扩展性设计

10.1 插件系统架构

interface PlayerPlugin {
    fun onAttach(player: PlayerAdapter)
    fun onDetach()
    fun handleEvent(event: PlayerEvent): Boolean
}

class PlayerPluginManager @Inject constructor() {
    private val plugins = mutableListOf<PlayerPlugin>()

    fun registerPlugin(plugin: PlayerPlugin) {
        plugins.add(plugin)
    }

    fun dispatchEvent(event: PlayerEvent) {
        plugins.forEach { it.handleEvent(event) }
    }
}

10.2 动态功能模块

dynamicFeatures = [ 
    ':features:downloads',
    ':features:cast',
    ':features:comments'
]

本终极版设计文档提供了从宏观架构到代码级别的完整指导方案,具有以下核心优势:

  1. 模块化设计:物理模块与逻辑分层完美对应,支持团队并行开发
  2. 现代化技术栈:全面采用Jetpack组件和Kotlin协程等现代Android开发技术
  3. 极致性能优化:从内存管理到播放预加载的全方位性能考量
  4. 企业级安全:涵盖网络安全、数据存储安全和内容保护的多层防护
  5. 完备的质量保障:包含单元测试、UI测试和CI/CD的完整质量体系
  6. 未来可扩展性:插件系统和动态功能模块设计确保长期可维护性

建议开发团队根据实际业务需求进行适当裁剪,并在实施过程中持续优化各模块实现细节。

No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
Previous
Next