
文章目录
-
- [🎯 本阶段学习目标](#🎯 本阶段学习目标)
- [📅 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功能强大但相对复杂,建议多看官方文档和示例。
重点理解架构思想,这对后续学习很有帮助! 🛠️