第三阶段:ExoPlayer进阶播放器

文章目录

    • [🎯 本阶段学习目标](#🎯 本阶段学习目标)
    • [📅 1. ExoPlayer架构理解](#📅 1. ExoPlayer架构理解)
      • [🎮 什么是ExoPlayer?](#🎮 什么是ExoPlayer?)
        • [🔍 ExoPlayer vs MediaPlayer对比](#🔍 ExoPlayer vs MediaPlayer对比)
        • [🏗️ ExoPlayer核心架构](#🏗️ ExoPlayer核心架构)
    • [📅 2. 基础播放功能](#📅 2. 基础播放功能)
      • [🎬 创建你的第一个ExoPlayer](#🎬 创建你的第一个ExoPlayer)
        • [📱 布局文件](#📱 布局文件)
    • [📅 3. 网络视频优化](#📅 3. 网络视频优化)
      • [🌐 网络视频播放挑战](#🌐 网络视频播放挑战)
        • [📡 数据源配置](#📡 数据源配置)
        • [🔄 自适应码率实现](#🔄 自适应码率实现)
        • [🎯 完整的网络播放器实现](#🎯 完整的网络播放器实现)
        • [📋 权限配置](#📋 权限配置)
    • [📅 4. 自定义UI](#📅 4. 自定义UI)
      • [🎨 自定义播放器界面](#🎨 自定义播放器界面)
    • [📅 5. 高级功能](#📅 5. 高级功能)
      • [📋 播放列表管理](#📋 播放列表管理)
    • [🎓 第三阶段总结](#🎓 第三阶段总结)
      • [✅ 你已经掌握的技能](#✅ 你已经掌握的技能)

从基础播放器升级到专业级播放器 🚀

掌握Google官方推荐的ExoPlayer,开发功能完整的视频播放器

🎯 本阶段学习目标

学完这个阶段,你将能够:

  • 🎮 理解ExoPlayer架构:掌握专业播放器的设计思想
  • 🌐 实现网络视频播放:处理各种网络情况和格式
  • 🎨 自定义播放器UI:打造个性化的播放界面
  • 优化播放性能:实现缓存、预加载等高级功能

📅 1. ExoPlayer架构理解

🎮 什么是ExoPlayer?

生活比喻 :如果MediaPlayer是简单的DVD播放机 ,那么ExoPlayer就是专业的多媒体工作站

text 复制代码
📀 MediaPlayer = 家用DVD机(简单易用,功能有限)
🎛️ ExoPlayer = 专业工作站(功能强大,高度可定制)
🔍 ExoPlayer vs MediaPlayer对比

📊 播放器功能对比表

对比维度 MediaPlayer ExoPlayer 优势方 详细说明
🎬 支持格式 有限 丰富 ⭐ ExoPlayer MediaPlayer只支持系统内置格式;ExoPlayer支持DASH、HLS、SmoothStreaming等
🎨 自定义能力 很少 极强 ⭐ ExoPlayer MediaPlayer几乎无法自定义;ExoPlayer可替换所有组件
🌐 网络适应 基础 智能 ⭐ ExoPlayer ExoPlayer具备自适应码率、智能缓存等高级网络功能
📱 使用复杂度 简单 相对复杂 ⭐ MediaPlayer MediaPlayer API简单易用;ExoPlayer需要更多配置
📦 包大小 0MB 约2MB ⭐ MediaPlayer MediaPlayer系统自带;ExoPlayer需要额外引入库
🔄 更新频率 跟随系统 频繁更新 ⭐ ExoPlayer ExoPlayer独立更新,功能迭代快;MediaPlayer依赖系统更新
🛠️ 调试能力 有限 强大 ⭐ ExoPlayer ExoPlayer提供详细的调试信息和性能监控
⚡ 性能表现 一般 优秀 ⭐ ExoPlayer ExoPlayer在内存管理和播放效率上更优

🎯 选择建议表

使用场景 推荐播放器 理由
📱 简单音频播放 MediaPlayer API简单,系统自带,满足基础需求
🎬 专业视频应用 ExoPlayer 功能强大,可定制性强,适合复杂需求
🌐 网络直播 ExoPlayer 支持多种流媒体协议,网络适应性强
🎮 游戏音效 MediaPlayer 轻量级,启动快,适合短音频
📺 视频播放器 ExoPlayer 支持多格式,可自定义UI,用户体验好
🎵 音乐播放器 ExoPlayer 支持多种音频格式,功能丰富

✅ 选择ExoPlayer的理由

  • 🎬 需要播放多种格式的视频
  • 🌐 需要播放网络直播流
  • 🎨 需要自定义播放器界面
  • ⚡ 需要高级功能(缓存、预加载等)
  • 📱 开发专业的视频应用
  • 🔧 需要精确控制播放行为
🏗️ ExoPlayer核心架构

🏭 ExoPlayer架构图解

text 复制代码
                    🎮 ExoPlayer (总指挥)
                           |
        ┌─────────────────────────────────────────┐
        |                                         |
        v                                         v
   📋 MediaSource                           🎛️ TrackSelector
   (原料仓库)                                (质检员)
        |                                         |
        |                                         |
        v                                         v
   🚚 DataSource  ──────────────────────→   🏭 Renderer
   (运输队)                                   (加工车间)
        |                                         |
        |                                         |
        v                                         v
   📊 LoadControl                           📺 输出设备
   (调度员)                                 (屏幕/扬声器)

🔧 核心组件详解表

组件 生活比喻 主要职责 具体功能 可定制性
🎮 Player 工厂总指挥 统一管理和协调 控制播放状态、协调各组件工作 ⭐⭐⭐
📋 MediaSource 原料仓库 提供媒体数据 支持本地文件、网络流、直播等 ⭐⭐⭐⭐⭐
🚚 DataSource 运输队 获取数据 HTTP请求、文件读取、缓存管理 ⭐⭐⭐⭐
🏭 Renderer 加工车间 处理音视频 解码、渲染音视频数据 ⭐⭐⭐⭐⭐
🎛️ TrackSelector 质检员 选择轨道 根据设备能力选择最佳音视频轨道 ⭐⭐⭐⭐
📊 LoadControl 调度员 控制加载 决定缓冲策略、内存使用 ⭐⭐⭐

🔄 ExoPlayer工作流程

text 复制代码
步骤1: 📋 MediaSource告诉Player要播放什么
       ↓
步骤2: 🚚 DataSource负责获取视频数据  
       ↓
步骤3: 🎛️ TrackSelector选择最合适的音视频轨道
       ↓  
步骤4: 📊 LoadControl决定何时加载更多数据
       ↓
步骤5: 🏭 Renderer处理音视频数据并输出
       ↓
步骤6: 🎮 Player协调所有组件工作

💡 架构优势

  • 🔧 模块化设计:每个组件职责单一,可独立替换
  • 🎨 高度可定制:可以替换任何组件实现自定义功能
  • ⚡ 性能优化:组件间协作高效,资源利用最优
  • 🛠️ 易于扩展:新功能可通过扩展组件实现

📅 2. 基础播放功能

🎬 创建你的第一个ExoPlayer

kotlin 复制代码
/**
 * 🎬 ExoPlayer基础播放器
 */
class BasicExoPlayerActivity : AppCompatActivity() {
  
    private var player: ExoPlayer? = null
    private lateinit var playerView: PlayerView
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_exoplayer)
      
        playerView = findViewById(R.id.player_view)
      
        initializePlayer()
    }
  
    /**
     * 🚀 初始化ExoPlayer
     * 
     * 这是创建ExoPlayer的标准流程:
     * 1. 使用Builder模式创建实例
     * 2. 绑定到UI组件
     * 3. 设置媒体源
     * 4. 配置播放参数
     */
    private fun initializePlayer() {
        // 第一步:使用Builder模式创建ExoPlayer实例
        // Builder模式允许我们灵活配置各种组件
        player = ExoPlayer.Builder(this)
            .build()  // 使用默认配置构建播放器
            .also { exoPlayer ->
                // 第二步:将ExoPlayer绑定到PlayerView
                // PlayerView是ExoPlayer提供的UI组件,包含播放控制器
                playerView.player = exoPlayer
              
                // 第三步:创建媒体项(MediaItem)
                // MediaItem封装了媒体源的信息,如URI、元数据等
                val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4))
                exoPlayer.setMediaItem(mediaItem)
              
                // 第四步:配置播放参数
                exoPlayer.playWhenReady = true  // 设置为true表示准备完成后立即播放
                exoPlayer.seekTo(0, 0L)         // 跳转到指定位置(0表示从头开始)
                exoPlayer.prepare()             // 异步准备媒体数据,准备完成后会开始播放
            }
    }
  
    /**
     * 🏛️ 设置播放控制按钮的事件监听
     * 
     * 这里展示了如何手动控制ExoPlayer的播放状态
     * 包括播放/暂停、快进/快退等基本操作
     */
    private fun setupPlaybackControls() {
        // 播放/暂停按钮控制
        findViewById<Button>(R.id.btn_play_pause).setOnClickListener {
            player?.let { exoPlayer ->
                // 检查当前播放状态并切换
                if (exoPlayer.isPlaying) {
                    exoPlayer.pause()  // 暂停播放,保持当前位置
                } else {
                    exoPlayer.play()   // 恢复或开始播放
                }
            }
        }
      
        // 快进10秒按钮
        findViewById<Button>(R.id.btn_forward).setOnClickListener {
            player?.let { exoPlayer ->
                val currentPosition = exoPlayer.currentPosition  // 获取当前播放位置(毫秒)
                val newPosition = currentPosition + 10000       // 加上10秒(10000毫秒)
                exoPlayer.seekTo(newPosition)                   // 跳转到新位置
            }
        }
      
        // 快退10秒按钮
        findViewById<Button>(R.id.btn_rewind).setOnClickListener {
            player?.let { exoPlayer ->
                val currentPosition = exoPlayer.currentPosition     // 获取当前播放位置
                val newPosition = maxOf(0, currentPosition - 10000) // 减去10秒,但不小于0
                exoPlayer.seekTo(newPosition)                       // 跳转到新位置
            }
        }
    }
  
    /**
     * 🔄 Activity生命周期管理
     * 
     * ExoPlayer的生命周期管理需要根据Android版本进行区分:
     * - API 24+:在onStart/onStop中管理
     * - API 23-:在onResume/onPause中管理
     */
    override fun onStart() {
        super.onStart()
        // Android 7.0 (API 24) 及以上版本在onStart中初始化播放器
        // 这样可以支持多窗口模式
        if (Util.SDK_INT > 23) {
            initializePlayer()
        }
    }
  
    override fun onResume() {
        super.onResume()
        // Android 6.0 (API 23) 及以下版本在onResume中初始化播放器
        // 或者当播放器为空时也需要初始化
        if (Util.SDK_INT <= 23 || player == null) {
            initializePlayer()
        }
    }
  
    override fun onPause() {
        super.onPause()
        // Android 6.0 (API 23) 及以下版本在onPause中释放播放器
        // 这样可以避免在后台继续播放
        if (Util.SDK_INT <= 23) {
            releasePlayer()
        }
    }
  
    override fun onStop() {
        super.onStop()
        // Android 7.0 (API 24) 及以上版本在onStop中释放播放器
        // 这样可以在多窗口模式下正确处理
        if (Util.SDK_INT > 23) {
            releasePlayer()
        }
    }
  
    /**
     * 🧹 释放播放器资源
     * 
     * 这是非常重要的步骤,必须在适当的时机调用:
     * 1. 释放底层资源(解码器、缓冲区等)
     * 2. 停止后台线程
     * 3. 避免内存泄漏
     */
    private fun releasePlayer() {
        player?.release()  // 释放所有底层资源,包括解码器、网络连接等
        player = null      // 清空引用,帮助GC回收内存
    }
}
📱 布局文件
xml 复制代码
<!-- activity_exoplayer.xml -->
<?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="match_parent"
    android:orientation="vertical">
  
    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
      
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp">
      
        <Button
            android:id="@+id/btn_rewind"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="⏪ -10s" />
          
        <Button
            android:id="@+id/btn_play_pause"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="⏯️ 播放/暂停" />
          
        <Button
            android:id="@+id/btn_forward"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="⏩ +10s" />
          
    </LinearLayout>
  
</LinearLayout>

📅 3. 网络视频优化

🌐 网络视频播放挑战

生活比喻 :网络视频播放就像是在线看电影

text 复制代码
🏠 本地视频 = 家里的DVD(稳定,不卡顿)
🌐 网络视频 = 在线电影(可能卡顿,需要缓冲)
📡 数据源配置
kotlin 复制代码
/**
 * 📡 网络数据源管理器
 * 
 * 负责管理ExoPlayer的网络数据获取和缓存策略
 * 包括:HTTP请求配置、缓存管理、超时处理等
 */
class NetworkDataSourceManager(private val context: Context) {
  
    /**
     * 🚚 创建数据源工厂
     * 
     * 数据源工厂是ExoPlayer获取媒体数据的核心组件
     * 它决定了如何从网络、本地文件或缓存中获取数据
     * 
     * @return DataSource.Factory 配置好的数据源工厂
     */
    fun createDataSourceFactory(): DataSource.Factory {
        // 第一步:配置HTTP数据源
        // HTTP数据源负责从网络获取视频数据
        val httpDataSourceFactory = DefaultHttpDataSource.Factory()
            // 设置User-Agent,用于标识应用,服务器可以根据此进行统计或限制
            .setUserAgent(Util.getUserAgent(context, "MyVideoApp"))
            // 设置连接超时时间,默认8秒,可根据网络情况调整
            .setConnectTimeoutMs(DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS)
            // 设置读取超时时间,默认8秒,防止网络卡顿时无限等待
            .setReadTimeoutMs(DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS)
            // 允许跨协议重定向(如从HTTP重定向到HTTPS)
            .setAllowCrossProtocolRedirects(true)
      
        // 第二步:创建带缓存功能的数据源
        // CacheDataSource可以将网络数据缓存到本地,提高播放效率
        return CacheDataSource.Factory()
            .setCache(getDownloadCache())                           // 设置缓存实例
            .setUpstreamDataSourceFactory(httpDataSourceFactory)    // 设置上游数据源(HTTP)
            .setCacheWriteDataSinkFactory(null)                    // 禁用写入缓存(只读缓存)
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)   // 缓存出错时忽略缓存,直接从网络获取
    }
  
    /**
     * 💾 创建下载缓存
     * 
     * 缓存可以显著提高用户体验:
     * 1. 减少网络流量消耗
     * 2. 提高播放启动速度
     * 3. 在网络不稳定时提供更好的播放体验
     * 
     * @return Cache 配置好的缓存实例
     */
    private fun getDownloadCache(): Cache {
        // 创建缓存目录,位于应用的外部存储目录
        // 这样即使应用被卸载,缓存文件也会被清理
        val downloadDirectory = File(context.getExternalFilesDir(null), "downloads")
      
        // 创建SimpleCache实例
        val downloadCache = SimpleCache(
            downloadDirectory,          // 缓存目录
            NoOpCacheEvictor(),        // 缓存清理策略(NoOp表示不自动清理)
            ExoDatabaseProvider(context) // 数据库提供者,用于存储缓存元数据
        )
        return downloadCache
    }
  
    /**
     * 🎬 根据URL类型创建MediaSource
     * 
     * ExoPlayer支持多种流媒体格式,需要根据不同的URL类型
     * 创建相应的MediaSource来处理不同的流媒体协议
     * 
     * @param uri 媒体文件的URI
     * @return MediaSource 相应的媒体源实例
     * @throws IllegalStateException 当不支持的文件类型时抛出
     */
    fun createMediaSource(uri: Uri): MediaSource {
        // 获取配置好的数据源工厂
        val dataSourceFactory = createDataSourceFactory()
      
        // 根据URI推断内容类型,创建相应的MediaSource
        return when (val type = Util.inferContentType(uri)) {
            // DASH(Dynamic Adaptive Streaming over HTTP)------ 动态自适应流
            // 适用于高质量视频流,支持多码率自适应
            C.CONTENT_TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri(uri))
              
            // SmoothStreaming------ 微软的流媒体协议
            // 主要用于微软的流媒体服务
            C.CONTENT_TYPE_SS -> SsMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri(uri))
              
            // HLS(HTTP Live Streaming)------ 苹果的流媒体协议
            // 广泛用于直播和点播服务,支持自适应码率
            C.CONTENT_TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri(uri))
              
            // 普通的渐进式下载(如MP4、AVI等)
            // 适用于传统的视频文件格式
            C.CONTENT_TYPE_OTHER -> ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri(uri))
              
            // 不支持的类型,抛出异常
            else -> {
                throw IllegalStateException("不支持的媒体类型: $type")
            }
        }
    }
}
🔄 自适应码率实现
kotlin 复制代码
/**
 * 🔄 自适应码率管理器
 * 
 * 负责根据网络状况和设备性能自动调整视频质量
 * 实现智能的码率适应,提供流畅的播放体验
 */
class AdaptiveBitrateManager {
  
    /**
     * 🎯 创建自适应轨道选择器
     * 
     * TrackSelector是ExoPlayer的核心组件,负责从多个可用轨道中
     * 选择最适合当前网络和设备条件的音视频轨道
     * 
     * @param context Android上下文
     * @return DefaultTrackSelector 配置好的轨道选择器
     */
    fun createAdaptiveTrackSelector(context: Context): DefaultTrackSelector {
        return DefaultTrackSelector(context).apply {
            // 设置初始的视频轨道选择参数
            setParameters(
                buildUponParameters()
                    .setMaxVideoSizeSd()                              // 默认最大SD质量(720p),防止初始加载过慢
                    .setPreferredVideoMimeType(MimeTypes.VIDEO_H264)   // 优先选择H.264编码,兼容性更好
                    .build()
            )
        }
    }
  
    /**
     * 📊 根据网络状况调整视频质量
     * 
     * 根据当前网络类型动态调整视频质量参数,
     * 平衡视频质量和网络流量消耗,提供最佳的观看体验
     * 
     * @param trackSelector 轨道选择器实例
     * @param networkType 当前网络类型
     */
    fun adjustQualityByNetwork(
        trackSelector: DefaultTrackSelector,
        networkType: NetworkType
    ) {
        // 获取参数构建器,用于修改轨道选择参数
        val parametersBuilder = trackSelector.buildUponParameters()
      
        // 根据不同网络类型设置相应的质量参数
        when (networkType) {
            NetworkType.WIFI -> {
                // WiFi环境:网络稳定且通常不计流量,允许高清播放
                parametersBuilder
                    .setMaxVideoSize(1920, 1080)    // 最大支持Full HD (1080p)
                    .setMaxVideoBitrate(5000000)     // 最大码率 5Mbps,保证高清画质
            }
          
            NetworkType.MOBILE_4G -> {
                // 4G环境:网速较快但需考虑流量消耗,选择中等质量
                parametersBuilder
                    .setMaxVideoSize(1280, 720)     // 最大支持HD (720p)
                    .setMaxVideoBitrate(2000000)     // 最大码率 2Mbps,平衡质量和流量
            }
          
            NetworkType.MOBILE_3G -> {
                // 3G环境:网速有限,选择低质量保证流畅播放
                parametersBuilder
                    .setMaxVideoSize(854, 480)      // 最大支持SD (480p)
                    .setMaxVideoBitrate(800000)      // 最大码率 800kbps,防止卡顿
            }
          
            NetworkType.MOBILE_2G -> {
                // 2G环境:网速极慢,禁用视频只播放音频
                parametersBuilder
                    .setMaxVideoSize(0, 0)          // 禁用视频轨道
                    .setMaxVideoBitrate(0)           // 视频码率设为0,只播放音频
            }
        }
      
        // 应用新的参数配置
        trackSelector.setParameters(parametersBuilder.build())
    }
  
    /**
     * 📶 网络类型枚举
     * 
     * 定义了应用可能遇到的各种网络环境
     * 用于区分不同网络条件下的播放策略
     */
    enum class NetworkType {
        WIFI,        // WiFi网络:通常稳定且不计流量
        MOBILE_4G,   // 4G移动网络:速度快但消耗流量
        MOBILE_3G,   // 3G移动网络:速度中等,需节省流量
        MOBILE_2G,   // 2G移动网络:速度慢,仅适合音频
        UNKNOWN      // 未知网络类型:使用保守策略
    }
}
🎯 完整的网络播放器实现
kotlin 复制代码
/**
 * 🌐 网络视频播放器
 * 
 * 整合了数据源管理、自适应码率等网络优化功能
 * 展示如何将前面的配置实际应用到ExoPlayer中
 */
class NetworkVideoPlayerActivity : AppCompatActivity() {
  
    private var player: ExoPlayer? = null
    private lateinit var playerView: PlayerView
    private lateinit var networkDataSourceManager: NetworkDataSourceManager
    private lateinit var adaptiveBitrateManager: AdaptiveBitrateManager
    private lateinit var trackSelector: DefaultTrackSelector
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_network_player)
  
        playerView = findViewById(R.id.player_view)
  
        // 初始化网络管理组件
        initNetworkComponents()
  
        // 创建并配置播放器
        initializeNetworkPlayer()
  
        // 设置网络监听
        setupNetworkListener()
    }
  
    /**
     * 🔧 初始化网络相关组件
     * 
     * 创建数据源管理器和自适应码率管理器
     * 这些组件将用于优化网络视频播放体验
     */
    private fun initNetworkComponents() {
        // 创建网络数据源管理器
        networkDataSourceManager = NetworkDataSourceManager(this)
  
        // 创建自适应码率管理器
        adaptiveBitrateManager = AdaptiveBitrateManager()
  
        // 创建轨道选择器(支持自适应码率)
        trackSelector = adaptiveBitrateManager.createAdaptiveTrackSelector(this)
    }
  
    /**
     * 🚀 初始化网络播放器
     * 
     * 使用配置好的组件创建ExoPlayer实例
     * 并设置网络视频源进行播放
     */
    private fun initializeNetworkPlayer() {
        // 第一步:使用自定义的轨道选择器创建ExoPlayer
        player = ExoPlayer.Builder(this)
            .setTrackSelector(trackSelector)  // ⭐️ 应用自适应码率选择器
            .build()
            .also { exoPlayer ->
                // 绑定到PlayerView
                playerView.player = exoPlayer
  
                // 第二步:创建网络媒体源
                val videoUri = Uri.parse("https://example.com/video.m3u8") // HLS视频示例
                val mediaSource = networkDataSourceManager.createMediaSource(videoUri) // ⭐️ 
  
                // 第三步:设置媒体源并开始播放
                exoPlayer.setMediaSource(mediaSource)
                exoPlayer.playWhenReady = true
                exoPlayer.prepare()
  
                // 第四步:添加播放器监听器
                exoPlayer.addListener(playerEventListener)
            }
  
        // 根据当前网络状况调整初始质量
        val currentNetworkType = getCurrentNetworkType()
        adaptiveBitrateManager.adjustQualityByNetwork(trackSelector, currentNetworkType)
    }
  
    /**
     * 📶 设置网络状态监听
     * 
     * 监听网络变化,动态调整视频质量
     * 当网络从WiFi切换到移动网络时自动降低质量
     */
    private fun setupNetworkListener() {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            val networkCallback = object : ConnectivityManager.NetworkCallback() {
                override fun onAvailable(network: Network) {
                    // 网络可用时调整质量
                    runOnUiThread {
                        val networkType = getCurrentNetworkType()
                        adaptiveBitrateManager.adjustQualityByNetwork(trackSelector, networkType)
                        showNetworkStatus("网络已连接: ${getNetworkTypeName(networkType)}")
                    }
                }
  
                override fun onLost(network: Network) {
                    // 网络断开时暂停播放
                    runOnUiThread {
                        player?.pause()
                        showNetworkStatus("网络连接已断开")
                    }
                }
  
                override fun onCapabilitiesChanged(
                    network: Network,
                    networkCapabilities: NetworkCapabilities
                ) {
                    // 网络能力变化时重新评估质量设置
                    runOnUiThread {
                        val networkType = getCurrentNetworkType()
                        adaptiveBitrateManager.adjustQualityByNetwork(trackSelector, networkType)
                        showNetworkStatus("网络类型变化: ${getNetworkTypeName(networkType)}")
                    }
                }
            }
  
            connectivityManager.registerDefaultNetworkCallback(networkCallback)
        }
    }
  
    /**
     * 📊 获取当前网络类型
     * 
     * 检测当前设备的网络连接类型
     * 用于决定合适的视频质量设置
     */
    private fun getCurrentNetworkType(): AdaptiveBitrateManager.NetworkType {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val network = connectivityManager.activeNetwork
            val capabilities = connectivityManager.getNetworkCapabilities(network)
  
            return when {
                capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> {
                    AdaptiveBitrateManager.NetworkType.WIFI
                }
                capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> {
                    // 进一步检测移动网络类型
                    val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
                    when (telephonyManager.networkType) {
                        TelephonyManager.NETWORK_TYPE_LTE,
                        TelephonyManager.NETWORK_TYPE_NR -> AdaptiveBitrateManager.NetworkType.MOBILE_4G
                        TelephonyManager.NETWORK_TYPE_UMTS,
                        TelephonyManager.NETWORK_TYPE_HSDPA -> AdaptiveBitrateManager.NetworkType.MOBILE_3G
                        TelephonyManager.NETWORK_TYPE_GPRS,
                        TelephonyManager.NETWORK_TYPE_EDGE -> AdaptiveBitrateManager.NetworkType.MOBILE_2G
                        else -> AdaptiveBitrateManager.NetworkType.MOBILE_4G // 默认4G
                    }
                }
                else -> AdaptiveBitrateManager.NetworkType.UNKNOWN
            }
        }
  
        return AdaptiveBitrateManager.NetworkType.UNKNOWN
    }
  
    /**
     * 🎧 播放器事件监听器
     * 
     * 监听播放器状态变化,处理各种播放事件
     * 包括缓冲状态、播放错误、质量变化等
     */
    private val playerEventListener = object : Player.Listener {
        override fun onPlaybackStateChanged(playbackState: Int) {
            when (playbackState) {
                Player.STATE_BUFFERING -> {
                    showNetworkStatus("正在缓冲...")
                }
                Player.STATE_READY -> {
                    showNetworkStatus("准备就绪")
                }
                Player.STATE_ENDED -> {
                    showNetworkStatus("播放完成")
                }
            }
        }
  
        override fun onPlayerError(error: PlaybackException) {
            showNetworkStatus("播放错误: ${error.message}")
  
            // 根据错误类型进行处理
            when (error.errorCode) {
                PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> {
                    // 网络连接失败,尝试降低质量重试
                    adaptiveBitrateManager.adjustQualityByNetwork(
                        trackSelector, 
                        AdaptiveBitrateManager.NetworkType.MOBILE_2G
                    )
                }
                PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> {
                    // 网络超时,暂停播放等待网络恢复
                    player?.pause()
                }
            }
        }
  
        override fun onTracksChanged(tracks: Tracks) {
            // 轨道变化时显示当前质量信息
            val videoTrack = tracks.groups.find { it.type == C.TRACK_TYPE_VIDEO }
            videoTrack?.let { group ->
                if (group.isSelected) {
                    val format = group.getTrackFormat(0)
                    showNetworkStatus("当前质量: ${format.width}x${format.height} @${format.bitrate/1000}kbps")
                }
            }
        }
    }
  
    /**
     * 📱 显示网络状态信息
     */
    private fun showNetworkStatus(message: String) {
        // 可以用Toast、Snackbar或自定义View显示
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
  
    /**
     * 🏷️ 获取网络类型名称
     */
    private fun getNetworkTypeName(networkType: AdaptiveBitrateManager.NetworkType): String {
        return when (networkType) {
            AdaptiveBitrateManager.NetworkType.WIFI -> "WiFi"
            AdaptiveBitrateManager.NetworkType.MOBILE_4G -> "4G"
            AdaptiveBitrateManager.NetworkType.MOBILE_3G -> "3G"
            AdaptiveBitrateManager.NetworkType.MOBILE_2G -> "2G"
            AdaptiveBitrateManager.NetworkType.UNKNOWN -> "未知"
        }
    }
  
    /**
     * 🔄 Activity生命周期管理
     */
    override fun onStart() {
        super.onStart()
        if (Util.SDK_INT > 23) {
            initializeNetworkPlayer()
        }
    }
  
    override fun onResume() {
        super.onResume()
        if (Util.SDK_INT <= 23 || player == null) {
            initializeNetworkPlayer()
        }
    }
  
    override fun onPause() {
        super.onPause()
        if (Util.SDK_INT <= 23) {
            releasePlayer()
        }
    }
  
    override fun onStop() {
        super.onStop()
        if (Util.SDK_INT > 23) {
            releasePlayer()
        }
    }
  
    /**
     * 🧹 释放播放器资源
     */
    private fun releasePlayer() {
        player?.release()
        player = null
    }
}
📋 权限配置

AndroidManifest.xml 中添加必要的权限:

xml 复制代码
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- 电话权限(用于检测移动网络类型) -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

<!-- 存储权限(用于缓存) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

📅 4. 自定义UI

🎨 自定义播放器界面

kotlin 复制代码
/**
 * 🎨 自定义播放器控制器
 */
class CustomPlayerControlView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
  
    private var player: Player? = null
    private var isVisible = true
    private var hideAction: Runnable? = null
  
    // UI组件
    private lateinit var playPauseButton: ImageButton
    private lateinit var progressBar: SeekBar
    private lateinit var currentTimeText: TextView
    private lateinit var totalTimeText: TextView
    private lateinit var fullscreenButton: ImageButton
  
    init {
        LayoutInflater.from(context).inflate(R.layout.custom_player_control, this, true)
        initViews()
        setupClickListeners()
    }
  
    private fun initViews() {
        playPauseButton = findViewById(R.id.btn_play_pause)
        progressBar = findViewById(R.id.progress_bar)
        currentTimeText = findViewById(R.id.tv_current_time)
        totalTimeText = findViewById(R.id.tv_total_time)
        fullscreenButton = findViewById(R.id.btn_fullscreen)
    }
  
    private fun setupClickListeners() {
        // 播放/暂停按钮
        playPauseButton.setOnClickListener {
            player?.let { p ->
                if (p.isPlaying) {
                    p.pause()
                } else {
                    p.play()
                }
            }
        }
      
        // 进度条拖拽
        progressBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                if (fromUser) {
                    player?.let { p ->
                        val duration = p.duration
                        if (duration != C.TIME_UNSET) {
                            val position = (progress / 100f * duration).toLong()
                            p.seekTo(position)
                        }
                    }
                }
            }
          
            override fun onStartTrackingTouch(seekBar: SeekBar?) {
                removeHideAction()
            }
          
            override fun onStopTrackingTouch(seekBar: SeekBar?) {
                scheduleHide()
            }
        })
      
        // 全屏按钮
        fullscreenButton.setOnClickListener {
            toggleFullscreen()
        }
      
        // 点击控制器区域显示/隐藏
        setOnClickListener {
            if (isVisible) {
                hide()
            } else {
                show()
            }
        }
    }
  
    /**
     * 🎮 绑定播放器
     */
    fun setPlayer(player: Player?) {
        this.player?.removeListener(playerListener)
        this.player = player
        this.player?.addListener(playerListener)
        updateAll()
    }
  
    /**
     * 👁️ 显示控制器
     */
    fun show() {
        if (!isVisible) {
            isVisible = true
            visibility = VISIBLE
            updateAll()
            scheduleHide()
        }
    }
  
    /**
     * 👻 隐藏控制器
     */
    fun hide() {
        if (isVisible) {
            isVisible = false
            visibility = GONE
            removeHideAction()
        }
    }
  
    /**
     * ⏰ 定时隐藏
     */
    private fun scheduleHide() {
        removeHideAction()
        hideAction = Runnable { hide() }
        postDelayed(hideAction, 3000) // 3秒后隐藏
    }
  
    private fun removeHideAction() {
        hideAction?.let { removeCallbacks(it) }
        hideAction = null
    }
  
    /**
     * 🔄 更新所有UI
     */
    private fun updateAll() {
        updatePlayPauseButton()
        updateProgress()
        updateTimeTexts()
    }
  
    private fun updatePlayPauseButton() {
        player?.let { p ->
            playPauseButton.setImageResource(
                if (p.isPlaying) R.drawable.ic_pause else R.drawable.ic_play
            )
        }
    }
  
    private fun updateProgress() {
        player?.let { p ->
            val duration = p.duration
            val position = p.currentPosition
          
            if (duration != C.TIME_UNSET && duration > 0) {
                val progress = (100 * position / duration).toInt()
                progressBar.progress = progress
            }
        }
    }
  
    private fun updateTimeTexts() {
        player?.let { p ->
            currentTimeText.text = formatTime(p.currentPosition)
            totalTimeText.text = formatTime(p.duration)
        }
    }
  
    private fun formatTime(timeMs: Long): String {
        if (timeMs == C.TIME_UNSET) return "00:00"
      
        val totalSeconds = timeMs / 1000
        val minutes = totalSeconds / 60
        val seconds = totalSeconds % 60
        return String.format("%02d:%02d", minutes, seconds)
    }
  
    private fun toggleFullscreen() {
        // 全屏逻辑实现
        (context as? Activity)?.let { activity ->
            if (activity.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
                activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
            } else {
                activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
            }
        }
    }
  
    /**
     * 🎧 播放器状态监听
     */
    private val playerListener = object : Player.Listener {
        override fun onPlaybackStateChanged(playbackState: Int) {
            updateAll()
        }
      
        override fun onIsPlayingChanged(isPlaying: Boolean) {
            updatePlayPauseButton()
            if (isPlaying) {
                scheduleHide()
            } else {
                removeHideAction()
            }
        }
    }
}

📅 5. 高级功能

📋 播放列表管理

kotlin 复制代码
/**
 * 📋 播放列表管理器
 */
class PlaylistManager(private val player: ExoPlayer) {
  
    private val playlist = mutableListOf<MediaItem>()
    private var currentIndex = 0
  
    /**
     * ➕ 添加视频到播放列表
     */
    fun addToPlaylist(uri: Uri, title: String? = null) {
        val mediaItem = MediaItem.Builder()
            .setUri(uri)
            .setMediaMetadata(
                MediaMetadata.Builder()
                    .setTitle(title ?: "未知标题")
                    .build()
            )
            .build()
      
        playlist.add(mediaItem)
        updatePlayerPlaylist()
    }
  
    /**
     * 🗑️ 从播放列表移除
     */
    fun removeFromPlaylist(index: Int) {
        if (index in 0 until playlist.size) {
            playlist.removeAt(index)
            if (currentIndex >= playlist.size) {
                currentIndex = maxOf(0, playlist.size - 1)
            }
            updatePlayerPlaylist()
        }
    }
  
    /**
     * ⏭️ 播放下一个
     */
    fun playNext() {
        if (currentIndex < playlist.size - 1) {
            currentIndex++
            player.seekTo(currentIndex, 0)
        }
    }
  
    /**
     * ⏮️ 播放上一个
     */
    fun playPrevious() {
        if (currentIndex > 0) {
            currentIndex--
            player.seekTo(currentIndex, 0)
        }
    }
  
    /**
     * 🔀 随机播放
     */
    fun shufflePlaylist() {
        val currentItem = playlist[currentIndex]
        playlist.shuffle()
        currentIndex = playlist.indexOf(currentItem)
        updatePlayerPlaylist()
    }
  
    /**
     * 🔄 更新播放器播放列表
     */
    private fun updatePlayerPlaylist() {
        player.setMediaItems(playlist)
        player.seekTo(currentIndex, 0)
        player.prepare()
    }
  
    /**
     * 📊 获取播放列表信息
     */
    fun getPlaylistInfo(): PlaylistInfo {
        return PlaylistInfo(
            totalCount = playlist.size,
            currentIndex = currentIndex,
            currentTitle = playlist.getOrNull(currentIndex)?.mediaMetadata?.title?.toString() ?: "未知"
        )
    }
  
    data class PlaylistInfo(
        val totalCount: Int,
        val currentIndex: Int,
        val currentTitle: String
    )
}

🎓 第三阶段总结

✅ 你已经掌握的技能

🎮 ExoPlayer架构理解

  • 理解了ExoPlayer的组件化设计
  • 掌握了各个组件的作用和配合
  • 能够选择合适的播放器方案

🌐 网络视频播放

  • 实现了多种格式的网络视频播放
  • 掌握了数据源配置和缓存策略
  • 理解了自适应码率的原理和实现

🎨 自定义UI开发

  • 开发了自定义的播放器控制界面
  • 实现了手势控制和自动隐藏功能
  • 掌握了全屏播放的处理方式

📋 高级功能实现

  • 实现了播放列表管理功能
  • 掌握了播放器状态监听
  • 了解了字幕和多音轨的处理

💡 学习建议

ExoPlayer功能强大但相对复杂,建议多看官方文档和示例。

重点理解架构思想,这对后续学习很有帮助! 🛠️

相关推荐
掌心天涯2 小时前
Android Gradle工程引入三方so库的方法
android·jni
J***Q2922 小时前
Kotlin DSL开发技巧
android·开发语言·kotlin
勇气要爆发2 小时前
第二阶段:Android音视频基础
android·音视频
度熊君3 小时前
深入理解 Kotlin 协程结构化并发
android·程序员
吴Wu涛涛涛涛涛Tao3 小时前
用 Flutter + BLoC 写一个顺手的涂鸦画板(支持撤销 / 重做 / 橡皮擦 / 保存相册)
android·flutter·ios
bqliang3 小时前
从喝水到学会 Android ASM 插桩
android·kotlin·android studio
HAPPY酷4 小时前
Flutter 开发环境搭建全流程
android·python·flutter·adb·pip
圆肖4 小时前
File Inclusion
android·ide·android studio
青旬4 小时前
我的AI搭档:从“年久失修”的AGP 3.4到平稳着陆AGP 8.0时代
android·ai编程