最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

從零構(gòu)建一個(gè)Android全功能視頻播放器

 更新時(shí)間:2026年05月20日 08:34:18   作者:碼流怪俠  
文章全面介紹了Android播放器的開(kāi)發(fā),從編解碼、渲染、同步、協(xié)議等多個(gè)方面進(jìn)行詳細(xì)講解,首先介紹了三種播放方案的橫向?qū)Ρ?接著深入分析了MediaCodec、Surface/SurfaceView/TextureView的核心概念,需要的朋友可以參考下

一、Android 播放器架構(gòu)全景

1.1 為什么 Android 播放器開(kāi)發(fā)值得深入

Android 平臺(tái)的視頻播放器開(kāi)發(fā),幾乎涵蓋了音視頻技術(shù)的所有核心問(wèn)題:

  • 編解碼:硬解碼(MediaCodec + OMX)、軟解碼(FFmpeg)
  • 渲染:SurfaceView、TextureView、GLSurfaceView、SurfaceTexture
  • 同步:音畫(huà)同步、緩沖管理、卡頓處理
  • 協(xié)議:HTTP(ExoPlayer)、RTSP(Live555)、RTMP(FFmpeg)
  • 系統(tǒng):OMX 服務(wù)、MediaCodec 狀態(tài)機(jī)、BufferQueue 機(jī)制

掌握這些,就掌握了整個(gè) Android 多媒體子系統(tǒng)的半壁江山。

1.2 Android 播放器的技術(shù)演進(jìn)

Android 1.0 ~ 2.3(2008-2010)
  └── VideoView(底層封裝 MediaPlayer,僅 SurfaceView)
  └── MediaPlayer(C 層實(shí)現(xiàn),不透明,擴(kuò)展性極差)
Android 3.0(2011)—— 里程碑
  └── MediaCodec API 引入(OpenMax 標(biāo)準(zhǔn)化)
  └── SurfaceTexture(TextureView 基礎(chǔ))
  └── OMX IL 1.1.2 成為標(biāo)準(zhǔn)接口
Android 4.1+(2012)—— 開(kāi)放能力
  └── MediaCodec 完全開(kāi)放(非僅 HAL)
  └── SurfaceView 雙緩沖機(jī)制完善
  └── MediaCodec + Surface 組合成為主流
Android 4.4+(2013)—— ExoPlayer 開(kāi)源
  └── Google 內(nèi)部播放器框架開(kāi)源
  └── DASH/HLS 全面支持
  └── MediaCodec 無(wú)鎖緩沖隊(duì)列
Android 7.0+(2016)—— 現(xiàn)代化
  └── MediaCodec 2.0 API(Codename:Nougat)
  └── Surface枕持化
  └──MediaMuxer 支持 HEVC
Android 10+(2019)—— 隱私與 MediaSession
  └── Scoped Storage
  └── MediaSession 標(biāo)準(zhǔn)化
Android 11+(2020)—— 現(xiàn)代框架
  └── ExoPlayer 2.x 成為 Jetpack 組件
  └── Media3 統(tǒng)一 API(MediaSession + ExoPlayer)

1.3 三種播放方案橫向?qū)Ρ?/h3>
方案架構(gòu)層次適用場(chǎng)景優(yōu)點(diǎn)缺點(diǎn)
MediaPlayer + SurfaceView系統(tǒng)級(jí)快速集成、簡(jiǎn)單播放一行代碼、硬件加速無(wú)法自定義解碼、無(wú)法縮略圖
MediaCodec + Surface應(yīng)用層(裸 API)需要自定義解碼/濾鏡完全可控、靈活代碼量大、狀態(tài)機(jī)復(fù)雜
ExoPlayer(Media3)應(yīng)用層(框架層)直播/點(diǎn)播/DRM功能完整、社區(qū)活躍框架包袱、定制需深挖源碼

入門建議:先掌握 MediaCodec 裸 API(理解原理),再過(guò)渡到 ExoPlayer(工程實(shí)用)。

1.4 Android 播放器核心組件

┌─────────────────────────────────────────────────────────────┐
│                       Application 層                         │
│  ┌────────────────────────────────────────────────────────┐ │
│  │  播放器外殼(PlayerActivity / PlayerFragment)           │ │
│  └────────────────────────────────────────────────────────┘ │
│                              │                               │
│  ┌───────────────────────────┼───────────────────────────┐ │
│  │                      控制層                             │ │
│  │  ┌──────────────┐   ┌──────────────┐   ┌──────────┐  │ │
│  │  │ ExoPlayer /  │   │  音畫(huà)同步器  │   │ 緩沖管理  │  │ │
│  │  │ MediaCodec   │   │  SyncEngine  │   │ BufferMgr│  │ │
│  │  └──────────────┘   └──────────────┘   └──────────┘  │ │
│  └───────────────────────────┬───────────────────────────┘ │
│                              │                               │
│  ┌───────────────────────────┼───────────────────────────┐ │
│  │                      解碼層                             │ │
│  │  ┌──────────────┐   ┌──────────────┐                 │ │
│  │  │  VideoDecoder│   │  AudioDecoder│                 │ │
│  │  │  (MediaCodec)│   │  (MediaCodec)│                 │ │
│  │  └──────────────┘   └──────────────┘                 │ │
│  └───────────────────────────┬───────────────────────────┘ │
│                              │                               │
│  ┌───────────────────────────┼───────────────────────────┐ │
│  │                      渲染層                             │ │
│  │  ┌──────────────┐   ┌──────────────┐   ┌──────────┐  │ │
│  │  │ SurfaceView  │   │ TextureView  │   │ GLSurface│  │ │
│  │  │ (雙緩沖)     │   │ (UI線程合成) │   │ View     │  │ │
│  │  └──────────────┘   └──────────────┘   └──────────┘  │ │
│  └───────────────────────────┬───────────────────────────┘ │
└──────────────────────────────┼───────────────────────────────┘
                               │
┌──────────────────────────────┼───────────────────────────────┐
│                    系統(tǒng)服務(wù)層(Binder IPC)                  │
│  ┌─────────────────┐  ┌─────────────────┐  ┌───────────────┐ │
│  │   OMX Master   │  │  MediaCodec    │  │ AudioFlinger  │ │
│  │  (OMX IL 核心) │  │  (Codec 服務(wù))  │  │ (音頻輸出)    │ │
│  └─────────────────┘  └─────────────────┘  └───────────────┘ │
│  ┌─────────────────┐  ┌─────────────────┐  ┌───────────────┐ │
│  │  SurfaceFlinger│  │  AudioPolicy   │  │  MediaPlayer │ │
│  │  (幀合成/顯示)  │  │  Service       │  │  Service      │ │
│  └─────────────────┘  └─────────────────┘  └───────────────┘ │
└──────────────────────────────────────────────────────────────┘

二、核心技術(shù)概念詳解

2.1 MediaCodec:編解碼器的核心 API

MediaCodec 是 Android 提供的一套訪問(wèn)硬件/軟件編解碼器的底層 API,本質(zhì)上是 生產(chǎn)者-消費(fèi)者 模式的隊(duì)列處理器。

2.1.1 架構(gòu)模型

                    ┌─────────────────────────────────────┐
                    │           MediaCodec                 │
                    │                                     │
  編碼后數(shù)據(jù) ──────→│  InputBuffer 隊(duì)列(生產(chǎn)者寫入)      │
  (AVPacket)      │                                     │
                    │  ┌───────────────────────────────┐  │
                    │  │  Codec Core(硬件/軟件)        │  │
                    │  │  ┌─────────┐  ┌─────────┐    │  │
                    │  │  │ 解碼器  │  │ 編碼器  │    │  │
                    │  │  └─────────┘  └─────────┘    │  │
                    │  └───────────────────────────────┘  │
                    │                                     │
  解碼后數(shù)據(jù) ←──────│  OutputBuffer 隊(duì)列(消費(fèi)者讀?。?    │
  (Surface/YUV)  │                                     │
                    └─────────────────────────────────────┘

2.1.2 編解碼器狀態(tài)機(jī)

                           configure()
         ┌───────────────────────────────────────────────┐
         │                                             ↓
     ┌───┴───┐                                     ┌─────┴─────┐
     │       │                                     │          │
     │ Unconfigured                               │  Configured │
     │       │                                     │          │
     └───────┘                                     └──────┬───┘
         ↑                                             │
         │             ┌────────────────────────────────┘
         │             │     start()
         │             ↓
     ┌───┴───────────────────────────────────────────┐
     │                                                │
     │  ┌─────────┐   dequeueInputBuffer()  ┌───────┴───────┐
     │  │         │────────────────────────→│               │
     │  │         │                          │  InputPending │
  queueInput │                          └───────┬───────┘
     │  │  Buffer                              │
     │  │         │                          ┌──┴──────────┐
     │  │         │ ←─── dequeueOutputBuffer │              │
     │  │  Flushed │                          │OutputPending │
     │  │         │                          └───────┬──────┘
     │  └─────────┘                                │
     │         ↑                                    │
     └─────────┼────────────────────────────────────┘
               │ flush() / endOfStream / error
         ┌─────┴─────┐
         │           │
         │  Executing │ ←─── start() 后進(jìn)入此狀態(tài)
         │           │
         └───────────┘

狀態(tài)說(shuō)明

狀態(tài)說(shuō)明可調(diào)用方法
Uninitialized初始狀態(tài)configure()
Configured已配置start() / release()
Flushed刷新?tīng)顟B(tài)(start 后立即進(jìn)入)queueInputBuffer()
Running運(yùn)行中dequeueInputBuffer() / dequeueOutputBuffer()
End of Stream流結(jié)束queueInputBuffer (EOS)
Error錯(cuò)誤狀態(tài)reset() / release()

