在Android上實現(xiàn)視頻播放的多種方案
一、項目介紹
1. 背景與意義
隨著移動互聯(lián)網(wǎng)的發(fā)展,視頻已成為流量最大的媒體形式之一。無論是社交短視頻、在線視頻播放、還是直播推流功能,Android 應用對視頻播放的需求無處不在。要實現(xiàn)一個穩(wěn)定、流暢、功能豐富的視頻播放模塊,需要掌握多種底層 API 與第三方框架,才能應對不同網(wǎng)絡、格式、編碼與業(yè)務場景。
本教程將全面介紹在 Android 上實現(xiàn)視頻播放的多種方案,包括:
系統(tǒng)
VideoView:最簡單的 API,快速集成原生
MediaPlayer+SurfaceView:更靈活的底層實現(xiàn)原生
MediaPlayer+TextureView:支持旋轉(zhuǎn)、縮放等變換ExoPlayer:Google 推薦,支持 DASH/HLS、緩存、DRM
Media3(Jetpack)**:繼承 ExoPlayer,未來趨勢
第三方播放器:如 IJKPlayer(FFmpeg)、Vitamio 等
低層
MediaCodec:自定義解碼管線,適合特殊需求Compose +
AndroidView:在 Jetpack Compose 中集成視頻
通過對比各方案的用法、優(yōu)缺點、適用場景,以及完整的示例代碼,你將能夠根據(jù)項目需求,快速抉擇并集成視頻播放功能。
二、相關(guān)知識
在深入代碼之前,請先了解以下核心概念:
容器類型
SurfaceView:獨立的渲染緩沖區(qū),性能高但不支持普通 View 層級變換。TextureView:在普通 View 層中渲染,支持平移、旋轉(zhuǎn)、縮放,但性能略低。PlayerView/StyledPlayerView:ExoPlayer 提供的封裝視圖。
播放器 API 層
VideoView:封裝了MediaPlayer+SurfaceView,快速集成但可定制性差。MediaPlayer:Android 原生媒體播放引擎,支持本地與網(wǎng)絡流媒體。ExoPlayer:Google 開源,支持 DASH、HLS、SmoothStreaming、自定義數(shù)據(jù)源。Media3:更高層的 Jetpack 媒體庫,未來推薦。
流媒體協(xié)議
HTTP Progressive:直接下載 MP4、MKV 等文件。
HLS (M3U8):通過
#EXTM3U播放器邊下載邊播放。DASH (MPD):動態(tài)自適應比特率。
DRM 與清晰度切換
ExoPlayer 和 Media3 內(nèi)置支持 Widevine、PlayReady 等 DRM。
動態(tài)切換分辨率、碼率,需實現(xiàn)
TrackSelector或DefaultTrackSelector。
Lifecycle 與回收
Activity/Fragment 的
onStart/onStop或onResume/onPause中控制播放器的play()/pause(),并在銷毀時release()。
三、實現(xiàn)思路
我們將按以下順序?qū)崿F(xiàn)并對比各方案:
方案一:
VideoView方案二:
MediaPlayer+SurfaceView方案三:
MediaPlayer+TextureView方案四:ExoPlayer
方案五:Media3
方案六:IJKPlayer(FFmpeg)
方案七:
MediaCodec自解碼方案八:Jetpack Compose 集成方案
每個方案都將提供:
布局示例
Activity/Fragment 代碼
生命周期管理
錯誤處理與回調(diào)
最后,我們將總結(jié)各方案優(yōu)缺點,并給出不同場景的最佳實踐建議。
四、環(huán)境與依賴
// 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
// 描述: 跳轉(zhuǎn)到各個示例 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
// =======================================================
// 此處省略數(shù)百行自解碼代碼,僅做簡要示意:
// - 使用 MediaExtractor 分離軌道
// - 用 MediaCodec 解碼到 Surface
// - 用 SurfaceView / TextureView 渲染
// 建議查閱官方文檔與 Codelab 深入實現(xiàn)。
// =======================================================
// 方案八: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 變換(旋轉(zhuǎn)、縮放);
性能次于 SurfaceView;
ExoPlayer
支持 DASH、HLS、自定義加載;
擁有豐富擴展(緩存、DRM、字幕);
Media3
Jetpack 新推薦,兼容未來更新;
API 與 ExoPlayer 基本一致;
IJKPlayer
基于 FFmpeg,支持更多格式;
需部署 native 庫,包體大;
MediaCodec
最低層控制,適合自定義渲染或特殊解碼需求;
開發(fā)成本高;
Compose 集成
在 Compose 中可使用
AndroidView嵌入任意 View;未來可期待原生 Compose 視頻組件;
七、性能與優(yōu)化
硬件加速
SurfaceView與 ExoPlayer 默認硬件加速;
網(wǎng)絡緩沖
ExoPlayer 可自定義
LoadControl;
并發(fā)與切換
避免頻繁
prepare()/release();
內(nèi)存管理
及時
release()資源,避免泄漏;
UI 與渲染
避免在主線程做 heavy UI 操作;
八、項目總結(jié)與拓展
本文多角度、全方案地介紹了 Android 上幾乎所有主流的視頻播放實現(xiàn)方式,配以示例代碼與優(yōu)缺點對比,便于在不同業(yè)務場景中做出選擇。未來可擴展:
自適應碼率:HLS/DASH 動態(tài)切換
DRM:Protected clearplay
節(jié)省流量:集成緩存、預下載
UI 特效:濾鏡、彈幕、畫中畫
九、FAQ
Q1:哪種方案最簡單?
A:VideoView,但可定制性最低。
Q2:推薦使用哪個?
A:ExoPlayer/Media3,功能最全,社區(qū)活躍。
Q3:如何播放直播 HLS?
A:ExoPlayer 直接 MediaItem.fromUri("https://.../live.m3u8") 即可。
Q4:IJKPlayer 包體大怎么辦?
A:可定制 native 庫,只打包需要的 ABI。
Q5:Compose 未來會有原生視頻組件嗎?
A:已在開發(fā)中,但目前仍需 AndroidView 嵌入。
以上就是在Android上實現(xiàn)視頻播放的多種方案的詳細內(nèi)容,更多關(guān)于Android視頻播放的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android布局之GridLayout網(wǎng)格布局
網(wǎng)格布局標簽是GridLayout。這個布局是android4.0新增的布局。這個布局只有4.0之后的版本才能使用。本文給大家介紹Android布局之GridLayout網(wǎng)格布局相關(guān)知識,感興趣的朋友一起學習吧2015-12-12
Android實現(xiàn)WebView點擊攔截跳轉(zhuǎn)原生
這篇文章主要介紹了Android實現(xiàn)WebView點擊攔截跳轉(zhuǎn)原生,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android使用WindowManager構(gòu)造懸浮view
這篇文章主要為大家詳細介紹了Android使用WindowManager構(gòu)造懸浮view的具體方法,感興趣的小伙伴們可以參考一下2016-05-05
android實現(xiàn)常駐通知欄遇到的問題及解決辦法
Android添加ButterKnife時報錯Error:(2, 0) Cannot add extension wit
解決Android MediaRecorder錄制視頻過短問題

