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