關(guān)鍵 API

// ===== 創(chuàng)建解碼器 =====
val mediaFormat = MediaFormat.createVideoFormat(
    MediaFormat.KEY_MIME,      // "video/avc" / "video/hevc" / "audio/mp4a-latm"
    width,                      // 視頻寬
    height                      // 視頻高
)
// 可選參數(shù)
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
    MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 5_000_000)
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)  // 1s 一個(gè) I 幀
val decoder = MediaCodec.createDecoderByType("video/avc")
// ===== 配置解碼器 =====
decoder.configure(mediaFormat, surface, crypto, 0)
// 參數(shù)說(shuō)明:
//   mediaFormat:格式(寬/高/碼率/幀率/I幀間隔)
//   surface:輸出 Surface(視頻直接渲染到 Surface,跳過(guò) APP 層 YUV 拷貝)
//   crypto:DRM 加密(null = 無(wú)加密)
//   flags:0 = 解碼器,CONFIGURE_FLAG_ENCODE = 編碼器
// ===== 啟動(dòng)解碼器 =====
decoder.start()
// ===== 解碼循環(huán) =====
while (isPlaying) {
    // 1. 取輸入緩沖區(qū),寫入壓縮數(shù)據(jù)
    val inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_US)
    if (inputBufIndex >= 0) {
        val inputBuf = decoder.getInputBuffer(inputBufIndex)
        val sampleSize = extractor.readSampleData(inputBuf, 0)
        if (sampleSize < 0) {
            decoder.queueInputBuffer(inputBufIndex, 0, 0, 0,
                MediaCodec.BUFFER_FLAG_END_OF_STREAM)
        } else {
            decoder.queueInputBuffer(
                inputBufIndex, 0, sampleSize,
                extractor.sampleTime,   // PTS(微秒)
                0                        // flags
            )
            extractor.advance()
        }
    }
    // 2. 取輸出緩沖區(qū),獲取解碼后數(shù)據(jù)
    val bufferInfo = MediaCodec.BufferInfo()
    val outputBufIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
    when {
        outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
            val newFormat = decoder.outputFormat
            Log.d("Decoder", "Output format changed: $newFormat")
        }
        outputBufIndex >= 0 -> {
            // 可選:檢查是否為關(guān)鍵幀
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0) {
                Log.d("Decoder", "Key frame output")
            }
            // 渲染到 Surface(第三個(gè)參數(shù) = 延遲渲染的時(shí)間戳,微秒)
            decoder.releaseOutputBuffer(outputBufIndex, bufferInfo.presentationTimeUs)
        }
    }
}

2.1.3 MediaFormat:格式描述核心類

// ===== 常見(jiàn)視頻格式參數(shù) =====
val videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080).apply {
    setInteger(MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)  // 直接渲染模式
    setInteger(MediaFormat.KEY_BIT_RATE, 5_000_000)            // 5 Mbps
    setInteger(MediaFormat.KEY_FRAME_RATE, 30)                 // 30 fps
    setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)            // I 幀間隔 1 秒
    setLong(MediaFormat.KEY_MAX_INPUT_SIZE, 1920 * 1080 * 4)  // 最大輸入緩沖
}
// ===== 常見(jiàn)音頻格式參數(shù) =====
val audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 48000, 2).apply {
    setInteger(MediaFormat.KEY_BIT_RATE, 128_000)             // 128 kbps
    setInteger(MediaFormat.KEY_AAC_PROFILE,
        MediaCodecInfo.CodecProfileLevel.AACObjectLC)
    setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 16384)
}
// ===== 查詢可用編碼器/解碼器 =====
fun findDecoder(mime: String): MediaCodecInfo? {
    val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
    for (info in codecList.codecInfos) {
        if (info.isEncoder) continue
        for (type in info.supportedTypes) {
            if (type.equals(mime, ignoreCase = true)) {
                return info
            }
        }
    }
    return null
}

2.2 Surface:解碼輸出的渲染目標(biāo)

Surface 是 Android 圖形系統(tǒng)的核心接口,代表生產(chǎn)者-消費(fèi)者共享的原生隊(duì)列。

2.2.1 Surface 的三重身份

┌─────────────────────────────────────────────────────────┐
│                      Surface                            │
│                                                         │
│  ① 視頻解碼器輸出目標(biāo)(生產(chǎn)者)                          │
│     MediaCodec.configure(format, surface, ...)          │
│     → 解碼器直接寫入 Surface,跳過(guò) APP 層 YUV 拷貝      │
│                                                         │
│  ② 圖形消費(fèi)者(SurfaceView / TextureView)              │
│     SurfaceView:Surface 嵌入窗口,雙緩沖,獨(dú)立的 GPU 合成的 │
│     TextureView:Surface 作為紋理,UI 線程合成           │
│                                                         │
│  ③ ANativeWindow(Native 層)                          │
│     可直接對(duì)接 Camera NDK / FFmpeg libavcodec           │
└─────────────────────────────────────────────────────────┘

2.2.2 SurfaceView vs TextureView

特性SurfaceViewTextureView
渲染位置SurfaceFlinger(獨(dú)立合成)UI 線程(View 樹(shù)合成)
雙緩沖? 硬件雙緩沖(翻頁(yè)式)? 單緩沖(可能導(dǎo)致撕裂)
性能? 高(獨(dú)立層,無(wú) UI 線程開(kāi)銷)?? 中(UI 線程參與合成)
截屏?? 需特殊處理? 直接截圖
動(dòng)畫(huà)/變換? 不支持 View 變換? 支持旋轉(zhuǎn)/縮放/alpha
Z-order可置于任意位置服從 View 層級(jí)
延遲最低(直接寫入幀緩沖區(qū))略高(UI 合成延遲)
適用場(chǎng)景直播播放器、Camera 預(yù)覽視頻編輯、特效疊加

SurfaceView 使用示例

// XML 布局
<SurfaceView
    android:id="@+id/surfaceView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
// 代碼中獲取 Surface
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback2 {
    private lateinit var surfaceView: SurfaceView
    private var surface: Surface? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        surfaceView = findViewById(R.id.surfaceView)
        surfaceView.holder.addCallback(this)
        // 可選:設(shè)置 Surface 的像素格式和緩沖數(shù)量
        surfaceView.holder.setFormat(PixelFormat.RGBA_8888)
    }
    override fun surfaceCreated(holder: SurfaceHolder) {
        // Surface 創(chuàng)建后,立即配置解碼器
        surface = holder.surface
        setupDecoder(surface!!)
    }
    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                 width: Int, height: Int) {
        Log.d("Player", "Surface changed: ${width}x$height")
    }
    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // 釋放解碼器
        releaseDecoder()
        surface = null
    }
}

TextureView 使用示例

// XML
<TextureView
    android:id="@+id/textureView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
// 代碼
class TexturePlayerActivity : AppCompatActivity(), TextureView.SurfaceTextureListener {
    private lateinit var textureView: TextureView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        textureView = findViewById(R.id.textureView)
        textureView.surfaceTextureListener = this
    }
    override fun onSurfaceTextureAvailable(surface: SurfaceTexture,
                                            width: Int, height: Int) {
        // SurfaceTexture 可用后創(chuàng)建 Surface
        val surface = Surface(surface)
        setupDecoder(surface)
    }
    override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
        // 每幀更新時(shí)調(diào)用(UI 線程)
    }
    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
        releaseDecoder()
        surface.release()
        return true
    }
}

2.3 MediaExtractor:流解復(fù)用

MediaExtractor 負(fù)責(zé)從容器文件(MP4/MKV/TS 等)中提取軌道(Track),包括視頻軌、音頻軌、字幕軌。

