自己用 FFmpeg 寫一個 Android 播放器,到底在學什麼?
為什麼選擇看這一個課程?
自己用 FFmpeg 寫一個 Android 播放器,到底在學什麼?

為什麼選擇看這一個課程?
我在一家做影音類產品的公司寫 Android。平常用到的播放器都是第三方的,例如 ExoPlayer、GSYVideoPlayer,其實都已經封裝得很好了:丟一個 URL、綁一個 view,畫面就出來了。
但它們底層基本上都是 FFmpeg 包上來的。我用了這麼久,卻說不太清楚一支 .mp4 從硬碟到螢幕,中間到底發生了什麼。所以我跟著一套線上課程,從零開始用 NDK C++ + FFmpeg 寫了一個能播放的 Android 播放器,順便把整個流程整理成這篇筆記。
如果你也只會 setDataSource() 然後 start(),這篇可以幫你補上中間那一塊黑盒子。
播放器的本質,是一條流水線
先講結論。一個播放器拆到最底層,就是這樣一條 Pipeline(流水線):
[解封裝] 把 mp4 拆成壓縮封包
├──→ [視頻解碼] → YUV 幀 → [OpenGL ES 渲染] → 螢幕
└──→ [音頻解碼] → PCM → [重採樣] → [OpenSL ES] → 喇叭
.mp4 不是「影片」,它是一個容器(Container),裡面裝著一條視頻軌(通常是 H.264 編碼)和一條音頻軌(通常是 AAC 編碼),還有一張「索引表」記錄每一幀在檔案的哪個位置。
播放,就是把這個容器拆開、把壓縮資料解開、把畫面畫到螢幕、把聲音送到喇叭。整套課程也就是順著這條線,一段一段把它建起來。下面依課程的五個部分,介紹每一段在學什麼。
第一部分:多媒體基礎理論
在碰 FFmpeg API 之前,要先有一套詞彙。這部分鋪墊整個領域的基礎概念:
- 封裝格式(Container):MP4、FLV、MOV、AVI 的差異,以及 Box 結構的概念。
- 視頻編碼:H.264 的 NAL、SPS/PPS、GOP,以及 I / P / B 三種幀的特性。也是在這裡理解到,有 B 幀時 **解碼順序(DTS)≠ 顯示順序(PTS)。
- 音頻編碼:AAC、FLAC、PCM 的取捨,以及一個完整檔案的解碼流程長什麼樣。
- 色彩空間與像素格式:為什麼視頻用 YUV 而不是 RGB(人眼對亮度比色彩敏感,可對色度降採樣省約一半資料),以及 YUV→RGB 的轉換公式。
- 色彩空間與像素格式:為什麼視頻用 YUV 而不是 RGB(人眼對亮度比色彩敏感,可對色度降採樣省約一半資料),以及 YUV→RGB 的轉換公式。
- 音頻採樣:採樣率、聲道、樣本格式,以及 Interleaved 與 Planar 兩種排列方式。
- MP4 Box 結構**:
moov(索引)、mdat(實際資料)、以及stbl樣本表如何讓播放器精準 seek。 - JNI 核心概念:Java 與 C/C++ 怎麼互相呼叫、函式命名規則、字串轉換,以及
JNI_OnLoad的角色。
這部分沒有什麼程式碼,但少了它,後面的 API 會看不懂在操作什麼。
第二部分:FFmpeg 核心 API
這部分是整個專案的引擎,學的是怎麼用 FFmpeg 把一個檔案從封包變成原始資料:
- 解封裝流程:
avformat_open_input→avformat_find_stream_info→av_read_frame,以及AVFormatContext/AVStream/AVCodecParameters這幾個核心結構體。 - 串流遍歷與
av_find_best_stream:怎麼自動找出最佳的視頻 / 音頻軌。 - 讀取封包:
av_read_frame與AVPacket,特別是引用計數與unref/free的釋放規則。 - Seek:
av_seek_frame的時間戳換算與各種 flag,以及 seek 後要清空解碼器緩衝。 - 解碼器初始化:軟解(CPU)與硬解(MediaCodec)的差異,以及「找解碼器 → 分配 context → 複製參數 → open」的三步驟。
- 送收解碼模型:新版 FFmpeg 把解碼拆成
avcodec_send_packet與avcodec_receive_frame。重點是它不是一對一 — — 送一個封包可能收到 0 幀或多幀,所以要用雙層迴圈把幀收乾淨。 - swscale:用 CPU 做像素格式轉換(YUV→RGBA)。
- swresample:把任意格式的音頻統一轉成播放所需的 S16 PCM。
走完這部分,就能在 log 裡看到解碼出來的幀率了,雖然畫面還沒出來。
第三部分:Android 渲染與播放技術
有了原始的 YUV 幀和 PCM,接下來是怎麼把它們真正輸出。這部分講三套底層技術:
- ANativeWindow(CPU 渲染):最直覺的做法 — — 用
swscale在 CPU 轉成 RGBA,再透過ANativeWindow一行一行畫到 Surface。這裡會碰到 Stride 對齊 的問題:GPU buffer 每行寬度不一定等於影像寬度,整塊複製會花屏,要逐行依 stride 步進。缺點是 1080p 每幀要 CPU 處理約 300 萬像素,很吃效能。 - OpenSL ES(音頻播放):NDK 提供的低延遲音頻 API,採「物件 → 介面」兩步驟模式,用 Pull 模式回呼不斷要資料。輸出的 PCM 格式必須和 swresample 的輸出完全一致。
- OpenGL ES + EGL(GPU 渲染):專業播放器的做法,把 YUV 三平面各自上傳成紋理,用 Fragment Shader 在 GPU 上做 YUV→RGB。同樣一幀 1080p,CPU 路線要 15~30ms,GPU 路線只要 1~3ms。EGL 則負責 OpenGL ES 與視窗系統之間的橋接(畫在哪、用什麼格式、雙緩衝怎麼翻)。
這三套是第四部分模組化架構的技術基礎。
第四部分:模組化播放器架構
技術點都通了之後,真正的工程挑戰才開始:怎麼把解封裝、解碼、渲染、重採樣、播放這五個跑在不同執行緒的東西,串成一條乾淨的線?
課程的答案是 **C++ 抽象介面 + 觀察者模式(Observer Pattern)。每個模組只依賴抽象介面,具體實作藏在後面:
XThread(執行緒基類)
└── IObserver(觀察者:AddObserver / Notify / Update)
├── IDemux ── FFDemux 解封裝
├── IDecode ── FFDecode 解碼
├── IVideoView── GLVideoView 視頻渲染
├── IResample ── FFResample 音頻重採樣
└── IAudioPlay── SLAudioPLay 音頻播放
這部分依序實作了:
- 基礎設施:跨平台日誌
XLog、媒體資料容器XData(含記憶體所有權轉移)、執行緒基類XThread(協作式停止)、參數橋樑XParameter。 - 觀察者模式
IObserver:解封裝讀到封包就Notify給解碼器,解碼器解出幀再Notify給渲染器或重採樣器,生產者和消費者完全解耦。 - 各模組實作:Demux / Decode / 視頻渲染(XEGL / XShader / XTexture)/ 音頻(Resample / AudioPlay)。
- Pipeline 串接與啟動時序:怎麼把整條鏈組起來,以及解決「Surface 還沒建立就啟動 Pipeline」導致的白畫面問題。
這套架構最實用的地方在於可替換性:哪天要把軟解換成硬解、OpenGL 換成 Vulkan、OpenSL 換成 AAudio,只要新增一個實作類,上層程式碼不用動。
第五部分:關鍵 Bug 與工程經驗
跟著影片手打程式碼,最值得記下來的其實是那些踩過的坑。課程最後把它們彙整成一份清單,幾個比較有代表性的:
- NPOT 紋理全黑:ES 2.0 規範要求非 2 次方紋理(如 1920×1080)的 Wrap Mode 必須設成
GL_CLAMP_TO_EDGE,否則紋理不完整、取樣全黑。 - 音頻被默默丟棄:
XData::Alloc()配置了記憶體卻忘了設size,下游if (size<=0) return把資料全當成空的丟掉。 - 白畫面:
JNI_OnLoad在 Surface 建立前就啟動 Pipeline,幀無處可渲染。要把 Pipeline 延後到 surface 就緒後再啟動。 - OpenSL 無聲:locator 用了 Android 版的 buffer queue,取介面卻用跨平台版的 ID,兩者必須配對。
- 觀察者方向寫反:應該是
subject->AddObserver(observer),反了就收不到資料。
這些坑背後反覆出現的核心觀念有五個:記憶體所有權(誰分配誰釋放,AVPacket 要用 av_packet_free)、執行緒安全(跨執行緒容器要上鎖、持鎖時不 sleep)、初始化時機(EGL Context 綁定執行緒、Pipeline 要等 Surface)、格式一致性(音頻每個欄位都要對齊),以及 C++ 分離編譯的基本功。
最後的目標
整套課程的終點,是一個模組化、可替換底層實作的播放器:用 FFmpeg 解封裝 / 解碼,OpenGL ES 做視頻渲染,OpenSL ES 做音頻播放,再用 C++ 抽象層加觀察者模式把它們串成一條完整的 Pipeline。
要說明的是,這個專案到這裡還不算一個「完整」的播放器。因為影片只到 P95、缺了最後一章,所以整理也只到這裡(對應約 P28 ~ P93.5)。還沒涵蓋的部分包括音視頻同步(A/V Sync)、Seek 控制 UI、以及播放 / 暫停 / 進度等播放控制層。換句話說,它能把畫面和聲音正確輸出,但離一個可上架的播放器還有播放控制這一大塊。
心得
剛好現在公司維護的產品是影音類型的 App,目前用到的播放器都是第三方的,例如 ExoPlayer、GSYPlayer,其實都已經封裝得很好了,但它們底層基本上都是 FFmpeg 下去封裝上來的,所以才想看這個教學影片學一下。確實學到了怎麼編譯 FFmpeg、C++,以及怎麼透過封裝 FFmpeg 產出一個 Android 上可以用的基本播放器。
不過整個教學看完之後,老實說還是滿模糊的。第一是因為 FFmpeg 本身就是一個很龐大的庫,再加上裡面還用到 OpenGL、OpenSL 影像跟音訊的部分,這些結合起來之後又更難了,還需要一定的 C++ 基礎。一般人要入門難度偏高,沒人帶、或是沒有公司本身的產品需求,真的不太容易看懂。
但如果產品不是一般的播放器,可能就會需要自己透過 FFmpeg 編譯成想要的設定 — — 例如監視器播放器要求的可能是低功耗,直播播放器要求的可能是 OpenGL 直接繪製。這時候有沒有走過一遍底層,差別就出來了。
最後可以特別說一下這份筆記的製作方法:我把每一支教學影片當作一個分支,push 之後再請 AI 去分析這個分支新增了哪些程式碼,整理後貼到 Notion。雖然有 Notion MCP 可以用,但有點耗 token。另外,看影片時程式碼都是手打的,很容易出錯,尤其 Native 層的錯誤比較難觀察,這時候很適合搭配 AI 去分析錯誤、調整方向;不然像以前可能光找一個錯誤就要老半天,整個學習進度和慾望都會被拖累。
📺 Bilibili 教學影片 https://www.bilibili.com/video/BV1m4YjeNEHJ/?vd_source=96074d6252915f8aca24f7557336569d&spm_id_from=333.788.videopod.episodes
🎓 Udemy 課程 https://www.udemy.com/course/ffmpeg-android/
💻 教學原始碼(原作者) https://github.com/playactorzxc/FFmpeg01
메타데이터
- post_id
- 5e7e68d9478f
- slug
- 自己用-ffmpeg-寫一個-android-播放器-到底在學什麼-5e7e68d9478f
- url
- https://medium.com/@west7418/%E8%87%AA%E5%B7%B1%E7%94%A8-ffmpeg-%E5%AF%AB%E4%B8%80%E5%80%8B-android-%E6%92%AD%E6%94%BE%E5%99%A8-%E5%88%B0%E5%BA%95%E5%9C%A8%E5%AD%B8%E4%BB%80%E9%BA%BC-5e7e68d9478f
- canonical_url
- https://medium.com/@west7418/%E8%87%AA%E5%B7%B1%E7%94%A8-ffmpeg-%E5%AF%AB%E4%B8%80%E5%80%8B-android-%E6%92%AD%E6%94%BE%E5%99%A8-%E5%88%B0%E5%BA%95%E5%9C%A8%E5%AD%B8%E4%BB%80%E9%BA%BC-5e7e68d9478f
- author_url
- https://medium.com/@west7418
- status
- ok
- fetched_at
- 2026-07-13 06:23:13