一、项目介绍
1. 背景与意义
随着移动互联网的发展,视频已成为流量最大的媒体形式之一。无论是社交短视频、在线视频播放、还是直播推流功能,Android 应用对视频播放的需求无处不在。要实现一个稳定、流畅、功能丰富的视频播放模块,需要掌握多种底层 API 与第三方框架,才能应对不同网络、格式、编码与业务场景。
本教程将全面介绍在 Android 上实现视频播放的多种方案,包括:
-
系统
VideoView
:最简单的 API,快速集成 -
原生
MediaPlayer
+SurfaceView
:更灵活的底层实现 -
原生
MediaPlayer
+TextureView
:支持旋转、缩放等变换 -
ExoPlayer:Google 推荐,支持 DASH/HLS、缓存、DRM
-
Media3(Jetpack)**:继承 ExoPlayer,未来趋势
-
第三方播放器:如 IJKPlayer(FFmpeg)、Vitamio 等
-
低层
MediaCodec
:自定义解码管线,适合特殊需求 -
Compose +
AndroidView
:在 Jetpack Compose 中集成视频
通过对比各方案的用法、优缺点、适用场景,以及完整的示例代码,你将能够根据项目需求,快速抉择并集成视频播放功能。
二、相关知识
在深入代码之前,请先了解以下核心概念:
-
容器类型
-
SurfaceView
:独立的渲染缓冲区,性能高但不支持普通 View 层级变换。 -
TextureView
:在普通 View 层中渲染,支持平移、旋转、缩放,但性能略低。 -
PlayerView
/StyledPlayerView
:ExoPlayer 提供的封装视图。
-
-
播放器 API 层
-
VideoView
:封装了MediaPlayer
+SurfaceView
,快速集成但可定制性差。 -
MediaPlayer
:Android 原生媒体播放引擎,支持本地与网络流媒体。 -
ExoPlayer
:Google 开源,支持 DASH、HLS、SmoothStreaming、自定义数据源。 -
Media3
:更高层的 Jetpack 媒体库,未来推荐。
-
-
流媒体协议
-
HTTP Progressive:直接下载 MP4、MKV 等文件。
-
HLS (M3U8):通过
#EXTM3U
播放器边下载边播放。 -
DASH (MPD):动态自适应比特率。
-
-
DRM 与清晰度切换
-
ExoPlayer 和 Media3 内置支持 Widevine、PlayReady 等 DRM。
-
动态切换分辨率、码率,需实现
TrackSelector
或DefaultTrackSelector
。
-
-
Lifecycle 与回收
-
Activity/Fragment 的
onStart
/onStop
或onResume
/onPause
中控制播放器的play()
/pause()
,并在销毁时release()
。
-
三、实现思路
我们将按以下顺序实现并对比各方案:
-
方案一:
VideoView
-
方案二:
MediaPlayer
+SurfaceView
-
方案三:
MediaPlayer
+TextureView
-
方案四:ExoPlayer
-
方案五:Media3
-
方案六:IJKPlayer(FFmpeg)
-
方案七:
MediaCodec
自解码 -
方案八:Jetpack Compose 集成方案
每个方案都将提供:
-
布局示例
-
Activity/Fragment 代码
-
生命周期管理
-
错误处理与回调
最后,我们将总结各方案优缺点,并给出不同场景的最佳实践建议。
四、环境与依赖
// app/build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.videoplaydemo"
minSdkVersion 21
targetSdkVersion 34
}
buildFeatures { viewBinding true }
kotlinOptions { jvmTarget = "1.8" }
}
dependencies {
// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer:2.18.2'
// Media3
implementation "androidx.media3:media3-exoplayer:1.0.0"
implementation "androidx.media3:media3-ui:1.0.0"
// IJKPlayer
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
// Compose (for Compose 方案)
implementation "androidx.compose.ui:ui:1.4.0"
implementation "androidx.compose.material:material:1.4.0"
implementation "androidx.activity:activity-compose:1.7.0"
}
五、整合代码
// =======================================================
// 文件: res/layout/activity_main.xml
// 描述: 简单导航,选择不同播放方案
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:padding="16dp"
android:layout_width="match_parent" android:layout_height="match_parent">
<Button android:id="@+id/btnVideoView" android:text="VideoView 方案"/>
<Button android:id="@+id/btnSurface" android:text="MediaPlayer+SurfaceView"/>
<Button android:id="@+id/btnTexture" android:text="MediaPlayer+TextureView"/>
<Button android:id="@+id/btnExo" android:text="ExoPlayer 方案"/>
<Button android:id="@+id/btnMedia3" android:text="Media3 方案"/>
<Button android:id="@+id/btnIJK" android:text="IJKPlayer 方案"/>
<Button android:id="@+id/btnCodec" android:text="MediaCodec 自解码"/>
<Button android:id="@+id/btnCompose" android:text="Compose 集成方案"/>
</LinearLayout>
// =======================================================
// 文件: MainActivity.kt
// 描述: 跳转到各个示例 Activity
// =======================================================
package com.example.videoplaydemo
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.videoplaydemo.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(s: Bundle?) {
super.onCreate(s)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnVideoView .setOnClickListener { startActivity(Intent(this, VideoViewActivity::class.java)) }
binding.btnSurface .setOnClickListener { startActivity(Intent(this, SurfaceActivity::class.java)) }
binding.btnTexture .setOnClickListener { startActivity(Intent(this, TextureActivity::class.java)) }
binding.btnExo .setOnClickListener { startActivity(Intent(this, ExoActivity::class.java)) }
binding.btnMedia3 .setOnClickListener { startActivity(Intent(this, Media3Activity::class.java)) }
binding.btnIJK .setOnClickListener { startActivity(Intent(this, IjkActivity::class.java)) }
binding.btnCodec .setOnClickListener { startActivity(Intent(this, CodecActivity::class.java)) }
binding.btnCompose .setOnClickListener { startActivity(Intent(this, ComposeActivity::class.java)) }
}
}
// =======================================================
// 方案一:VideoViewActivity.kt
// Layout: res/layout/activity_video_view.xml
// =======================================================
// activity_video_view.xml
/*
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent" android:layout_height="match_parent"/>
<ProgressBar android:id="@+id/progress"
style="?android:attr/progressBarStyleLarge"
android:layout_gravity="center"/>
</FrameLayout>
*/
// VideoViewActivity.kt
package com.example.videoplaydemo
import android.net.Uri
import android.os.Bundle
import android.widget.MediaController
import androidx.appcompat.app.AppCompatActivity
import com.example.videoplaydemo.databinding.ActivityVideoViewBinding
class VideoViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityVideoViewBinding
override fun onCreate(s: Bundle?) {
super.onCreate(s)
binding = ActivityVideoViewBinding.inflate(layoutInflater)
setContentView(binding.root)
val uri = Uri.parse("https://www.example.com/video.mp4")
binding.progress.show()
binding.videoView.setVideoURI(uri)
binding.videoView.setMediaController(MediaController(this))
binding.videoView.setOnPreparedListener {
binding.progress.hide()
it.isLooping = true
binding.videoView.start()
}
}
override fun onPause(){ super.onPause(); binding.videoView.pause() }
override fun onResume(){ super.onResume(); binding.videoView.start() }
override fun onDestroy(){ super.onDestroy(); binding.videoView.stopPlayback() }
}
// =======================================================
// 方案二:SurfaceView + MediaPlayer
// File: res/layout/activity_surface.xml
// =======================================================
/*
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout ...>
<SurfaceView android:id="@+id/surfaceView" .../>
<ProgressBar android:id="@+id/progress" .../>
</FrameLayout>
*/
// SurfaceActivity.kt
package com.example.videoplaydemo
import android.media.MediaPlayer
import android.os.Bundle
import android.view.SurfaceHolder
import androidx.appcompat.app.AppCompatActivity
import com.example.videoplaydemo.databinding.ActivitySurfaceBinding
class SurfaceActivity: AppCompatActivity(), SurfaceHolder.Callback {
private lateinit var binding: ActivitySurfaceBinding
private var player: MediaPlayer? = null
override fun onCreate(s: Bundle?){ super.onCreate(s)
binding = ActivitySurfaceBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.surfaceView.holder.addCallback(this)
}
override fun surfaceCreated(holder: SurfaceHolder) {
player = MediaPlayer().apply {
setDataSource("https://.../video.mp4")
setDisplay(holder)
setOnPreparedListener {
binding.progress.hide()
isLooping = true; start()
}
prepareAsync()
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
player?.release(); player = null
}
override fun surfaceChanged(h: SurfaceHolder, f:Int, w:Int, h2:Int){}
}
// =======================================================
// 方案三:TextureView + MediaPlayer
// File: res/layout/activity_texture.xml
// =======================================================
/*
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout ...>
<TextureView android:id="@+id/textureView" .../>
<ProgressBar android:id="@+id/progress" .../>
</FrameLayout>
*/
// TextureActivity.kt
package com.example.videoplaydemo
import android.graphics.SurfaceTexture
import android.media.MediaPlayer
import android.os.Bundle
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import com.example.videoplaydemo.databinding.ActivityTextureBinding
class TextureActivity: AppCompatActivity(), TextureView.SurfaceTextureListener {
private lateinit var binding: ActivityTextureBinding
private var player: MediaPlayer? = null
override fun onCreate(s: Bundle?){ super.onCreate(s)
binding = ActivityTextureBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textureView.surfaceTextureListener = this
}
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w:Int, h:Int){
player = MediaPlayer().apply {
setSurface(android.view.Surface(st))
setDataSource("https://.../video.mp4")
setOnPreparedListener {
binding.progress.hide()
isLooping=true; start()
}
prepareAsync()
}
}
override fun onSurfaceTextureSizeChanged(st:SurfaceTexture,w:Int,h:Int){}
override fun onSurfaceTextureDestroyed(st:SurfaceTexture):Boolean{ player?.release(); player=null; return true }
override fun onSurfaceTextureUpdated(st:SurfaceTexture){}
}
// =======================================================
// 方案四:ExoPlayer
// File: res/layout/activity_exo.xml
// =======================================================
/*
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/playerView" .../>
*/
// ExoActivity.kt
package com.example.videoplaydemo
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.videoplaydemo.databinding.ActivityExoBinding
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
class ExoActivity: AppCompatActivity() {
private lateinit var binding: ActivityExoBinding
private var player: ExoPlayer? = null
override fun onCreate(s: Bundle?){ super.onCreate(s)
binding = ActivityExoBinding.inflate(layoutInflater)
setContentView(binding.root)
player = ExoPlayer.Builder(this).build().also {
binding.playerView.player = it
val mediaItem = MediaItem.fromUri(Uri.parse("https://.../video.mp4"))
it.setMediaItem(mediaItem); it.repeatMode = ExoPlayer.REPEAT_MODE_ALL
it.prepare(); it.play()
}
}
override fun onPause(){ super.onPause(); player?.pause() }
override fun onResume(){ super.onResume(); player?.play() }
override fun onDestroy(){ super.onDestroy(); player?.release(); player=null }
}
// =======================================================
// 方案五:Media3 (Jetpack)
// File: res/layout/activity_media3.xml
// =======================================================
/*
<?xml version="1.0" encoding="utf-8"?>
<androidx.media3.ui.PlayerView ... android:id="@+id/playerView"/>
*/
// Media3Activity.kt
package com.example.videoplaydemo
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import com.example.videoplaydemo.databinding.ActivityMedia3Binding
class Media3Activity: AppCompatActivity() {
private lateinit var binding: ActivityMedia3Binding
private var player: ExoPlayer? = null
override fun onCreate(s: Bundle?){ super.onCreate(s)
binding = ActivityMedia3Binding.inflate(layoutInflater)
setContentView(binding.root)
player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse("https://.../video.mp4")))
repeatMode = ExoPlayer.REPEAT_MODE_ALL; prepare(); play()
}
binding.playerView.player = player
}
override fun onPause(){ super.onPause(); player?.pause() }
override fun onResume(){ super.onResume(); player?.play() }
override fun onDestroy(){ super.onDestroy(); player?.release(); player=null }
}
// =======================================================
// 方案六:IJKPlayer
// File: res/layout/activity_ijk.xml
// =======================================================
/*
<?xml version="1.0" encoding="utf-8"?>
<tv.danmaku.ijk.media.player.IjkVideoView ... android:id="@+id/ijkView"/>
*/
// IjkActivity.kt
package com.example.videoplaydemo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import tv.danmaku.ijk.media.player.IjkMediaPlayer
import com.example.videoplaydemo.databinding.ActivityIjkBinding
class IjkActivity: AppCompatActivity() {
private lateinit var binding: ActivityIjkBinding
override fun onCreate(s: Bundle?){ super.onCreate(s)
binding = ActivityIjkBinding.inflate(layoutInflater)
setContentView(binding.root)
IjkMediaPlayer.loadLibrariesOnce(null); IjkMediaPlayer.native_profileBegin("libijkplayer.so")
binding.ijkView.setVideoPath("https://.../video.mp4")
binding.ijkView.start()
}
override fun onDestroy(){ super.onDestroy()
binding.ijkView.stopPlayback()
IjkMediaPlayer.native_profileEnd()
}
}
// =======================================================
// 方案七:MediaCodec 自解码(略示意)
// File: CodecActivity.kt
// =======================================================
// 此处省略数百行自解码代码,仅做简要示意:
// - 使用 MediaExtractor 分离轨道
// - 用 MediaCodec 解码到 Surface
// - 用 SurfaceView / TextureView 渲染
// 建议查阅官方文档与 Codelab 深入实现。
// =======================================================
// 方案八:Compose 集成
// File: ComposeActivity.kt
// =======================================================
package com.example.videoplaydemo
import android.net.Uri
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
class ComposeActivity: AppCompatActivity() {
override fun onCreate(s: Bundle?){ super.onCreate(s)
val player = ExoPlayer.Builder(this).build().apply {
setMediaItem(MediaItem.fromUri(Uri.parse("https://.../video.mp4")))
prepare(); play()
}
setContent {
Box(Modifier.fillMaxSize()) {
AndroidView(factory = { ctx ->
PlayerView(ctx).apply {
this.player = player; useController=true
}
}, modifier=Modifier.fillMaxSize())
}
}
}
override fun onDestroy(){ super.onDestroy()
player.release()
}
}
六、代码解读
-
VideoView
-
简单易用,封装度高;
-
无法控制底层缓冲或自定义渲染;
-
-
MediaPlayer
+SurfaceView
-
适合大批量视频或直播;
-
性能高,但不支持 View 变换;
-
-
MediaPlayer
+TextureView
-
支持任意 2D 变换(旋转、缩放);
-
性能次于 SurfaceView;
-
-
ExoPlayer
-
支持 DASH、HLS、自定义加载;
-
拥有丰富扩展(缓存、DRM、字幕);
-
-
Media3
-
Jetpack 新推荐,兼容未来更新;
-
API 与 ExoPlayer 基本一致;
-
-
IJKPlayer
-
基于 FFmpeg,支持更多格式;
-
需部署 native 库,包体大;
-
-
MediaCodec
-
最低层控制,适合自定义渲染或特殊解码需求;
-
开发成本高;
-
-
Compose 集成
-
在 Compose 中可使用
AndroidView
嵌入任意 View; -
未来可期待原生 Compose 视频组件;
-
七、性能与优化
-
硬件加速
-
SurfaceView
与 ExoPlayer 默认硬件加速;
-
-
网络缓冲
-
ExoPlayer 可自定义
LoadControl
;
-
-
并发与切换
-
避免频繁
prepare()
/release()
;
-
-
内存管理
-
及时
release()
资源,避免泄漏;
-
-
UI 与渲染
-
避免在主线程做 heavy UI 操作;
-
八、项目总结与拓展
本文多角度、全方案地介绍了 Android 上几乎所有主流的视频播放实现方式,配以示例代码与优缺点对比,便于在不同业务场景中做出选择。未来可扩展:
-
自适应码率:HLS/DASH 动态切换
-
DRM:Protected clearplay
-
节省流量:集成缓存、预下载
-
UI 特效:滤镜、弹幕、画中画
九、FAQ
Q1:哪种方案最简单?
A:VideoView
,但可定制性最低。
Q2:推荐使用哪个?
A:ExoPlayer/Media3,功能最全,社区活跃。
Q3:如何播放直播 HLS?
A:ExoPlayer 直接 MediaItem.fromUri("https://.../live.m3u8")
即可。
Q4:IJKPlayer 包体大怎么办?
A:可定制 native 库,只打包需要的 ABI。
Q5:Compose 未来会有原生视频组件吗?
A:已在开发中,但目前仍需 AndroidView
嵌入。