// ===== 創(chuàng)建 Extractor 并設(shè)置數(shù)據(jù)源 =====
val extractor = MediaExtractor()
extractor.setDataSource(filePath)  // 或 URL、FileDescriptor
// ===== 枚舉所有軌道 =====
for (i in 0 until extractor.trackCount) {
    val format = extractor.getTrackFormat(i)
    val mime = format.getString(MediaFormat.KEY_MIME) ?: continue
    if (mime.startsWith("video/")) {
        Log.d("Extractor", "Video track: $i, $format")
        extractor.selectTrack(i)  // 選擇視頻軌
    } else if (mime.startsWith("audio/")) {
        Log.d("Extractor", "Audio track: $i, $format")
    }
}
// ===== 獲取當(dāng)前選中軌道的格式 =====
val videoFormat = extractor.getTrackFormat(selectedTrack)
val width = videoFormat.getInteger(MediaFormat.KEY_WIDTH)
val height = videoFormat.getInteger(MediaFormat.KEY_HEIGHT)
val duration = videoFormat.getLong(MediaFormat.KEY_DURATION)  // 微秒
val frameRate = videoFormat.getIntegerOrDefault(MediaFormat.KEY_FRAME_RATE, 30)
val colorFormat = videoFormat.getIntegerOrDefault(MediaFormat.KEY_COLOR_FORMAT, 0)
// ===== 讀取壓縮數(shù)據(jù) =====
val buffer = ByteBuffer.allocate(1024 * 1024)  // 1MB 緩沖
val sampleSize = extractor.readSampleData(buffer, 0)
if (sampleSize < 0) {
    // 流結(jié)束
}
val sampleTime = extractor.sampleTime      // 當(dāng)前幀 PTS(微秒)
val sampleFlags = extractor.sampleFlags     // BUFFER_FLAG_*
// ===== 跳到指定時(shí)間 =====
extractor.seekTo(positionUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
// SEEK_TO_CLOSEST_SYNC:跳到最近的同步點(diǎn)(I 幀)
// SEEK_TO_PREVIOUS_SYNC:跳到上一個(gè) I 幀
// SEEK_TO_NEXT_SYNC:跳到下一個(gè) I 幀
// ===== 釋放 =====
extractor.release()

2.4 音畫(huà)同步:PTS 驅(qū)動(dòng)的顯示控制

Android 播放器的音畫(huà)同步,是在解碼器輸出幀后,通過(guò)比較 幀的 PTS當(dāng)前音視頻時(shí)鐘 的差值來(lái)決定立即顯示、延遲等待還是丟棄

2.4.1 同步策略分類

策略主時(shí)鐘原理優(yōu)點(diǎn)缺點(diǎn)
視頻同步到音頻音頻音頻精確,音頻播放,視頻等音頻音頻不卡頓視頻可能掉幀
音頻同步到視頻視頻視頻固定幀率,音頻調(diào)整采樣率視頻流暢音頻可能有 Pop
同步到系統(tǒng)時(shí)鐘NTP以系統(tǒng)時(shí)間為基準(zhǔn)外部一致性好漂移難控制

行業(yè)最佳實(shí)踐:視頻同步到音頻(音頻為主時(shí)鐘),因?yàn)橐纛l采樣率由硬件晶振控制,精度遠(yuǎn)高于視頻幀率。

2.4.2 同步算法實(shí)現(xiàn)

class SyncController(private val masterClock: MasterClock) {
    // 同步閾值
    companion object {
        private const val MAX_FRAME_DROP_MS = 100L   // 超過(guò) 100ms 即丟幀
        private const val SYNC_THRESHOLD_MS = 50L    // 50ms 內(nèi)無(wú)需同步
        private const val SLEEP_GRANULARITY_MS = 10L // 睡眠粒度
    }
    /**
     * @param framePtsUs  幀的 PTS(微秒)
     * @param currentTimeUs 當(dāng)前主時(shí)鐘時(shí)間(微秒)
     * @return SyncResult:立即顯示 / 等待后顯示 / 丟棄
     */
    fun getSyncResult(framePtsUs: Long, currentTimeUs: Long): SyncResult {
        val diffMs = (framePtsUs - currentTimeUs) / 1000
        return when {
            // 視頻超前:需要等待
            diffMs > SYNC_THRESHOLD_MS -> {
                SyncResult.WaitAndDisplay(diffMs)
            }
            // 視頻落后超過(guò)閾值:丟棄
            diffMs < -MAX_FRAME_DROP_MS -> {
                Log.w("Sync", "Frame late by ${-diffMs}ms, dropping")
                SyncResult.Drop
            }
            // 在可接受范圍內(nèi):立即顯示
            else -> SyncResult.DisplayNow
        }
    }
    sealed class SyncResult {
        object DisplayNow : SyncResult()
        data class WaitAndDisplay(val waitMs: Long) : SyncResult()
        object Drop : SyncResult()
    }
}
/**
 * 主時(shí)鐘(音頻作為主時(shí)鐘)
 */
class AudioMasterClock(private val audioTrack: AudioTrack) : MasterClock {
    override fun getCurrentTimeUs(): Long {
        // AudioTrack.getPlaybackHeadPosition() 返回已播放采樣數(shù)
        val headPos = audioTrack.playbackHeadPosition
        // 將采樣位置轉(zhuǎn)換為微秒
        val sampleRate = audioTrack.sampleRate
        return headPos * 1_000_000L / sampleRate
    }
}

2.5 緩沖管理:避免卡頓的核心

2.5.1 三級(jí)緩沖架構(gòu)

┌─────────────────────────────────────────────────────────────┐
│                        網(wǎng)絡(luò)/HTTP 緩沖                        │
│                   OkHttp / URLConnection                    │
│                   典型緩沖:2–10 MB                          │
└─────────────────────────────┬───────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      解碼器輸入緩沖                          │
│                    MediaCodec Input Buffer                   │
│                   典型緩沖:3–5 幀                           │
│                   dequeueInputBuffer() → queueInputBuffer() │
└─────────────────────────────┬───────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      解碼器輸出緩沖                          │
│                   MediaCodec Output Buffer                   │
│                   典型緩沖:5–10 幀                         │
│              dequeueOutputBuffer() → releaseOutputBuffer()  │
└─────────────────────────────┬───────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                        渲染緩沖                              │
│              Surface/TextureView 幀緩沖                      │
│                   典型緩沖:2–3 幀                           │
│                   SurfaceView 雙緩沖(自動(dòng))                  │
└─────────────────────────────────────────────────────────────┘

2.5.2 緩沖監(jiān)控與自適應(yīng)

class BufferMonitor(private val decoder: MediaCodec) {
    private val bufferHistory = ArrayDeque<Long>(100)
    // 監(jiān)控解碼器緩沖隊(duì)列深度
    fun checkBufferHealth(): BufferHealth {
        val inputCount = bufferHistory.filter { it > 0 }.size
        val outputCount = bufferHistory.filter { it < 0 }.size
        return when {
            inputCount > 3 && outputCount < 1 -> BufferHealth.UNDERFLOW
            inputCount < 1 && outputCount > 5 -> BufferHealth.OVERFLOW
            else -> BufferHealth.NORMAL
        }
    }
    enum class BufferHealth {
        NORMAL,
        UNDERFLOW,  // 解碼器"餓"了(輸入不足)
        OVERFLOW    // 解碼器"撐"了(輸出積壓)
    }
    // 動(dòng)態(tài)調(diào)整:緩沖不足時(shí)降低解碼幀率,積壓時(shí)加速
    fun adaptiveDecodeSpeed(health: BufferHealth): Float {
        return when (health) {
            BufferHealth.UNDERFLOW -> 1.2f   // 加速解碼
            BufferHealth.OVERFLOW -> 0.8f  // 減速解碼
            BufferHealth.NORMAL -> 1.0f
        }
    }
}

三、ExoPlayer(Media3):工程級(jí)播放器框架

3.1 ExoPlayer vs MediaCodec:何時(shí)選擇

小場(chǎng)景 → MediaPlayer + SurfaceView(一行代碼)

需要自定義解碼流程 → MediaCodec 裸 API

需要:直播(HLS/DASH/RTMP)、多音軌、DRM、縮略圖、
     預(yù)加載、播放器狀態(tài)機(jī) → ExoPlayer (Media3)

3.2 ExoPlayer(Media3)架構(gòu)

