告别卡顿手把手教你为Android App集成ExoPlayer播放器含DASH/HLS直播支持在移动应用开发中视频播放功能已经成为许多App的核心体验之一。无论是社交平台的短视频、教育类App的课程视频还是新闻媒体的直播内容流畅的视频播放体验直接影响用户留存率。作为Android开发者我们常常面临一个关键选择是使用系统自带的MediaPlayer还是集成更强大的第三方播放器Google推出的ExoPlayer正逐渐成为Android视频播放领域的事实标准。它不仅解决了原生MediaPlayer的诸多限制还提供了对现代流媒体协议如DASH和HLS的完善支持。根据GitHub统计ExoPlayer已经获得超过20k星标被广泛应用于YouTube、Google Photos等知名产品中。本文将带你从零开始完整实现ExoPlayer在Android项目中的集成过程。不同于简单的API调用教程我们会深入探讨如何配置硬件加速解码、优化内存使用以及处理各种网络条件下的播放稳定性问题。无论你是需要播放本地视频文件还是处理复杂的直播流这套方案都能显著提升你的应用视频播放体验。1. 为什么选择ExoPlayer在开始编码之前理解技术选型的依据至关重要。Android生态中存在多种播放器解决方案每个都有其适用场景。核心优势对比特性ExoPlayerMediaPlayerijkplayerDASH/HLS支持✔️部分✔️硬件解码优化✔️✔️✔️自定义渲染器✔️✖️有限播放状态精细控制✔️✖️中等官方维护GoogleAOSP社区包体积增加~1.2MB0MB~3MBExoPlayer的独特价值在于可扩展架构通过自定义Renderers、Extractors和DataSources可以支持几乎任何媒体格式自适应比特率根据网络条件自动切换不同质量的视频流精确控制提供比MediaPlayer更细粒度的播放状态管理和监听现代协议支持对DASH、HLS、SmoothStreaming等流媒体协议的原生支持// 项目级build.gradle中添加仓库 allprojects { repositories { google() jcenter() } }提示虽然ExoPlayer支持API级别16但建议最低兼容到API 21以获得最佳硬件解码支持2. 基础集成步骤让我们从最基本的集成开始。假设你正在开发一个全新的Android应用需要添加视频播放功能。2.1 添加依赖首先在模块的build.gradle文件中添加依赖dependencies { implementation com.google.android.exoplayer:exoplayer-core:2.18.1 implementation com.google.android.exoplayer:exoplayer-ui:2.18.1 // 如果需要DASH支持 implementation com.google.android.exoplayer:exoplayer-dash:2.18.1 // 如果需要HLS支持 implementation com.google.android.exoplayer:exoplayer-hls:2.18.1 }ExoPlayer采用模块化设计你可以只引入需要的功能组件exoplayer-core核心功能exoplayer-ui预制播放控件和界面exoplayer-dashDASH流媒体支持exoplayer-hlsHLS流媒体支持exoplayer-rtspRTSP协议支持2.2 布局文件配置在XML布局中添加PlayerViewcom.google.android.exoplayer2.ui.PlayerView android:idid/player_view android:layout_widthmatch_parent android:layout_height200dp app:show_bufferingwhen_playing app:surface_typetexture_view app:resize_modefit/关键属性说明show_buffering缓冲时显示指示器surface_type使用texture_view支持动画变换或surface_view性能更好resize_mode视频缩放模式fit/fixed_width/fixed_height/fill/zoom2.3 初始化播放器在Activity或Fragment中初始化class VideoPlayerActivity : AppCompatActivity() { private lateinit var player: ExoPlayer private lateinit var playerView: PlayerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_player) playerView findViewById(R.id.player_view) // 创建播放器实例 player ExoPlayer.Builder(this) .setSeekForwardIncrementMs(10000) // 快进10秒 .setSeekBackIncrementMs(10000) // 后退10秒 .build() playerView.player player // 准备媒体资源 val mediaItem MediaItem.fromUri(https://example.com/video.mp4) player.setMediaItem(mediaItem) player.prepare() // 自动开始播放根据需要 player.playWhenReady true } override fun onDestroy() { super.onDestroy() player.release() } }3. 高级配置与优化基础集成完成后让我们深入一些高级配置这些将显著提升播放体验。3.1 硬件解码配置ExoPlayer默认会尝试使用硬件解码通过MediaCodec但我们可以进行更精细的控制val renderersFactory DefaultRenderersFactory(this) .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) .setMediaCodecSelector(object : MediaCodecSelector { override fun getDecoderInfos( mimeType: String, requiresSecureDecoder: Boolean, requiresTunnelingDecoder: Boolean ): ListMediaCodecInfo { // 优先选择硬件解码器 val decoderInfos MediaCodecUtil.getDecoderInfos( mimeType, requiresSecureDecoder, requiresTunnelingDecoder ) return decoderInfos.sortedWith(compareBy( { !it.isHardwareAccelerated }, // 硬件加速优先 { it.name } // 按名称排序 )) } }) // 使用自定义的RenderersFactory创建播放器 player ExoPlayer.Builder(this) .setRenderersFactory(renderersFactory) .build()3.2 自适应比特率流对于网络视频流自适应比特率(ABR)能根据网络条件自动调整视频质量val bandwidthMeter DefaultBandwidthMeter.Builder(this) .setInitialBitrateEstimate(500000) // 初始比特率估计(500kbps) .build() val adaptiveTrackSelectionFactory AdaptiveTrackSelection.Factory( /* minDurationForQualityIncreaseMs */ 1000, /* maxDurationForQualityDecreaseMs */ 5000, /* minDurationToRetainAfterDiscardMs */ 2500, /* bandwidthFraction */ 0.75f ) val trackSelector DefaultTrackSelector(this, adaptiveTrackSelectionFactory) trackSelector.parameters trackSelector.buildUponParameters() .setMaxVideoSizeSd() // 根据需求设置最大视频尺寸 .build() player ExoPlayer.Builder(this) .setTrackSelector(trackSelector) .setBandwidthMeter(bandwidthMeter) .build()3.3 缓存优化对于需要重复播放的视频添加缓存可以显著减少流量消耗// 创建缓存 val cacheDir File(cacheDir, media_cache) val cache SimpleCache(cacheDir, NoOpCacheEvictor()) // 创建缓存数据源工厂 val upstreamFactory DefaultHttpDataSource.Factory() .setConnectTimeoutMs(5000) .setReadTimeoutMs(10000) val cacheDataSourceFactory CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory(upstreamFactory) .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) // 使用缓存数据源创建媒体源 val mediaSource ProgressiveMediaSource.Factory(cacheDataSourceFactory) .createMediaItem(MediaItem.fromUri(videoUrl))4. 处理直播流媒体ExoPlayer对直播协议的支持是其强大之处让我们看看如何实现DASH和HLS直播。4.1 DASH直播集成// 添加依赖implementation com.google.android.exoplayer:exoplayer-dash:2.18.1 val dashMediaSource DashMediaSource.Factory( DefaultDashChunkSource.Factory(DefaultHttpDataSource.Factory()), DefaultHttpDataSource.Factory() ).createMediaItem(MediaItem.fromUri(https://example.com/live.mpd)) player.setMediaSource(dashMediaSource) player.prepare()DASH配置要点MPD(Media Presentation Description)文件是DASH流的核心清单确保服务器支持CORS否则可能遇到跨域问题对于DRM保护的内容需要额外配置License服务器信息4.2 HLS直播集成// 添加依赖implementation com.google.android.exoplayer:exoplayer-hls:2.18.1 val hlsMediaSource HlsMediaSource.Factory( DefaultHttpDataSource.Factory() ).setAllowChunklessPreparation(true) // 启用分块less准备 .createMediaItem(MediaItem.fromUri(https://example.com/live.m3u8)) player.setMediaSource(hlsMediaSource) player.prepare()HLS优化技巧使用setAllowChunklessPreparation(true)加速初始加载对于低延迟HLS考虑启用lowLatencyMode监控PLAYBACK_STATE_CHANGED事件处理直播中的中断4.3 直播状态处理直播与点播不同需要特别处理各种状态player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { when (playbackState) { Player.STATE_BUFFERING - showLoading() Player.STATE_READY - hideLoading() Player.STATE_ENDED - handleStreamEnd() Player.STATE_IDLE - handleError() } } override fun onPlayerError(error: PlaybackException) { when (error.errorCode) { PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED - showNetworkError() PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW - player.seekToDefaultPosition() // 自动恢复 else - showGenericError() } } })5. 性能监控与问题排查即使配置得当实际环境中仍可能遇到性能问题。建立有效的监控机制至关重要。5.1 关键性能指标val bandwidthMeter DefaultBandwidthMeter.Builder(this) .setEventListener { _, _, bitrateEstimate - Log.d(Network, 当前估计比特率: ${bitrateEstimate / 1000} kbps) } .build() player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(state: Int) { val videoFormat player.videoFormat videoFormat?.let { Log.d(VideoInfo, 分辨率: ${it.width}x${it.height} 码率: ${it.bitrate / 1000} kbps 编码: ${it.codecs} 帧率: ${it.frameRate} .trimIndent()) } } })5.2 常见问题解决方案问题1首帧加载慢预加载媒体player.prepare()后延迟几秒再显示播放器启用预缓冲DefaultLoadControl.Builder().setBufferDurationsMs(minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs)问题2内存泄漏确保在Activity/Fragment的onDestroy中调用player.release()使用弱引用持有Player相关监听器问题3播放卡顿// 在Application类中全局设置 ExoPlayer.setDetachSurfaceTimeoutMs(10000) // 延长surface分离超时 DefaultRenderersFactory.setExtensionRendererMode( DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)5.3 自定义日志class PlayerEventLogger(tag: String) : AnalyticsListener { override fun onAudioDisabled(event: AnalyticsListener.EventTime, decodingDisabledEvent: DecoderCounters) { Log.d(tag, 音频解码器释放) } override fun onDroppedVideoFrames(event: AnalyticsListener.EventTime, droppedFrames: Int, elapsedMs: Long) { Log.w(tag, 丢帧: $droppedFrames (${elapsedMs}ms)) } } // 使用 player.addAnalyticsListener(PlayerEventLogger(PlayerStats))6. 进阶功能实现掌握了基础播放功能后让我们探索一些提升用户体验的进阶特性。6.1 画中画模式Android 8.0支持画中画(PiP)模式// AndroidManifest.xml中配置Activity activity android:name.PlayerActivity android:supportsPictureInPicturetrue android:configChangesscreenSize|smallestScreenSize|screenLayout|orientation / // 在Activity中 private fun enterPipMode() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val params PictureInPictureParams.Builder() .setAspectRatio(Rational(16, 9)) .build() enterPictureInPictureMode(params) } } override fun onPictureInPictureModeChanged( isInPiP: Boolean, newConfig: Configuration? ) { if (isInPiP) { playerView.hideController() } else { playerView.showController() } }6.2 自定义控件ExoPlayer的UI组件高度可定制com.google.android.exoplayer2.ui.PlayerView android:idid/player_view android:layout_widthmatch_parent android:layout_heightwrap_content app:controller_layout_idlayout/custom_controls app:show_timeout3000 /com.google.android.exoplayer2.ui.PlayerView创建res/layout/custom_controls.xmlLinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal ImageButton android:idid/exo_play android:layout_width48dp android:layout_height48dp android:srcdrawable/custom_play/ ImageButton android:idid/exo_pause android:layout_width48dp android:layout_height48dp android:srcdrawable/custom_pause/ TextView android:idid/exo_position android:layout_widthwrap_content android:layout_heightwrap_content/ SeekBar android:idid/exo_progress android:layout_width0dp android:layout_heightwrap_content android:layout_weight1/ TextView android:idid/exo_duration android:layout_widthwrap_content android:layout_heightwrap_content/ /LinearLayout6.3 多音轨/字幕支持// 创建多轨道选择器 val trackSelector DefaultTrackSelector(this).apply { setParameters(buildUponParameters() .setPreferredTextLanguage(zh) // 首选中文 .setPreferredAudioLanguage(en) // 次选英文 ) } player ExoPlayer.Builder(this) .setTrackSelector(trackSelector) .build() // 手动选择轨道 fun selectTrack(type: Int, index: Int) { val mappedTrackInfo trackSelector.currentMappedTrackInfo mappedTrackInfo?.let { val rendererIndex it.getRendererIndex(type) if (rendererIndex ! C.INDEX_UNSET) { trackSelector.setParameters( trackSelector.parameters .buildUpon() .setSelectionOverride( rendererIndex, it.getTrackGroups(rendererIndex), TrackSelectionOverride(index) ) ) } } }7. 实战完整播放器实现结合前面所有知识点我们来实现一个完整的视频播放器包含以下功能本地视频和网络视频播放直播流支持画中画模式自定义控制界面多清晰度切换7.1 播放器封装class VideoPlayer( private val context: Context, private val playerView: PlayerView, private val cacheDir: File ) : Player.Listener { private var player: ExoPlayer? null private val bandwidthMeter DefaultBandwidthMeter.Builder(context).build() private val cache SimpleCache(cacheDir, NoOpCacheEvictor()) init { initializePlayer() } private fun initializePlayer() { val trackSelector DefaultTrackSelector(context).apply { parameters buildUponParameters() .setMaxVideoSizeSd() .build() } val loadControl DefaultLoadControl.Builder() .setBufferDurationsMs( MIN_BUFFER_MS, MAX_BUFFER_MS, PLAYBACK_BUFFER_MS, REBUFFER_MS ) .build() player ExoPlayer.Builder(context) .setTrackSelector(trackSelector) .setLoadControl(loadControl) .setBandwidthMeter(bandwidthMeter) .build().apply { addListener(thisVideoPlayer) playWhenReady true } playerView.player player } fun playMedia(mediaUri: Uri, isLive: Boolean false) { val mediaItem MediaItem.fromUri(mediaUri) val dataSourceFactory CacheDataSource.Factory() .setCache(cache) .setUpstreamDataSourceFactory( DefaultHttpDataSource.Factory() .setUserAgent(ExoPlayerDemo) ) val mediaSource when { mediaUri.path?.endsWith(.mpd) true - DashMediaSource.Factory(dataSourceFactory) .createMediaItem(mediaItem) mediaUri.path?.endsWith(.m3u8) true - HlsMediaSource.Factory(dataSourceFactory) .setAllowChunklessPreparation(!isLive) .createMediaItem(mediaItem) else - ProgressiveMediaSource.Factory(dataSourceFactory) .createMediaItem(mediaItem) } player?.setMediaSource(mediaSource) player?.prepare() } fun release() { player?.release() player null } companion object { private const val MIN_BUFFER_MS 5000 private const val MAX_BUFFER_MS 10000 private const val PLAYBACK_BUFFER_MS 2000 private const val REBUFFER_MS 5000 } }7.2 使用示例class MainActivity : AppCompatActivity() { private lateinit var videoPlayer: VideoPlayer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val cacheDir File(cacheDir, media_cache) val playerView findViewByIdPlayerView(R.id.player_view) videoPlayer VideoPlayer(this, playerView, cacheDir) // 播放网络视频 videoPlayer.playMedia(Uri.parse(https://example.com/video.mp4)) // 或者播放直播流 // videoPlayer.playMedia(Uri.parse(https://example.com/live.m3u8), true) } override fun onDestroy() { super.onDestroy() videoPlayer.release() } }7.3 质量切换实现fun showQualityDialog() { val trackSelector player?.trackSelector as? DefaultTrackSelector val mappedTrackInfo trackSelector?.currentMappedTrackInfo ?: return val trackGroups mappedTrackInfo.getTrackGroups( mappedTrackInfo.getRendererIndex(C.TRACK_TYPE_VIDEO) ) val qualityItems mutableListOfQualityItem() for (i in 0 until trackGroups.length) { val group trackGroups.get(i) for (j in 0 until group.length) { val format group.getFormat(j) qualityItems.add(QualityItem( id j, height format.height, bitrate format.bitrate )) } } AlertDialog.Builder(this) .setTitle(选择视频质量) .setItems(qualityItems.map { ${it.height}p (${it.bitrate / 1000}kbps) }.toTypedArray()) { _, which - selectVideoTrack(qualityItems[which].id) } .show() } private fun selectVideoTrack(index: Int) { val trackSelector player?.trackSelector as? DefaultTrackSelector ?: return val mappedTrackInfo trackSelector.currentMappedTrackInfo ?: return val rendererIndex mappedTrackInfo.getRendererIndex(C.TRACK_TYPE_VIDEO) if (rendererIndex C.INDEX_UNSET) return trackSelector.setParameters( trackSelector.parameters.buildUpon() .clearSelectionOverrides(rendererIndex) .setSelectionOverride( rendererIndex, mappedTrackInfo.getTrackGroups(rendererIndex), TrackSelectionOverride(index) ) ) }8. 疑难解答与最佳实践在实际项目中集成ExoPlayer时开发者常会遇到一些典型问题。以下是经过多个项目验证的解决方案。8.1 常见错误处理问题播放HLS流时出现404错误原因HLS播放列表(.m3u8)可能引用了不存在的.ts分片解决方案val hlsMediaSource HlsMediaSource.Factory(dataSourceFactory) .setAllowChunklessPreparation(true) .setLoadErrorHandlingPolicy(object : LoadErrorHandlingPolicy { override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorInfo): Long { return if (loadErrorInfo.responseCode 404) { 1000 // 1秒后重试 } else { C.TIME_UNSET // 使用默认策略 } } }) .createMediaItem(mediaItem)问题视频与音频不同步原因通常由于时间戳不正确或解码延迟导致解决方案// 在创建播放器时配置 player ExoPlayer.Builder(this) .setClock(DefaultClock()) .setRenderersFactory(DefaultRenderersFactory(this) .setEnableAudioTrackPlaybackParams(true) .setEnableVideoFrameReleaseListener(true) ) .build()8.2 内存优化技巧视频列表内存管理// 在RecyclerView.Adapter中 override fun onViewRecycled(holder: VideoViewHolder) { holder.playerView.player?.stop() holder.playerView.player?.clearMediaItems() } // 配置播放器池 object PlayerPool { private val pool ArrayDequeExoPlayer(3) fun getPlayer(context: Context): ExoPlayer { return pool.pollLast() ?: ExoPlayer.Builder(context).build() } fun releasePlayer(player: ExoPlayer) { if (pool.size 3) { player.stop() player.clearMediaItems() pool.addLast(player) } else { player.release() } } }大分辨率视频处理// 在播放前检查设备能力 fun canPlayVideo(width: Int, height: Int): Boolean { val displayMetrics Resources.getSystem().displayMetrics val maxDimension max(displayMetrics.widthPixels, displayMetrics.heightPixels) return width maxDimension * 1.5 height maxDimension * 1.5 } // 如果超出设备能力使用转码后的低分辨率版本 val mediaItem if (canPlayVideo(originalWidth, originalHeight)) { MediaItem.fromUri(highResUri) } else { MediaItem.fromUri(lowResUri) }8.3 电池效率优化后台播放控制// 在Service中 class PlaybackService : Service() { private val wakeLock by lazy { (getSystemService(POWER_SERVICE) as PowerManager) .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, VideoPlayer::Lock) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { wakeLock.acquire(10 * 60 * 1000L /*10分钟*/) return START_STICKY } override fun onDestroy() { wakeLock.release() super.onDestroy() } } // 在播放器中 player.setWakeMode(C.WAKE_MODE_NETWORK) // 或WAKE_MODE_LOCAL自适应比特率策略val adaptiveTrackSelectionFactory AdaptiveTrackSelection.Factory( /* minDurationForQualityIncreaseMs */ 2000, // 更保守的质量提升 /* maxDurationForQualityDecreaseMs */ 10000, // 更慢的质量下降 /* minDurationToRetainAfterDiscardMs */ 5000, /* bandwidthFraction */ 0.7f // 使用更少的带宽余量 )9. 测试与调试确保播放器在各种条件下稳定工作是发布前的关键步骤。ExoPlayer提供了丰富的调试工具。9.1 自动化测试UI测试示例RunWith(AndroidJUnit4::class) class PlayerTest { get:Rule val activityRule ActivityScenarioRule(MainActivity::class.java) Test fun testPlayback() { // 启动播放 onView(withId(R.id.play_button)).perform(click()) // 验证播放状态 val player getPlayerInstance() InstrumentationRegistry.getInstrumentation().runOnMainSync { assertThat(player.playbackState).isEqualTo(Player.STATE_READY) } // 模拟网络变化 val constraints ConnectivityManager.NetworkCallback() val cm getSystemService(ConnectivityManager::class.java) cm.registerNetworkCallback( NetworkRequest.Builder().build(), constraints ) // 强制切换到弱网 TestNetworkManager.setNetworkQuality(NetworkQuality.POOR) // 验证自适应降级 Thread.sleep(5000) // 等待自适应调整 val format player.videoFormat assertThat(format.bitrate).isLessThan(1000000) // 1Mbps } private fun getPlayerInstance(): ExoPlayer { var player: ExoPlayer? null activityRule.scenario.onActivity { player it.playerView.player as? ExoPlayer } return player ?: throw IllegalStateException(Player not initialized) } }9.2 性能分析使用Android Profiler监控播放器性能CPU分析关注MediaCodec线程的CPU使用率内存分析检查MediaCodec和Surface相关的内存分配网络分析通过DefaultBandwidthMeter记录带宽波动关键指标日志player.addAnalyticsListener(object : AnalyticsListener { override fun onVideoSizeChanged(eventTime: EventTime, size: VideoSize) { log(视频尺寸: ${size.width}x${size.height}) } override fun onDroppedVideoFrames( eventTime: EventTime, droppedFrames: Int, elapsedMs: Long ) { log(丢帧: $droppedFrames (${elapsedMs}ms)) } override fun onBandwidthEstimate( eventTime: EventTime, totalLoadTimeMs: Long, totalBytesLoaded: Long, bitrateEstimate: Long ) { log(带宽估计: ${bitrateEstimate / 1000} kbps) } })9.3 兼容性测试创建测试矩阵确保覆盖主要设备和Android版本设备类型Android版本测试重点低端设备8.0内存使用、解码性能中端设备9.0自适应比特率切换高端设备114K/HDR支持各种屏幕比例10视频缩放和裁剪行为不同网络条件全版本缓冲策略和恢复能力使用Firebase Test Lab自动化这些测试android { testOptions { execution ANDROIDX_TEST_ORCHESTRATOR } } dependencies { androidTestUtil androidx.test:orchestrator:1.4.2 androidTestImplementation androidx.test.ext:junit:1.1.3 androidTestImplementation androidx.test:rules:1.4.0 }10. 发布与监控播放器上线后持续监控其表现至关重要。以下是如何建立有效的监控体系。10.1 关键指标收集定义监控指标class PlayerMetrics private constructor() { // 单例实现 companion object { Volatile private var instance: PlayerMetrics? null fun getInstance(): PlayerMetrics { return instance ?: synchronized(this) { instance ?: PlayerMetrics().also { instance it } } } } private val metrics mutableMapOfString, Any() fun logEvent(event: String, data: MapString, Any) { // 实际项目中发送到分析服务器 metrics[event] data Log.d(PlayerMetrics, $event: $data) } fun getPlaybackStats(): MapString, Any { return metrics.toMap() } } // 使用示例 PlayerMetrics.getInstance().logEvent(playback_start, mapOf( video_id to currentVideoId, quality to currentQuality ))核心监控点播放开始成功率平均起播时间卡顿次数和时长比特率切换频率错误率和错误类型10.2 崩溃报告集成使用Firebase Crashlytics捕获播放器相关崩溃player.addListener(object : Player.Listener { override fun onPlayerError(error: PlaybackException) { FirebaseCrashlytics.getInstance().log(Player error: ${error.errorCode}) FirebaseCrashlytics.getInstance().recordException(error) } }) // 配置额外的调试信息 FirebaseCrashlytics.getInstance().setCustomKey(player_version, ExoPlayerLibraryInfo.VERSION) FirebaseCrashlytics.getInstance().setCustomKey(device_support, when { Util.SDK_INT 21 - legacy Util.SDK_INT 23 - basic else - full } )10.3 A/B测试策略通过实验优化播放参数// 定义实验组 enum class BufferExperiment { DEFAULT, AGGRESSIVE, CONSERVATIVE } // 获取当前用户分组 val experiment RemoteConfig.getInstance().getString(buffer_strategy).let { when (it) { aggressive - BufferExperiment.AGGRESSIVE conservative - BufferExperiment.CONSERVATIVE else - BufferExperiment.DEFAULT } } // 应用实验配置 when (experiment) { BufferExperiment.AGGRESSIVE - { DefaultLoadControl.Builder() .setBufferDurationsMs(3000, 15000, 1000, 2000) } BufferExperiment.CONSERVATIVE - { DefaultLoadControl.Builder() .setBufferDurationsMs(10000, 30000, 5000, 10000) } else - DefaultLoadControl.Builder() }.build().apply { player.setLoadControl(this) }10.4 用户反馈集成// 在播放器控件中添加反馈按钮 playerView.setCustomErrorMessage { error - // 显示自定义错误界面 val feedbackView layoutInflater.inflate( R.layout.player_error, playerView, false ) feedbackView.findViewByIdButton(R.id.report_button).setOnClickListener { showFeedbackDialog(error) } playerView.addView(feedbackView) feedbackView } private fun showFeedbackDialog(error: PlaybackException) { MaterialAlertDialogBuilder(this) .setTitle(播放遇到问题) .setMessage(错误代码: ${error.errorCode}\n请描述您遇到的问题) .setView(R.layout.feedback_form) .setPositiveButton(提交) { _, _ - submitFeedback() } .show() }11. 未来兼容性随着Android平台和媒体技术的发展保持播放器的前瞻性很重要。11.1 Android 13新特性预测性媒体准备if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { val mediaController MediaControllerCompat(this, SessionToken(this, ComponentName(this, PlayerService::class.java))) mediaController.sendCommand( androidx.media3.session.command.PREPARE_MEDIA, Bundle().apply { putString(media_id, nextVideoId) }, null ) }音频焦点处理改进val audioAttributes AudioAttributes.Builder() .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_MOVIE) .setAllowedCapturePolicy( if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM } else { AudioAttributes.ALLOW_CAPTURE_BY_ALL } ) .build() player.setAudioAttributes(audioAttributes, /* handleAudioFocus */ true)11.2 媒体3扩展库Google正在开发Media3库作为ExoPlayer的下一代演进// 未来迁移到 implementation androidx.media3:media3-exoplayer:1.0.0 implementation androidx.media3:media3-ui