┌─────────────────────────────────────────────────────────────┐
│                    Media3 ExoPlayer                         │
│                                                             │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                  Player UI(PlayerView)               │ │
│  └────────────────────────────────────────────────────────┘ │
│                              │                               │
│  ┌───────────────────────────┼───────────────────────────┐ │
│  │               ExoPlayer 核心                          │ │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐  │ │
│  │  │ MediaSource  │  │ TrackSelector │  │LoadControl  │  │ │
│  │  │ (數(shù)據(jù)源管理) │  │ (軌道選擇)    │  │(緩沖控制)   │  │ │
│  │  └──────────────┘  └──────────────┘  └─────────────┘  │ │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐  │ │
│  │  │ Renderers    │  │ MediaCodec   │  │ Clock/Sync  │  │ │
│  │  │ (渲染器)     │  │ (解碼器)      │  │(時(shí)鐘同步)   │  │ │
│  │  └──────────────┘  └──────────────┘  └─────────────┘  │ │
│  └───────────────────────────────────────────────────────┘ │
│                              │                               │
│  ┌───────────────────────────┼───────────────────────────┐ │
│  │              數(shù)據(jù)源層(MediaSource)                     │ │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│  │  │Progressive│ │ HlsMediaSource│ │DashMediaSource│ │RtspMediaSource│ │
│  │  │(普通文件)│ │(HTTP直播流) │ │(自適應(yīng)流)   │ │(實(shí)時(shí)流)  │ │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

3.3 ExoPlayer 快速上手

3.3.1 依賴引入(Media3 / ExoPlayer 2.19+)

// build.gradle.kts (Module)
dependencies {
    // Media3 ExoPlayer(推薦)
    implementation("androidx.media3:media3-exoplayer:1.2.1")
    implementation("androidx.media3:media3-ui:1.2.1")          // 播放器控件
    implementation("androidx.media3:media3-exoplayer-hls:1.2.1") // HLS 支持
    implementation("androidx.media3:media3-exoplayer-dash:1.2.1") // DASH 支持
    implementation("androidx.media3:media3-exoplayer-rtsp:1.2.1") // RTSP 支持
    implementation("androidx.media3:media3-session:1.2.1")      // MediaSession
}

3.3.2 基礎(chǔ)播放(10 行代碼)

class MainActivity : AppCompatActivity() {
    private var player: ExoPlayer? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // ===== 1. 創(chuàng)建 Player =====
        player = ExoPlayer.Builder(this).build().also { exoPlayer ->
            // ===== 2. 綁定視圖 =====
            findViewById<PlayerView>(R.id.playerView).also { pv ->
                pv.player = exoPlayer
                pv.useController = true  // 顯示播放控制條
            }
            // ===== 3. 創(chuàng)建 MediaItem(數(shù)據(jù)源)=====
            val mediaItem = MediaItem.Builder()
                .setUri("https://www.w3schools.com/html/mov_bbb.mp4")
                // 或本地文件:Uri.parse("file:///sdcard/video.mp4")
                .setMimeType("video/mp4")
                .build()
            // ===== 4. 設(shè)置媒體并播放 =====
            exoPlayer.setMediaItem(mediaItem)
            exoPlayer.prepare()
            exoPlayer.playWhenReady = true  // 準(zhǔn)備好后自動(dòng)播放
        }
    }
    override fun onStart() {
        super.onStart()
        player?.playWhenReady = player?.playWhenReady ?: true
    }
    override fun onStop() {
        super.onStop()
        player?.playWhenReady = false  // 切后臺(tái)暫停
    }
    override fun onDestroy() {
        super.onDestroy()
        player?.release()  // 務(wù)必釋放!
        player = null
    }
}

3.3.3 XML 布局(PlayerView)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.media3.ui.PlayerView
        android:id="@+id/playerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:show_buffering="when_playing"      <!-- 播放時(shí)顯示緩沖 -->
        app:use_controller="true"             <!-- 顯示控制條 -->
        app:controller_layout_id="@layout/exo_player_control_view" <!-- 自定義控制條 -->
        app:resize_mode="fit"                 <!-- 自適應(yīng) -->
        app:surface_type="surface_view"       <!-- SurfaceView(性能優(yōu)先)-->
    />
</androidx.constraintlayout.widget.ConstraintLayout>

3.4 ExoPlayer 進(jìn)階配置

3.4.1 自定義 LoadControl(緩沖控制)

val loadControl = DefaultLoadControl.Builder()
    .setAllocator(DefaultAllocator(true, 32))
    .setBufferDurationsMs(
        /* minBufferMs     = */ 15_000,   // 最小緩沖 15 秒
        /* maxBufferMs     = */ 50_000,   // 最大緩沖 50 秒
        /* bufferForPlaybackMs       = */  2_500,   // 開(kāi)始播放需緩沖 2.5 秒
        /* bufferForPlaybackAfterRebufferMs = */ 5_000  // 重緩沖需緩沖 5 秒
    )
    .setTargetBufferBytes(25 * 1024 * 1024)  // 目標(biāo)緩沖 25 MB
    .build()
val player = ExoPlayer.Builder(context)
    .setLoadControl(loadControl)
    .build()

3.4.2 軌道選擇(多音軌/多字幕)

// 監(jiān)聽(tīng)軌道變化
player.addListener(object : Player.Listener {
    override fun onTracksChanged(tracks: Tracks) {
        for (trackGroup in tracks.trackGroups) {
            for (i in 0 until trackGroup.length) {
                val format = trackGroup.getFormat(i)
                Log.d("Track", "軌道 ${trackGroup.type}: ${format.sampleMimeType} "
                        + "${format.width}x${format.height} @ ${format.bitrate}")
            }
        }
    }
})
// 手動(dòng)選擇軌道
val trackSelector = DefaultTrackSelector(context)
trackSelector.setParameters(
    trackSelector.buildUponParameters()
        .setPreferredTextLanguage("zh")        // 首選中文字幕
        .setPreferredAudioLanguage("zh-CN")     // 首選中文音頻
        .setMaxVideoSizeSd()                   // 最大 DVD 畫(huà)質(zhì)(省流量)
)

3.4.3 HLS 直播流播放

// HLS 直播流(自動(dòng)處理 #EXTINF 和分片下載)
val hlsMediaItem = MediaItem.Builder()
    .setUri("https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8")
    .setMimeType(MimeTypes.APPLICATION_M3U8)
    .build()
player.setMediaItem(hlsMediaItem)
player.prepare()
// DASH 直播流
val dashMediaItem = MediaItem.Builder()
    .setUri("https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd")
    .setMimeType(MimeTypes.APPLICATION_DASH)
    .build()

3.4.4 播放事件監(jiān)聽(tīng)

player.addListener(object : Player.Listener {
    override fun onPlaybackStateChanged(state: Int) {
        when (state) {
            Player.STATE_IDLE       -> Log.d("Player", "空閑")
            Player.STATE_BUFFERING  -> Log.d("Player", "緩沖中...")
            Player.STATE_READY      -> Log.d("Player", "就緒")
            Player.STATE_ENDED      -> Log.d("Player", "播放結(jié)束")
        }
    }
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        Log.d("Player", "播放狀態(tài): ${if (isPlaying) "播放中" else "暫停"}")
    }
    override fun onPlayerError(error: PlaybackException) {
        Log.e("Player", "播放錯(cuò)誤: ${error.errorCode}")
        when (error.errorCode) {
            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ->
                showRetryDialog("網(wǎng)絡(luò)錯(cuò)誤")
            PlaybackException.ERROR_CODE_DECODER_INIT_FAILED ->
                showRetryDialog("解碼器初始化失敗")
        }
    }
    override fun onPositionDiscontinuity(
        oldPosition: Player.PositionInfo,
        newPosition: Player.PositionInfo,
        reason: Int
    ) {
        Log.d("Player", "位置不連續(xù): ${oldPosition.positionMs} → ${newPosition.positionMs}")
    }
})

3.5 ExoPlayer 完整 Demo 項(xiàng)目結(jié)構(gòu)

app/
├── build.gradle.kts
└── src/main/
    ├── AndroidManifest.xml
    ├── java/com/example/androidplayer/
    │   ├── MainActivity.kt              # 主播放器界面
    │   ├── player/
    │   │   ├── PlayerManager.kt          # 播放器管理器(單例)
    │   │   ├── CustomPlayerActivity.kt   # 自定義播放器(不用 PlayerView)
    │   │   ├── HlsPlayerActivity.kt      # HLS 直播播放器
    │   │   └── PlayerService.kt          # 后臺(tái)播放服務(wù)(MediaSession)
    │   ├── decoder/
    │   │   ├── MediaCodecDecoder.kt      # 裸 MediaCodec 解碼示例
    │   │   └── SurfaceRenderer.kt        # Surface 渲染輔助
    │   └── utils/
    │       └── BufferMonitor.kt         # 緩沖監(jiān)控工具
    └── res/
        ├── layout/
        │   ├── activity_main.xml        # 基礎(chǔ) ExoPlayer 布局
        │   ├── activity_custom_player.xml  # 自定義布局
        │   └── activity_hls_player.xml   # HLS 直播布局
        └── values/strings.xml

四、裸 MediaCodec 解碼器:手把手實(shí)現(xiàn)

4.1 完整的 MediaCodec 解碼流程

以下代碼展示如何使用 MediaCodec 裸 API 解碼 MP4 文件并渲染到 Surface,不依賴 ExoPlayer:

/**
 * MediaCodecVideoDecoder:裸 MediaCodec 解碼器
 * 支持:H.264 / H.265 / VP8 / VP9
 */
class MediaCodecVideoDecoder(
    private val context: Context,
    private val surface: Surface
) {
    private var decoder: MediaCodec? = null
    private var extractor: MediaExtractor? = null
    private var videoTrackIndex = -1
    private var isRunning = false
    private var thread: Thread? = null
    // 幀率控制
    private var frameIntervalUs = 33_333L  // 30fps ≈ 33333us 每幀
    /**
     * 解碼主循環(huán)
     */
    fun start(filePath: String) {
        if (isRunning) return
        isRunning = true
        thread = Thread {
            try {
                // ===== 步驟 1:解復(fù)用 =====
                val ext = MediaExtractor()
                ext.setDataSource(filePath)
                extractor = ext
                // ===== 步驟 2:找到視頻軌道 =====
                for (i in 0 until ext.trackCount) {
                    val format = ext.getTrackFormat(i)
                    val mime = format.getString(MediaFormat.KEY_MIME) ?: continue
                    if (mime.startsWith("video/")) {
                        ext.selectTrack(i)
                        videoTrackIndex = i
                        // ===== 步驟 3:創(chuàng)建解碼器 =====
                        val mimeType = mime
                        decoder = MediaCodec.createDecoderByType(mimeType)
                        // ===== 步驟 4:配置解碼器(關(guān)鍵?。?====
                        // configure 的第三個(gè)參數(shù) surface:將解碼輸出直接送入 Surface
                        decoder!!.configure(format, surface, null, 0)
                        decoder!!.start()
                        break
                    }
                }
                if (videoTrackIndex < 0 || decoder == null) {
                    throw IllegalStateException("未找到視頻軌道或無(wú)法創(chuàng)建解碼器")
                }
                // ===== 步驟 5:解碼循環(huán) =====
                decodeLoop(ext, decoder!!)
            } catch (e: Exception) {
                Log.e("Decoder", "解碼錯(cuò)誤", e)
            } finally {
                release()
            }
        }
        thread?.start()
    }
    private fun decodeLoop(ext: MediaExtractor, dec: MediaCodec) {
        val bufferInfo = MediaCodec.BufferInfo()
        var inputDone = false
        var outputDone = false
        while (!outputDone && isRunning) {
            // ===== A. 喂入壓縮數(shù)據(jù)(生產(chǎn)者端)=====
            if (!inputDone) {
                val inputBufIndex = dec.dequeueInputBuffer(10_000)  // 10ms 超時(shí)
                when {
                    inputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
                        // 無(wú)可用輸入緩沖,稍后再試
                    }
                    inputBufIndex >= 0 -> {
                        val inputBuf = dec.getInputBuffer(inputBufIndex)!!
                        val sampleSize = ext.readSampleData(inputBuf, 0)
                        if (sampleSize < 0) {
                            // 流結(jié)束,發(fā)送 EOS
                            dec.queueInputBuffer(
                                inputBufIndex, 0, 0, 0,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM
                            )
                            inputDone = true
                            Log.d("Decoder", "Input EOS sent")
                        } else {
                            // 寫入壓縮數(shù)據(jù)及 PTS
                            dec.queueInputBuffer(
                                inputBufIndex, 0, sampleSize,
                                ext.sampleTime,  // PTS(微秒)
                                0
                            )
                            ext.advance()
                        }
                    }
                }
            }
            // ===== B. 取出解碼數(shù)據(jù)(消費(fèi)者端)=====
            val outputBufIndex = dec.dequeueOutputBuffer(bufferInfo, 10_000)
            when {
                outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
                    // 無(wú)可用輸出
                }
                outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                    val newFormat = dec.outputFormat
                    Log.d("Decoder", "輸出格式: $newFormat")
                    // 可在此獲取輸出寬高
                    val outWidth = newFormat.getInteger(MediaFormat.KEY_WIDTH)
                    val outHeight = newFormat.getInteger(MediaFormat.KEY_HEIGHT)
                    Log.d("Decoder", "輸出分辨率: ${outWidth}x${outHeight}")
                }
                outputBufIndex >= 0 -> {
                    val render = (bufferInfo.size != 0)
                    if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                        outputDone = true
                        Log.d("Decoder", "Output EOS received")
                    }
                    // ===== 步驟 6:渲染到 Surface =====
                    // 第二個(gè)參數(shù) = true → 按 PTS 定時(shí)渲染(音畫(huà)同步核心)
                    //                   false → 立即渲染
                    dec.releaseOutputBuffer(outputBufIndex, render)
                    if (render) {
                        // 記錄幀信息
                        val frameMs = bufferInfo.presentationTimeUs / 1000
                        // Log.v("Decoder", "Frame @ ${frameMs}ms")
                    }
                }
            }
        }
    }
    fun stop() {
        isRunning = false
        thread?.join(2000)
    }
    fun release() {
        isRunning = false
        try {
            decoder?.stop()
            decoder?.release()
            decoder = null
            extractor?.release()
            extractor = null
        } catch (e: Exception) {
            Log.e("Decoder", "釋放錯(cuò)誤", e)
        }
    }
}

4.2 帶音畫(huà)同步的完整播放器

/**
 * SimpleMediaPlayer:帶音畫(huà)同步的完整播放器
 * 使用 MediaCodec 裸 API 實(shí)現(xiàn)
 */
class SimpleMediaPlayer(
    private val context: Context,
    private val surfaceView: SurfaceView
) {
    private var videoDecoder: MediaCodec? = null
    private var audioTrack: AudioTrack? = null
    private var extractor: MediaExtractor? = null
    private var videoThread: Thread? = null
    private var audioThread: Thread? = null
    private var isRunning = AtomicBoolean(false)
    private val videoStartTimeUs = AtomicLong(0)
    // 視頻緩沖隊(duì)列
    private val videoFrameQueue = LinkedBlockingQueue<Frame>(5)
    // 音頻采樣隊(duì)列
    private val audioSampleQueue = LinkedBlockingQueue<AudioSample>(50)
    data class Frame(val data: ByteBuffer, val ptsUs: Long, val size: Int)
    data class AudioSample(val data: ByteBuffer, val ptsUs: Long, val size: Int)
    fun play(filePath: String) {
        if (isRunning.get()) return
        isRunning.set(true)
        // 初始化 Extractor
        val ext = MediaExtractor()
        ext.setDataSource(filePath)
        extractor = ext
        // 分別啟動(dòng)音視頻解碼線程
        videoThread = Thread { decodeVideo(ext, surfaceView.holder.surface) }
        audioThread = Thread { playAudio(ext) }
        videoThread?.start()
        audioThread?.start()
    }
    private fun decodeVideo(ext: MediaExtractor, surface: Surface) {
        // 找到視頻軌道
        var trackIndex = -1
        var format: MediaFormat? = null
        for (i in 0 until ext.trackCount) {
            val f = ext.getTrackFormat(i)
            val mime = f.getString(MediaFormat.KEY_MIME) ?: continue
            if (mime.startsWith("video/")) {
                ext.selectTrack(i)
                trackIndex = i
                format = f
                break
            }
        }
        if (trackIndex < 0 || format == null) return
        // 創(chuàng)建解碼器
        val mime = format.getString(MediaFormat.KEY_MIME)!!
        videoDecoder = MediaCodec.createDecoderByType(mime)
        videoDecoder!!.configure(format, surface, null, 0)
        videoDecoder!!.start()
        val bufferInfo = MediaCodec.BufferInfo()
        var inputDone = false
        while (isRunning.get()) {
            // 喂入數(shù)據(jù)
            if (!inputDone) {
                val bufIdx = videoDecoder!!.dequeueInputBuffer(10_000)
                if (bufIdx >= 0) {
                    val buf = videoDecoder!!.getInputBuffer(bufIdx)!!
                    val size = ext.readSampleData(buf, 0)
                    if (size < 0) {
                        videoDecoder!!.queueInputBuffer(bufIdx, 0, 0, 0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                        inputDone = true
                    } else {
                        videoDecoder!!.queueInputBuffer(bufIdx, 0, size,
                            ext.sampleTime, 0)
                        ext.advance()
                    }
                }
            }
            // 取出渲染
            val outIdx = videoDecoder!!.dequeueOutputBuffer(bufferInfo, 10_000)
            if (outIdx >= 0) {
                // 設(shè)置 PTS 參考時(shí)間(第一幀時(shí)記錄)
                if (videoStartTimeUs.get() == 0L && bufferInfo.size > 0) {
                    videoStartTimeUs.set(System.nanoTime() / 1000 - bufferInfo.presentationTimeUs)
                }
                val renderTimeUs = videoStartTimeUs.get() + bufferInfo.presentationTimeUs
                val currentTimeUs = System.nanoTime() / 1000
                val sleepUs = renderTimeUs - currentTimeUs
                if (sleepUs > 0) {
                    // 音畫(huà)同步:視頻等待音頻
                    Thread.sleep(sleepUs / 1000, (sleepUs % 1000).toInt() * 1000)
                }
                val doRender = (bufferInfo.size != 0)
                videoDecoder!!.releaseOutputBuffer(outIdx, doRender)
            }
        }
    }
    private fun playAudio(ext: MediaExtractor) {
        // 找到音頻軌道
        var trackIndex = -1
        var format: MediaFormat? = null
        for (i in 0 until ext.trackCount) {
            val f = ext.getTrackFormat(i)
            val mime = f.getString(MediaFormat.KEY_MIME) ?: continue
            if (mime.startsWith("audio/")) {
                ext.selectTrack(i)
                trackIndex = i
                format = f
                break
            }
        }
        if (trackIndex < 0 || format == null) return
        val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
        val channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
        // 創(chuàng)建 AudioTrack
        val channelMask = if (channelCount == 1) AudioFormat.CHANNEL_OUT_MONO
                          else AudioFormat.CHANNEL_OUT_STEREO
        val bufSize = AudioTrack.getMinBufferSize(sampleRate, channelMask,
                                                   AudioFormat.ENCODING_PCM_16BIT)
        audioTrack = AudioTrack.Builder()
            .setAudioFormat(AudioFormat.Builder()
                .setSampleRate(sampleRate)
                .setChannelMask(channelMask)
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .build())
            .setBufferSizeInBytes(bufSize * 2)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .build()
        audioTrack!!.play()
        // 音頻解碼(使用 MediaCodec)
        val mime = format.getString(MediaFormat.KEY_MIME)!!
        val audioDecoder = MediaCodec.createDecoderByType(mime)
        audioDecoder.configure(format, null, null, 0)
        audioDecoder.start()
        val bufferInfo = MediaCodec.BufferInfo()
        val audioBuf = ByteArray(4096)
        var inputDone = false
        while (isRunning.get()) {
            if (!inputDone) {
                val bufIdx = audioDecoder.dequeueInputBuffer(10_000)
                if (bufIdx >= 0) {
                    val buf = audioDecoder.getInputBuffer(bufIdx)!!
                    val size = ext.readSampleData(buf, 0)
                    if (size < 0) {
                        audioDecoder.queueInputBuffer(bufIdx, 0, 0, 0,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                        inputDone = true
                    } else {
                        audioDecoder.queueInputBuffer(bufIdx, 0, size,
                            ext.sampleTime, 0)
                        ext.advance()
                    }
                }
            }
            val outIdx = audioDecoder.dequeueOutputBuffer(bufferInfo, 10_000)
            if (outIdx >= 0) {
                val buf = audioDecoder.getOutputBuffer(outIdx)!!
                buf.get(audioBuf, 0, bufferInfo.size)
                audioTrack?.write(audioBuf, 0, bufferInfo.size)
                audioDecoder.releaseOutputBuffer(outIdx, false)
            }
        }
        audioDecoder.stop()
        audioDecoder.release()
        audioTrack?.stop()
        audioTrack?.release()
    }
    fun stop() {
        isRunning.set(false)
        videoThread?.join(2000)
        audioThread?.join(2000)
    }
}

五、實(shí)戰(zhàn):完整可運(yùn)行的 Demo 項(xiàng)目

5.1 項(xiàng)目配置

文件:app/build.gradle.kts(完整依賴)

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}
android {
    namespace = "com.example.androidplayer"
    compileSdk = 34
    defaultConfig {
        applicationId = "com.example.androidplayer"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    kotlinOptions {
        jvmTarget = "17"
    }
}
dependencies {
    // AndroidX Core
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    // Media3 / ExoPlayer(推薦使用 Media3)
    implementation("androidx.media3:media3-exoplayer:1.2.1")
    implementation("androidx.media3:media3-ui:1.2.1")
    implementation("androidx.media3:media3-exoplayer-hls:1.2.1")
    implementation("androidx.media3:media3-exoplayer-dash:1.2.1")
    implementation("androidx.media3:media3-exoplayer-rtsp:1.2.1")
    implementation("androidx.media3:media3-session:1.2.1")
    // Lifecycle
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    // Testing
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

5.2 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <!-- 網(wǎng)絡(luò)權(quán)限(播放 HTTP 流媒體)-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- 存儲(chǔ)權(quán)限(播放本地文件)-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <!-- SurfaceView 硬件加速(默認(rèn)開(kāi)啟)-->
    <uses-feature
        android:name="android.hardware.surface.virtual"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.microphone"
        android:required="false" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="AndroidPlayer"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AndroidPlayer"
        android:hardwareAccelerated="true"
        tools:targetApi="34">
        <!-- 主播放器 Activity -->
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:exported="true"
            android:screenOrientation="landscape"
            android:theme="@style/Theme.AndroidPlayer.Fullscreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- 自定義播放器(裸 MediaCodec) -->
        <activity
            android:name=".player.CustomPlayerActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:screenOrientation="landscape" />
        <!-- HLS 直播播放器 -->
        <activity
            android:name=".player.HlsPlayerActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:screenOrientation="landscape" />
    </application>
</manifest>

5.3 MainActivity:ExoPlayer 基礎(chǔ)播放

package com.example.androidplayer
import android.Manifest
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.ui.PlayerView
import com.example.androidplayer.databinding.ActivityMainBinding
/**
 * MainActivity:ExoPlayer 基礎(chǔ)播放器
 * 展示最簡(jiǎn)單的 ExoPlayer 集成方式
 */
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private var player: ExoPlayer? = null
    // ===== 示例媒體 URL =====
    private val sampleVideos = listOf(
        // Sintel 開(kāi)放電影(MP4)
        MediaItem.Builder()
            .setUri("https://download.sintel.org/sintel.mp4")
            .setMimeType("video/mp4")
            .build(),
        // Big Buck Bunny
        MediaItem.Builder()
            .setUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
            .setMimeType("video/mp4")
            .build(),
        // Tears of Steel
        MediaItem.Builder()
            .setUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4")
            .setMimeType("video/mp4")
            .build(),
    )
    private var currentVideoIndex = 0
    // 文件選擇器
    private val pickFileLauncher = registerForActivityResult(
        ActivityResultContracts.GetContent()
    ) { uri: Uri? ->
        uri?.let { playUri(it) }
    }
    // 權(quán)限請(qǐng)求
    private val permissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) {
            openFilePicker()
        } else {
            Toast.makeText(this, "需要存儲(chǔ)權(quán)限以打開(kāi)本地文件", Toast.LENGTH_SHORT).show()
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setupClickListeners()
    }
    private fun setupClickListeners() {
        // 點(diǎn)擊屏幕:選擇視頻
        binding.playerView.setOnClickListener {
            showVideoSelector()
        }
        // 雙擊:切換下一個(gè)視頻
        binding.playerView.setOnClickListener { /* prevent double-listener */ }
    }
    private fun showVideoSelector() {
        val titles = sampleVideos.mapIndexed { index, item ->
            when (index) {
                0 -> "Sintel (開(kāi)放電影)"
                1 -> "Big Buck Bunny"
                2 -> "Tears of Steel"
                else -> "視頻 ${index + 1}"
            }
        }.toMutableList().apply { add("打開(kāi)本地文件...") }
        android.app.AlertDialog.Builder(this)
            .setTitle("選擇視頻")
            .setItems(titles.toTypedArray()) { _, which ->
                if (which < sampleVideos.size) {
                    playMediaItem(sampleVideos[which])
                } else {
                    requestStoragePermission()
                }
            }
            .show()
    }
    private fun requestStoragePermission() {
        when {
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
                // Android 13+ 使用 READ_MEDIA_VIDEO
                if (ContextCompat.checkSelfPermission(this,
                        Manifest.permission.READ_MEDIA_VIDEO) == PackageManager.PERMISSION_GRANTED) {
                    openFilePicker()
                } else {
                    permissionLauncher.launch(Manifest.permission.READ_MEDIA_VIDEO)
                }
            }
            else -> {
                if (ContextCompat.checkSelfPermission(this,
                        Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
                    openFilePicker()
                } else {
                    permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
                }
            }
        }
    }
    private fun openFilePicker() {
        pickFileLauncher.launch("video/*")
    }
    private fun playMediaItem(mediaItem: MediaItem) {
        ensurePlayerReady()
        player?.setMediaItem(mediaItem)
        player?.prepare()
        player?.playWhenReady = true
    }
    private fun playUri(uri: Uri) {
        ensurePlayerReady()
        val item = MediaItem.Builder()
            .setUri(uri)
            .build()
        player?.setMediaItem(item)
        player?.prepare()
        player?.playWhenReady = true
    }
    private fun ensurePlayerReady() {
        if (player == null) {
            initializePlayer()
        }
    }
    private fun initializePlayer() {
        // 軌道選擇器(自動(dòng)選擇最佳質(zhì)量)
        val trackSelector = DefaultTrackSelector(this)
        trackSelector.setParameters(
            trackSelector.buildUponParameters()
                .setMaxVideoSizeHd()  // 最大 720p(省流量)
        )
        // 渲染工廠(優(yōu)先使用硬件解碼)
        val renderersFactory = DefaultRenderersFactory(this)
            .setEnableDecoderFallback(false)
            .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
        player = ExoPlayer.Builder(this, renderersFactory)
            .setTrackSelector(trackSelector)
            .setSeekForwardIncrementMs(10_000)   // 快進(jìn) 10 秒
            .setSeekBackIncrementMs(10_000)      // 快退 10 秒
            .build()
            .also { exo ->
                binding.playerView.player = exo
                binding.playerView.useController = true
                // 播放事件監(jiān)聽(tīng)
                exo.addListener(object : Player.Listener {
                    override fun onPlaybackStateChanged(state: Int) {
                        val stateName = when (state) {
                            Player.STATE_IDLE -> "空閑"
                            Player.STATE_BUFFERING -> "緩沖中"
                            Player.STATE_READY -> "就緒"
                            Player.STATE_ENDED -> "播放結(jié)束"
                            else -> "未知"
                        }
                        Toast.makeText(this@MainActivity, "播放器: $stateName",
                            Toast.LENGTH_SHORT).show()
                    }
                    override fun onPlayerError(error: PlaybackException) {
                        val msg = when (error.errorCode) {
                            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ->
                                "網(wǎng)絡(luò)連接失敗"
                            PlaybackException.ERROR_CODE_IO_BAD_URI ->
                                "無(wú)效的媒體地址"
                            PlaybackException.ERROR_CODE_DECODER_INIT_FAILED ->
                                "解碼器初始化失敗"
                            PlaybackException.ERROR_CODE_TIMEOUT ->
                                "網(wǎng)絡(luò)超時(shí)"
                            else -> "播放錯(cuò)誤: ${error.errorCodeName}"
                        }
                        Toast.makeText(this@MainActivity, msg, Toast.LENGTH_LONG).show()
                    }
                })
            }
        // 自動(dòng)播放第一個(gè)視頻
        playMediaItem(sampleVideos[0])
    }
    override fun onStart() {
        super.onStart()
        if (player == null) {
            initializePlayer()
        }
    }
    override fun onResume() {
        super.onResume()
        player?.playWhenReady = player?.playWhenReady ?: true
    }
    override fun onPause() {
        super.onPause()
        // 播放狀態(tài)自動(dòng)保存
    }
    override fun onStop() {
        super.onStop()
        player?.playWhenReady = false
    }
    override fun onDestroy() {
        super.onDestroy()
        releasePlayer()
    }
    private fun releasePlayer() {
        player?.release()
        player = null
    }
}

5.4 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black">
    <androidx.media3.ui.PlayerView
        android:id="@+id/playerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:show_buffering="when_playing"
        app:use_controller="true"
        app:resize_mode="fit"
        app:surface_type="surface_view"
        app:controller_layout_id="@layout/exo_player_control_view" />
    <!-- 加載指示器 -->
    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />
    <!-- 提示文字 -->
    <TextView
        android:id="@+id/hintText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:padding="16dp"
        android:text="點(diǎn)擊選擇視頻"
        android:textColor="@android:color/white"
        android:textSize="16sp" />
</FrameLayout>

5.5 CustomPlayerActivity:裸 MediaCodec 播放

package com.example.androidplayer.player
import android.os.Bundle
import android.util.Log
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.androidplayer.databinding.ActivityCustomPlayerBinding
/**
 * CustomPlayerActivity:使用裸 MediaCodec API 的自定義播放器
 * 展示完整的解復(fù)用 → 解碼 → 渲染流程
 * 對(duì)比 ExoPlayer 方案:代碼量更大,但完全可控
 */
class CustomPlayerActivity : AppCompatActivity(), SurfaceHolder.Callback2 {
    private lateinit var binding: ActivityCustomPlayerBinding
    private var decoder: MediaCodecVideoDecoder? = null
    private var videoPath: String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityCustomPlayerBinding.inflate(layoutInflater)
        setContentView(binding.root)
        // 從 Intent 獲取視頻路徑
        videoPath = intent.getStringExtra("video_path")
        setupUI()
        setupSurface()
    }
    private fun setupUI() {
        binding.surfaceView.holder.addCallback(this)
        // 全屏
        window.decorView.systemUiVisibility = (
            View.SYSTEM_UI_FLAG_FULLSCREEN
            or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
        )
    }
    private fun setupSurface() {
        binding.surfaceView.holder.setFormat(android.graphics.PixelFormat.RGBA_8888)
    }
    // ===== SurfaceHolder.Callback2 實(shí)現(xiàn) =====
    override fun surfaceCreated(holder: SurfaceHolder) {
        Log.d("CustomPlayer", "Surface created, starting decoder...")
        startPlayback(holder.surface)
    }
    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                 width: Int, height: Int) {
        Log.d("CustomPlayer", "Surface changed: ${width}x${height}")
    }
    override fun surfaceDestroyed(holder: SurfaceHolder) {
        Log.d("CustomPlayer", "Surface destroyed, stopping decoder...")
        stopPlayback()
    }
    override fun surfaceRedrawNeeded(holder: SurfaceHolder) {
        // 不需要額外處理
    }
    private fun startPlayback(surface: android.view.Surface) {
        val path = videoPath
        if (path == null) {
            Toast.makeText(this, "未指定視頻路徑", Toast.LENGTH_SHORT).show()
            finish()
            return
        }
        // 啟動(dòng)裸 MediaCodec 解碼器
        decoder = MediaCodecVideoDecoder(this, surface)
        decoder?.start(path)
    }
    private fun stopPlayback() {
        decoder?.stop()
        decoder?.release()
        decoder = null
    }
    override fun onPause() {
        super.onPause()
        stopPlayback()
    }
    override fun onDestroy() {
        super.onDestroy()
        stopPlayback()
    }
}

5.6 activity_custom_player.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"
    android:background="@android:color/black">
    <!-- SurfaceView:性能最優(yōu)的視頻渲染方式 -->
    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <!-- 進(jìn)度顯示 -->
    <TextView
        android:id="@+id/statusText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="top|start"
        android:padding="16dp"
        android:textColor="@android:color/white"
        android:textSize="12sp"
        android:background="#80000000"
        android:text="解碼中..." />
</FrameLayout>

六、常見(jiàn)問(wèn)題與調(diào)試技巧

6.1 解碼器創(chuàng)建失敗

// 常見(jiàn)原因 1:未找到對(duì)應(yīng) Mime 的解碼器
// 解決:先查詢可用解碼器
fun listAvailableCodecs() {
    val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
    for (info in codecList.codecInfos) {
        if (!info.isEncoder) {
            Log.d("Codec", "Decoder: ${info.name}")
            for (type in info.supportedTypes) {
                Log.d("Codec", "  - $type")
            }
        }
    }
}
// 常見(jiàn)原因 2:Surface 已銷毀后嘗試配置解碼器
// 解決:在 Surface 可用時(shí)再配置
// 常見(jiàn)原因 3:Android 10+ 作用域存儲(chǔ)限制
// 解決:使用 SAF (Storage Access Framework)
private val pickerLauncher = registerForActivityResult(
    ActivityResultContracts.OpenDocument()
) { uri ->
    uri?.let {
        // 持久化權(quán)限
        contentResolver.takePersistableUriPermission(
            it, Intent.FLAG_GRANT_READ_URI_PERMISSION)
        playUri(it)
    }
}

6.2 EGL 初始化失?。═extureView)

// TextureView 需要 EGL 支持,在部分設(shè)備上可能失敗
// 解決:降級(jí)到 SurfaceView
try {
    val surfaceTexture = textureView.surfaceTexture ?: return
    surfaceTexture.setDefaultBufferSize(videoWidth, videoHeight)
    val surface = Surface(surfaceTexture)
    // 配置解碼器
} catch (e: Exception) {
    Log.e("EGL", "TextureView EGL 失敗,降級(jí)到 SurfaceView", e)
    switchToSurfaceView()
}

6.3 音畫(huà)不同步

// 診斷:打印 PTS vs 實(shí)際渲染時(shí)間
Log.d("Sync", "Frame PTS: ${ptsUs}us, "
        + "Render delay: ${renderDelayUs}us, "
        + "Total buffer: ${totalBufferedMs}ms")

// 常見(jiàn)原因:
// 1. 視頻關(guān)鍵幀間隔太大 → 減少 I_FRAME_INTERVAL
// 2. 緩沖時(shí)間太長(zhǎng) → 減少 bufferForPlaybackMs
// 3. SurfaceView vs TextureView 延遲差異 → 使用 SurfaceView

6.4 解碼器緩沖區(qū)溢出/下溢

// 緩沖區(qū)溢出(Output Buffer 積壓):
// 原因:渲染速度 < 解碼速度
// 解決:增加 Surface 緩沖,或加速渲染

// 緩沖區(qū)下溢(Input Buffer 不足):
// 原因:網(wǎng)絡(luò)卡頓 / 文件讀取慢
// 解決:增加網(wǎng)絡(luò)緩沖,或使用 ExoPlayer 的 AdaptiveLoadControl

6.5 使用 adb 調(diào)試播放

# 查看 MediaCodec 狀態(tài)
adb shell dumpsys media.codec
# 查看 SurfaceFlinger 層
adb shell dumpsys SurfaceFlinger
# 查看 MediaPlayer 服務(wù)
adb shell dumpsys media.player
# 查看 ExoPlayer 日志(需要開(kāi)啟調(diào)試模式)
# 在代碼中:ExoPlayer.setLogLevel(Util.SDK_INT >= 21 ? Log.VERBOSE : Log.WARN)
adb logcat | grep -i "ExoPlayer\|MediaCodec\|Decoder"
# 抓取視頻幀進(jìn)行驗(yàn)證
adb shell screencap /sdcard/frame.png
adb pull /sdcard/frame.png

七、性能優(yōu)化與進(jìn)階方向

7.1 性能對(duì)比參考

渲染方式內(nèi)存占用CPU 占用渲染延遲適合場(chǎng)景
SurfaceView~1 幀直播、大屏播放器
TextureView~2 幀需 View 變換/截圖
GLSurfaceView~3 幀視頻濾鏡/OpenGL 特效
ExoPlayer (默認(rèn))~1 幀工程級(jí)播放器

7.2 進(jìn)階功能速覽

功能實(shí)現(xiàn)方式參考
視頻截圖textureView.bitmap / SurfaceView.lockCanvas()Android 官方
倍速播放player.setPlaybackParameters(PlaybackParameters(2.0f))ExoPlayer
多音軌切換trackSelector.setParameters(...)ExoPlayer
DRM 加密播放MediaDrmCallbackExoPlayer Widevine
后臺(tái)播放MediaSessionService + MediaSessionMedia3
投屏/CASTMediaRouteButton + PresentationAndroid Presentation
視頻濾鏡GLSurfaceView + OpenGL ES 2.0GPUImage Android
硬件解碼器選擇MediaCodecList 遍歷 + isHardwareAccelerated()CodecCapabilities

7.3 ExoPlayer 后臺(tái)播放 + MediaSession

/**
 * PlayerService:后臺(tái)播放服務(wù)
 * 支持通知欄控制、耳機(jī)線控、藍(lán)牙控制
 */
class PlayerService : MediaSessionService() {
    private var mediaSession: MediaSession? = null
    private lateinit var player: ExoPlayer
    override fun onCreate() {
        super.onCreate()
        player = ExoPlayer.Builder(this).build()
        mediaSession = MediaSession.Builder(this, player)
            .setCallback(MediaSessionCallback())
            .build()
    }
    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
        return mediaSession
    }
    override fun onTaskRemoved(rootIntent: Intent?) {
        if (!player.playWhenReady) {
            stopSelf()
        }
    }
    override fun onDestroy() {
        mediaSession?.run {
            player.release()
            release()
            mediaSession = null
        }
    }
    private inner class MediaSessionCallback : MediaSession.Callback {
        override fun onAddMediaItems(
            mediaSession: MediaSession,
            controller: MediaSession.ControllerInfo,
            mediaItems: MutableList<MediaItem>
        ): com.google.common.util.concurrent.ListenableFuture<MutableList<MediaItem>> {
            // 將 Uri 轉(zhuǎn)換為真實(shí)可播放的 MediaItem
            val resolved = mediaItems.map { item ->
                item.buildUpon()
                    .setUri(item.requestMetadata.mediaUri ?: item.localConfiguration?.uri)
                    .build()
            }.toMutableList()
            return Futures.immediateFuture(resolved)
        }
    }
}

八、調(diào)試命令與工具速查

工具命令/用途示例
adb dumpsys查看系統(tǒng)多媒體服務(wù)狀態(tài)adb shell dumpsys media.codec
adb logcat過(guò)濾 ExoPlayer/MediaCodec 日志adb logcat -s ExoPlayerImpl:* MediaCodec:*
adb screencap截取當(dāng)前幀adb shell screencap /sdcard/frame.png
adb screenrecord錄制播放過(guò)程adb shell screenrecord --time-limit 10 /sdcard/play.mp4
MediaCodecList查詢?cè)O(shè)備支持的編解碼器代碼中遍歷 MediaCodecList
GPU Render調(diào)試 GPU 渲染設(shè)置 → 開(kāi)發(fā)者 → GPU 渲染分析
perfetto系統(tǒng)級(jí)性能追蹤adb shell perfetto -c config.txt -o /sdcard/trace.perfetto
net槐網(wǎng)絡(luò)抓包Wireshark / mitmproxy 分析 HLS/DASH

九、總結(jié)

9.1 學(xué)習(xí)路徑推薦

第一階段(1–2周):
  MediaPlayer + SurfaceView → 實(shí)現(xiàn)簡(jiǎn)單播放
  ↓
第二階段(2–4周):
  MediaExtractor + MediaCodec → 理解解復(fù)用和解碼原理
  ↓
第三階段(4–6周):
  ExoPlayer 基礎(chǔ)使用 → 掌握軌道選擇、緩沖控制
  ↓
第四階段(持續(xù)):
  ExoPlayer 深度定制 → MediaSession、后服務(wù)、DRM

9.2 關(guān)鍵知識(shí)點(diǎn)回顧

層次核心概念關(guān)鍵 API
容器解析軌道提取、壓縮數(shù)據(jù)讀取MediaExtractor
解碼編解碼器狀態(tài)機(jī)、輸入/輸出緩沖MediaCodec.configure/start/dequeue
渲染Surface 生產(chǎn)者/消費(fèi)者、雙緩沖SurfaceView / TextureView
同步PTS 驅(qū)動(dòng)、音畫(huà)對(duì)齊策略SyncController
框架ExoPlayer 組件化、MediaSourceExoPlayer + PlayerView
后臺(tái)MediaSession、通知欄控制MediaSessionService

9.3 項(xiàng)目快速啟動(dòng)

# 1. 創(chuàng)建 Android Studio 項(xiàng)目
# File → New → New Project → Empty Activity

# 2. 添加 Media3 依賴(見(jiàn) 5.1 節(jié))

# 3. 將 MainActivity.kt 和 activity_main.xml 復(fù)制到項(xiàng)目中

# 4. 運(yùn)行到設(shè)備(推薦真機(jī),模擬器性能有限)

以上就是從零構(gòu)建一個(gè)Android全功能視頻播放器的詳細(xì)內(nèi)容,更多關(guān)于Android視頻播放器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

绥阳县| 南充市| 广宗县| 池州市| 灵寿县| 平度市| 噶尔县| 荆州市| 吉木萨尔县| 汉阴县| 神农架林区| 资中县| 尉氏县| 色达县| 读书| 安国市| 忻州市| 太湖县| 吉林省| 龙岩市| 临沂市| 宁安市| 辛集市| 南涧| 天水市| 盐山县| 酒泉市| 龙州县| 高青县| 怀来县| 浪卡子县| 合川市| 北碚区| 钟祥市| 晋州市| 筠连县| 广丰县| 台湾省| 武鸣县| 通州区| 米林县|