Android動效探索:徹底弄清如何讓你的視頻更加酷炫

在Android移動端視頻處理領域,除了基本的播放功能外,添加動畫和濾鏡等特效已經成為提升用戶體驗的重要手段。然而,很多開發人員可能對于實現這些功能所需的技術細節感到困惑。因此,本文旨在提供一個詳細的指導,幫助開發人員掌握如何使用開源MediaPlayer或自定義播放器,并利用OpenGL ES來實現視頻動畫和濾鏡效果。
1分鐘看圖掌握核心觀點??

從事Android移動端開發的人員一定會跟動效打交道,并且對于常見的幀動畫、屬性動畫使用起來更是得心應手,但是你一定也遇到一些問題,就是在做動效時,你能使用的資源無非就是圖片、gif圖或者PAG圖,這些資源只能做簡短、復雜度一般的效果,如果要做一個時間跨度較長并且動效要求較高的動效,這時候就需要借助視頻來做了。
01、視頻做動畫,你可能無從下手
我們可以直接使用Mediaplayer、VideoView等開源播放器把UI設計師給我們的視頻文件播放出來,一般情況下這樣就夠了。但是有一天UI設計師讓你在視頻的第50-100幀做些處理,視頻畫面做下抖動、放大等的處理,你可能會有些不知所措,這時候你的腦子里面可能有這些概念:

那么問題來了,究竟使用什么方案才能實現UI要求的效果?這個時候,你可能會deepseek或者找些技術博客去了解一下,不過結果無非是這樣的,仍然是無法把應該具備的知識點串起來:

總之這時候的你,還是無從下手!
所以如果沒有系統的了解,這時候就有可能使用錯方案,達不到效果,比如你可能會想到是不是在原先的視頻播放器窗口覆蓋一層View,View動態顯示截圖的視頻窗口圖片,這種方案就是存在問題的。那么本文就是為了幫助梳理這些知識點,整理出了為了實現視頻動效的完整實現流程,話不多說,先看實現結構圖:

仔細看上面這張結構圖,你的零散的知識點也許可以串聯起來一些了,但是可能還不夠全面!
結論先行,實現一個視頻動畫有兩種方式:
實現方案1
直接使用開源的MediaPlayer播放器,然后利用OpenGL ES進行圖形管線的接管與處理,對每一幀圖片再去處理。優點是實現起來更加的方便,可以快速上手,但是缺點就是你只能對既有的視頻幀做處理,沒辦法去修改視頻幀底層的邏輯,雖然可以實現復雜的動效,但是仍然是受限的。
實現方案2
使用FFmpeg自己手擼一個播放器,要是實現簡單動效,就借助原生的ANativeWindow,可以直接操作幀緩沖區(FrameBuffer),屬于內存到屏幕的像素級拷貝,沒有GPU的參與;或者使用GL介入,做視頻紋理的管理,實現更加復雜的動效。這個實現方式缺點是比較復雜,但是最大的優點就是FFmpeg本身可以做到跨平臺編譯,不止是可以使用在Android,也可以使用在iOS平臺。另外可以修改視頻的更底層邏輯,滿足更多的動效需求,比如類似抖音,有些特效都是可以做的。
兩個方案有共同點,都需要OpenGL ES進行渲染視圖,很多開發者只是了解這個概念,不清楚為什么要使用它,下面我們來徹底講清楚。
02、初識OpenGL ES相關概念
OpenGL,全稱是Open Graphics Library,譯名:開放圖形庫或者“開放式圖形庫”,用于渲染 2D、3D 矢量圖形的跨語言、跨平臺的應用程序編程接口(API)。OpenGL 跟語言和平臺無關。OpenGL 純粹專注于渲染,而不提供輸入、音頻以及窗口相關的 API。這些都有硬件和底層操作系統提供。OpenGL 的高效實現(利用了圖形加速硬件)存在于 Windows,部分 UNIX 平臺和 Mac OS,可以便捷利用顯卡等設備。
也就是說,OpenGL就是繪制圖形使用的,那么你的視頻中播放的一幀幀圖片,也是圖形,所以你要是想做動畫,也就是對圖形做形變,就需要使用OpenGL幫你繪制出最終的圖形。
OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三維圖形 API 的子集,針對手機、PDA和游戲主機等嵌入式設備而設計。經過多年發展,現在主要有兩個版本,OpenGL ES 1.x 針對固定管線硬件的,OpenGL ES 2.x 針對可編程管線硬件。Android 2.2 開始支持 OpenGL ES 2.0,OpenGL ES 2.0 基于 OpenGL 2.0 實現。一般在 Android 系統上使用 OpenGL,都是使用 OpenGL ES 2.0,1.0 僅作了解即可。我們在Android開發中,使用的穩定版本,也都是ES 2.0。
2.1坐標系的概念
作為一個Android移動端開發者。應該知道坐標系的概念,物體的位置都是通過坐標系確定的。OpenGL ES 采用的是右手坐標,選取屏幕中心為原點,從原點到屏幕邊緣默認長度為 1,也就是說默認情況下,從原點到(1,0,0)的距離和到(0,1,0)的距離在屏幕上展示的并不相同。坐標系向右為 X 正軸方向,向左為 X 負軸方向,向上為 Y 軸正軸方向,向下為 Y 軸負軸方向,屏幕面垂直向上為 Z 軸正軸方向,垂直向下為 Z 軸負軸方向。

總結一下:在 OpenGL 中,世界就是一個坐標系,一個只有 X、Y 和 Z 三個緯度的世界,其它的東西都需要你自己來建設,你能用到的原材料就只有點、線和面(三角形),當然還會有其他材料,比如陽光(光照)和顏色(材質)。
2.2相機
在OpenGL中,"相機"的概念類似于現實世界的相機或人眼,其功能是捕獲三維世界中的場景,并呈現到二維視圖上。通過調整“相機”參數,可以改變觀看的角度和范圍,從而影響最終呈現的效果。
2.3紋理
紋理是二維圖像,用于映射到三維物體的表面上,使其看起來更加真實和細膩。紋理映射是一種重要的渲染技術,通過將紋理應用于物體表面,賦予物體顏色、圖案等視覺效果,而不改變其幾何形態。紋理的作用類似于為物體穿上“衣服”,提升視覺上的真實感。
2.4OpenGL ES的使用流程

通過上面的流程,我們可以確認圖形的渲染大致可以表述如下:
管理一個 surface,這個 surface 就是一塊特殊的內存,能直接排版到 android 的視圖 view 上。
管理一個 EGL display,它能讓 opengl 把內容渲染到上述的 surface 上。
用戶可以自定義渲染器(render)。
讓渲染器在獨立的線程里運作,和 UI 線程分離。傳統的 View 及其實現類,渲染等工作都是在主線程上完成的。
在Android開發中,我們就是借助SurfaceView來進行視圖的渲染,SurfaceView的實質是將底層顯存 Surface 顯示在界面上,而 GLSurfaceView 做的就是在這個基礎上增加 OpenGL 繪制環。
有了上面這些概念之后,那么下面我們從簡單的MediaPlayer入手,從圖形管線接入的角度,徹底弄清GLSurfaceView的工作原理,再去介紹手擼播放器如何來做。讓你的知識點完全串聯起來,之前不曾了解的知識點,通過本文也可以進一步的補充。
03、輕松上手MediaPlayer實現視頻動畫
看一下完整的實現視頻動畫的流程圖:

1. OpenGL環境搭建
先看下引用GLSurfaceView的代碼結構。第一步是創建一個Activity,并且在布局文件里面構建一個自定義的VideoGLSurfaceView,Activity里面聲明該VideoGLSurfaceView準備使用。
布局文件如下:
// 其他代碼
<com.ne.firstvideo.gl.VideoGLSurfaceView
android:id\="@+id/glSurfaceView"
android:layout\_width\="match\_parent"
android:layout\_height\="200dp"
app:layout\_constraintTop\_toBottomOf\="@+id/original\_surfaceView"\>
</com.ne.firstvideo.gl.VideoGLSurfaceView\>
// 其他代碼VideoGLSurfaceView里面需要創建GlSurView環境。
private voidinit(Context context) {
// 使用 OpenGL ES 2.0 以兼容更多設備
setEGLContextClientVersion(2);
// 關鍵步驟 1: 設置透明背景
setEGLConfigChooser(new TransparentConfigChooser());
setZOrderOnTop(true); // 必須設置
getHolder().setFormat(PixelFormat.TRANSLUCENT); // 必須設置
renderer \= new VideoRenderer(this);
setRenderer(renderer);
setRenderMode(RENDERMODE\_WHEN\_DIRTY);
}在 OpenGL 中,一旦我們設置好了基本環境(即畫布),就可以開始繪制圖形了。在這個過程中,著色器(shader)相當于畫筆的功能,主要有兩類著色器:頂點著色器(Vertex Shader)和片元著色器(Fragment Shader)。頂點著色器通常用于定義待渲染圖形的頂點;例如,對于要繪制的三角形,可以通過頂點著色器指定該三角形的三個頂點。因此,形狀就得以確定。片元著色器則負責圖形的填充和呈現效果。它可以決定如何為三角形的內部區域上色。
2. Render渲染器聲明
當使用 GLSurfaceView 時,為了定義著色器,我們需要繼承 GLSurfaceView.Renderer 類。Renderer 在這里是渲染器的意思,負責圖形的渲染過程。OpenGL ES 2.0 專為支持可編程流水線的硬件設計,因此其使用與編程緊密結合。這里我們定義了渲染器VideoRenderer,首先,我們需要定義著色器的構建程序。程序如何寫,后面再詳講:
// 頂點著色器(兼容 OpenGL ES 2.0)
privatestaticfinal String VERTEX_SHADER =
"uniform mat4 uMVPMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec2 aTexCoord;\n" +
"varying vec2 vTexCoord;\n" +
"void main() {\n" +
" gl_Position = uMVPMatrix * aPosition;\n" +
" vTexCoord = aTexCoord;\n" +
"}";
// 片段著色器(支持外部紋理)
privatestaticfinal String FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"varying vec2 vTexCoord;\n" +
"uniform samplerExternalOES uVideoTexture;\n" +
"void main() {\n" +
" gl_FragColor = texture2D(uVideoTexture, vTexCoord);\n" +
"}";再去按照固定的寫法去構建著色器,代碼是相對固定的。
privatevoidinitShader(){
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
// 檢查錯誤
int[] linkStatus = newint[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("Renderer", "Shader link error: " + GLES20.glGetProgramInfoLog(program));
}
}再去創建好program,就說明你的環境基本可以使用了。
privatevoidinitShader(){
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
// 檢查錯誤
int[] linkStatus = newint[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("Renderer", "Shader link error: " + GLES20.glGetProgramInfoLog(program));
}
}3. 初始化MediaPlayer
在這里面進行了MediaPlayer的創建:
publicvoidsetVideoPath(String path){
this.pendingVideoPath = path;
if (mediaPlayer == null) {
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
publicvoid onPrepared(MediaPlayer mp) {
mp.start();
// 觸發 OpenGL 初始化(如果尚未就緒)
requestRender();
}
});
}
}細心的開發同學會發現,Mediaplayer創建完成之后,并沒有立即播放視頻,如果你播放視頻,會崩潰,這是因為視頻流繪制相關的SurfaceTexture的創建還沒完成,你想把畫面展示在Surface上面一定會失敗。所以我們需要加入一個監聽,等SurfaceTexture創建完成之后,再去播放視頻。
先聲明好回調。
surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(st -> {
// 請求渲染
mVideoGLSurfaceView.requestRender();
});
if (textureReadyListener != null) {
textureReadyListener.onSurfaceTextureReady(surfaceTexture);
}再去做監聽,進行視頻播放。
privatevoidinit(Context context){
// 使用 OpenGL ES 2.0 以兼容更多設備
setEGLContextClientVersion(2);
// 關鍵步驟 1: 設置透明背景
setEGLConfigChooser(new TransparentConfigChooser());
setZOrderOnTop(true); // 必須設置
getHolder().setFormat(PixelFormat.TRANSLUCENT); // 必須設置
renderer = new VideoRenderer(this);
setRenderer(renderer);
setRenderMode(RENDERMODE_WHEN_DIRTY);
// SurfaceTexture 就緒回調
renderer.setOnSurfaceTextureReadyListener(surfaceTexture -> {
if (mediaPlayer != null && pendingVideoPath != null) {
try {
// 1. 重置 MediaPlayer
mediaPlayer.reset();
// 2. 設置 DataSource
mediaPlayer.setDataSource(pendingVideoPath);
// 3. 設置 Surface
mediaPlayer.setSurface(new Surface(surfaceTexture));
// 4. 準備異步
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}4. 從SurfaceTexture獲取幀紋理
首先要獲取圖形頂點,此步驟用來定義圖形形狀。
publicVideoRenderer(VideoGLSurfaceView videoGLSurfaceView){
mVideoGLSurfaceView = videoGLSurfaceView;
// 初始化頂點緩沖
vertexBuffer = ByteBuffer.allocateDirect(VERTEX_DATA.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(VERTEX_DATA);
vertexBuffer.position(0);
// 初始化紋理坐標緩沖
texCoordBuffer = ByteBuffer.allocateDirect(TEX_COORD_DATA.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(TEX_COORD_DATA);
texCoordBuffer.position(0);
}Android 的 OpenGL 底層是用 C/C++ 實現的,所以和 Java 的數據類型字節序列有一定的區別,主要是數據的大小端問題。ByteBuffer.order() 方法設置以下數據的大小端順序,順序設置為 native 層的數據順序。使用 ByteOrder.nativeOrder() 可以得到 native 層的大小端數據順序。
進行具體繪制操作。主要是實現繼承自 GLSurfaceView.Renderer 的三個方法:
@Override
publicvoidonSurfaceCreated(GL10 gl, EGLConfig config){
initTexture();
initShader();
}
@Override
publicvoidonSurfaceChanged(GL10 gl, int width, int height){
GLES20.glViewport(0, 0, width, height);
Matrix.setIdentityM(mvpMatrix, 0);
}
@Override
publicvoidonDrawFrame(GL10 gl){
Log.d("VideoRender", "onDrawFrame");
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 更新幀計數器
frameCount++;
// 從第10幀開始動畫
if (frameCount >= 10 && !animationStarted) {
animationStarted = true;
frameCount = 0; // 重置計數器以便計算動畫進度
}
// 計算縮放因子
if (animationStarted && frameCount <= ANIMATION_DURATION) {
float progress = (float) frameCount / ANIMATION_DURATION;
scaleFactor = 1.0f + (MAX_SCALE - 1.0f) * progress;
} else {
scaleFactor = 1.0f;
}
// 計算旋轉角度
if (animationStarted && (frameCount <= ANIMATION_DURATION + 30 && frameCount > 20)) {
// 計算旋轉進度(從第20幀開始)
int rotationFrame = frameCount - (ROTATION_START_FRAME - 10);
if (rotationFrame < 0) rotationFrame = 0;
float rotationProgress = (float) rotationFrame / ROTATION_DURATION;
if (rotationProgress > 1) {
rotationProgress = 1;
}
rotationAngle = MAX_ROTATION * rotationProgress;
} else {
rotationAngle = 0.0f;
}
// 生成縮放后的MVP矩陣
float[] finalMvpMatrix = applyScaleAndRotationToMvpMatrix(mvpMatrix, scaleFactor, rotationAngle);
if (surfaceTexture != null) {
surfaceTexture.updateTexImage(); // 更新紋理
}
GLES20.glUseProgram(program);
int mvpMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");
// GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0); 這個是沒有任何縮放動畫的代碼
// 這個是有縮放效果的代碼
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, finalMvpMatrix, 0);
// 綁定頂點數據
int positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
// 綁定紋理坐標
int texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord");
GLES20.glEnableVertexAttribArray(texCoordHandle);
GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer);
// 繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(positionHandle);
GLES20.glDisableVertexAttribArray(texCoordHandle);
}5. 處理幀數據/疊加動畫
使用finalMvpMatrix 對原先的mvpMatrix做了轉變,在這里進行動畫相關的設置,這里我們做了一個旋轉的動畫,并且是從視頻的第10-20幀縮放,從第20-30幀旋轉,31幀開始回到原先狀態。
// 更新幀計數器
frameCount++;
// 從第10幀開始動畫
if (frameCount >= 100 && !animationStarted) {
animationStarted = true;
frameCount = 0; // 重置計數器以便計算動畫進度
}
// 計算縮放因子
if (animationStarted && frameCount <= ANIMATION_DURATION) {
float progress = (float) frameCount / ANIMATION_DURATION;
scaleFactor = 1.0f + (MAX_SCALE - 1.0f) * progress;
} else {
scaleFactor = 1.0f;
}
// 計算旋轉角度
if (animationStarted) {
// 計算旋轉進度(從第150幀開始)
int rotationFrame = frameCount - (ROTATION_START_FRAME - 100);
if (rotationFrame < 0) rotationFrame = 0;
float rotationProgress = (float) rotationFrame / ROTATION_DURATION;
rotationAngle = MAX_ROTATION * rotationProgress;
}
// 生成縮放后的MVP矩陣
float[] finalMvpMatrix = applyScaleAndRotationToMvpMatrix(mvpMatrix, scaleFactor, rotationAngle)
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, finalMvpMatrix, 0);privatefloat[] applyScaleAndRotationToMvpMatrix(float[] originalMatrix, float scale, float rotation) {
float[] finalMatrix = newfloat[16];
Matrix.setIdentityM(finalMatrix, 0);
// 1. 應用原始矩陣
Matrix.multiplyMM(finalMatrix, 0, originalMatrix, 0, finalMatrix, 0);
// 2. 應用縮放
Matrix.scaleM(finalMatrix, 0, scale, scale, 1.0f);
// 3. 應用旋轉(繞Z軸)
Matrix.rotateM(finalMatrix, 0, rotation, 0, 0, 1.0f);
return finalMatrix;
}6. 渲染到屏幕
// 綁定頂點數據
int positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
// 綁定紋理坐標
int texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord");
GLES20.glEnableVertexAttribArray(texCoordHandle);
GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer);
// 繪制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(positionHandle);
GLES20.glDisableVertexAttribArray(texCoordHandle);需要注意的是,這里使用到了紋理坐標和頂點坐標,這兩個坐標在下文也有使用,那么這兩個坐標起到什么作用?先來看下這兩個坐標的定義:
7. 效果呈現

到這里,對于如何使用OpenGL ES進行畫面渲染的流程,你應該也比較熟悉了,繼續往下看。
04、提升難度 FFmpeg手擼播放器實現動畫
在上面知識點了解之前,有人是先學習的FFmpeg,但是很多人在FFmpeg編譯這一步時就被勸退了,因為確實有些麻煩,不像上面的知識點那么純粹,使用FFmpeg做一款動畫播放器,涉及到FFmpeg的編譯、引入、jni的代碼編寫(C++)、Android工程、以及上面提供的SurfaceView、Surface、頂點和片段著色器這些知識點。那么這一章節會帶你克服之前可能遇到的問題,讓你順利開發出一個播放器。
4.1FFmpeg 編譯
Windows環境下,不要使用Cygwin,不然需要再去安裝一堆插件,解決版本兼容的問題,太麻煩了,試了好幾遍都無法成功。直接使用MSYS2,(需要注意的是,這里使用的是Windows的環境,如果你是MAC或者其他環境,操作起來更簡單,這個可以自行搜索一下)。
編譯完成之后,就可以生成可以跨平臺使用的可調用庫文件,這里以so文件舉例:

借助Android Studio創建一個C++項目,把上面的so文件拷到你的項目里,頭文件在include下面,這個拷arm64-v8a或者armeabi-v7a下面的頭文件都可以,如下所示:

4.2基礎播放器實現
先看一下流程圖,有了這個圖之后,就有了清晰的認識,在哪個環節實現動畫也就一目了然。再來看一下做一款播放器的流程圖:

1. 初始化FFmpeg庫
這里比較簡單,初始化一下網絡協議就行,為了方便起見,可以把頭部需要引用的庫都加進來。
#include<jni.h>
#include<string>
#include<android/native_window.h>
#include<android/native_window_jni.h>
#include<android/log.h>
#include<android/bitmap.h>
#define LOG_TAG "Firstvideo"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern"C" {
#include"include/libavutil/log.h"
#include"include/libavutil/frame.h"
#include"include/libavutil/avutil.h"
#include"include/libavutil/imgutils.h"
#include"include/libavutil/opt.h"
#include"include/libavformat/avformat.h"
#include"include/libavcodec/avcodec.h"
#include"include/libswscale/swscale.h"
//初始化FFmpeg庫
avformat_network_init();2. 打開視頻文件
constchar *videoPath = env->GetStringUTFChars(videoPath_, 0);
LOGD("videoPath: %s", videoPath);
if (videoPath == NULL) {
LOGE("videoPath is null");
return;
}
AVFormatContext *formatContext = avformat_alloc_context();
LOGD("open video file");
int ret = avformat_open_input(&formatContext, videoPath, NULL, NULL);
if (ret != 0) {
char errorBuf[256];
av_strerror(ret, errorBuf, sizeof(errorBuf));
LOGE("無法打開視頻文件: %s, 錯誤: %s", videoPath, errorBuf);
return;
}3. 查找流信息
LOGD("Retrieve stream information");
if (avformat_find_stream_info(formatContext, NULL) < 0) {
LOGE("Cannot find stream information");
return;
}4. 查找視頻流
LOGD("Find video stream");
int video_stream_index = -1;
for (int i = 0; i < formatContext->nb_streams; i++) {
if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
}
}
if (video_stream_index == -1) {
LOGE("No video stream found");
return;
}5. 獲取編碼器上下文
LOGD("Get a pointer to the codec context for the video stream");
AVCodecParameters *codecParameters = formatContext->streams[video_stream_index]->codecpar;
LOGD("Find the decoder for the video stream");
const AVCodec *codec = avcodec_find_decoder(codecParameters->codec_id);
if (codec == NULL) {
LOGE("Codec not found");
return;
}
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
if (codecContext == NULL) {
LOGE("CodecContext not found");
return;
}
if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {
LOGE("Fill CodecContext failed");
return;
}6. 打開編解碼器
LOGD("Open codec");
if (avcodec_open2(codecContext, codec, NULL) < 0) {
LOGE("Init CodecContext failed");
return;
}7. 為視頻幀分配空間
AVPixelFormat dstFormat = AV_PIX_FMT_RGBA;
AVPacket *packet = av_packet_alloc();
if (packet == NULL) {
LOGE("Could not allocate av packet");
return;
}
LOGD("Allocate video frame");
AVFrame *frame = av_frame_alloc();
LOGD("Allocate render frame");
AVFrame *renderFrame = av_frame_alloc();
if (frame == NULL || renderFrame == NULL) {
LOGE("Could not allocate video frame");
return;
}8. 分配處理視頻幀的內存空間
LOGD("Determine required buffer size and allocate buffer");
int size = av_image_get_buffer_size(dstFormat, codecContext->width, codecContext->height, 1);
uint8_t *buffer = (uint8_t *) av_malloc(size * sizeof(uint8_t));
av_image_fill_arrays(renderFrame->data, renderFrame->linesize, buffer, dstFormat, codecContext->width, codecContext->height, 1);9. 初始化圖像轉換結構體SwsContext
structSwsContext *swsContext = sws_getContext(codecContext->width,
codecContext->height,
codecContext->pix_fmt,
codecContext->width,
codecContext->height,
dstFormat,
SWS_BILINEAR,
NULL,
NULL,
NULL);
if (swsContext == NULL) {
LOGE("Init SwsContext failed");
return;
}10. 創建本地視圖窗口管理器
LOGD("native window");
ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
ANativeWindow_Buffer windowBuffer;
LOGD("get video width, height");11. 獲取視頻的寬高
int videoWidth = codecContext->width;
int videoHeight = codecContext->height;
LOGD("set video width, height:[%d, %d]", videoWidth, videoHeight);
LOGD("set native window");12. 向解碼器發送幀數據與解碼器接收幀數據
while (av_read_frame(formatContext, packet) == 0) {
if (packet->stream_index == video_stream_index) {
int sendPacketState = avcodec_send_packet(codecContext, packet);
if (sendPacketState == 0) {
LOGD("向解碼器-發送數據");
int receiveFrameState = avcodec_receive_frame(codecContext, frame);
if (receiveFrameState == 0) {
LOGD("從解碼器-接收數據");
frameCount++; // 成功解碼一幀,計數器遞增
if (frameCount == 5) {
// 提取第100幀生成Bitmap
convertFrameToBitmap(env, codecContext, frame, bitmap); // 自定義函數
}
ANativeWindow_lock(nativeWindow, &windowBuffer, NULL);
// 格式轉換
sws_scale(swsContext, (uint8_tconst *const *) frame->data,
frame->linesize, 0, codecContext->height,
renderFrame->data, renderFrame->linesize);
//獲取stride
uint8_t *dst = (uint8_t *) windowBuffer.bits;
uint8_t *src = (uint8_t *) renderFrame->data[0];
int dstStride = windowBuffer.stride * 4;
int srcStride = renderFrame->linesize[0];
// 由于Windows的stride和幀的stride不同,因此需要逐行復制
for (int i = 0; i < videoHeight; i++) {
memcpy(dst + i * dstStride, src + i * srcStride, srcStride);
}
ANativeWindow_unlockAndPost(nativeWindow);
} elseif (receiveFrameState == AVERROR(EAGAIN)) {
LOGD("從解碼器-接收-數據失敗:AVERROR(EAGAIN)");
} elseif (receiveFrameState == AVERROR_EOF) {
LOGD("從解碼器-接收-數據失敗:AVERROR_EOF");
} elseif (receiveFrameState == AVERROR(EINVAL)) {
LOGD("從解碼器-接收-數據失敗:AVERROR(EINVAL)");
} else {
LOGD("從解碼器-接收-數據失敗: 未知");
}
} elseif (sendPacketState == AVERROR(EAGAIN)) {
LOGD("向解碼器-發送-數據失敗:AVERROR(EAGAIN)");
} elseif (sendPacketState == AVERROR_EOF) {
LOGD("向解碼器-發送-數據失敗:AVERROR_EOF");
} elseif (sendPacketState == AVERROR(EINVAL)) {
LOGD("向解碼器-發送-數據失敗:AVERROR(EINVAL)");
} elseif (sendPacketState == AVERROR(ENOMEM)) {
LOGD("向解碼器-發送-數據失敗:AVERROR(ENOMEM)");
} else {
LOGD("向解碼器-發送-數據失敗:未知");
}
}
av_packet_unref(packet);
}動畫在sws_scale處完成,大致代碼如下:
// 格式轉換(原有邏輯)
sws_scale(swsContext, frame->data, frame->linesize, 0,
codecContext->height, renderFrame->data, renderFrame->linesize);
// 將renderFrame數據綁定到OpenGL紋理
glBindTexture(GL_TEXTURE_2D, mTextureID);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, renderFrame->width, renderFrame->height,
GL_RGBA, GL_UNSIGNED_BYTE, renderFrame->data[0]);
// 更新動畫參數(示例:每幀放大1%,旋轉1度)
mCurrentScale += 0.01f;
mCurrentRotation += 1.0f;
if (mCurrentRotation >= 360.0f) mCurrentRotation = 0.0f;
// 渲染到屏幕
glUseProgram(mProgram);
glUniform1f(mScaleUniform, mCurrentScale); // 傳遞縮放值
glUniform1f(mRotationUniform, mCurrentRotation); // 傳遞旋轉角度
// 繪制矩形(帶紋理)
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);可以看到,跟第二章部分內容一樣,這里也使用了GL環境進行縮放和旋轉動畫的處理。代碼的實現思路也基本是一致的,就是Surface承接渲染任務,然后使用頂點和片元著色器進行圖形的繪制和渲染。
13. 內存釋放
// 內存釋放
LOGD("release memory");
ANativeWindow_release(nativeWindow);4.3酷炫動畫的實現
先來看一下一個簡單的處理,把rgb做了一個簡單的均值,然后賦值給rgb都賦值為這個均值,就可以得到一個黑白的顏色,這就是最簡單的視頻處理。
const GLchar* VideoDrawer::GetFragmentShader(){
staticconst GLchar shader[] = "precision mediump float;\n"
"uniform sampler2D uTexture;\n"
"varying vec2 vCoordinate;\n"
"void main() {\n"
" vec4 color = texture2D(uTexture, vCoordinate);\n"
// " color.a = 0.5f;"
// " gl_FragColor = color;\n"
"float gray = (color.r + color.g + color.b)/3.0;\n"
"gl_FragColor = vec4(gray, gray, gray, 1.0);\n"
// " gl_FragColor = vec4(1, 1, 1, 1);\n"
"}";
return shader;
}關鍵是這一行 gl_FragColor = vec4(gray, gray, gray, 1.0)

再來看一個靈魂出竅的效果,這個就是類似抖音這種做的濾鏡,代碼會復雜些,但是原理基本沒啥區別。

4.4自己寫播放器的好處
看到這里,你可能會說使用Mediaplayer跟自己寫FFmpeg沒啥區別,這么麻煩干嘛,那下面再來詳細總結下FFmpeg的好處:
1. 格式支持更全面
FFmpeg 支持幾乎所有的音視頻格式(如 H.265/HEVC、VP9、FLAC、MKV、MOV 等),甚至冷門格式或損壞文件。
傳統播放器 依賴系統解碼器,可能無法播放未安裝解碼器的格式(如某些 4K 視頻或無損音頻)。
2. 解碼能力更強
FFmpeg 直接調用底層庫(如 libx264、libvpx),支持硬解碼、多線程解碼,流暢播放高碼率視頻。
傳統播放器 可能因解碼優化不足導致卡頓,尤其是播放高分辨率(如 4K/8K)或高幀率視頻時。
3. 高度自定義與靈活性
FFmpeg 播放器 支持通過命令行參數或腳本控制播放行為,例如:
調整播放速度:ffplay -vf "setpts=0.5\*PTS" input.mp4(2倍速播放)
實時濾鏡:添加去噪、銳化、色彩校正等效果。
截取片段:ffplay -ss 00:01:30 -t 10 input.mp4(從1分30秒開始播放10秒)。
傳統播放器 通常僅提供固定功能,無法深度自定義。
4. 處理異常文件更穩定
FFmpeg 可強制忽略錯誤繼續播放不完整或損壞的媒體文件(如未下載完的視頻)。
ffplay -err\_detect ignore\_err input\_corrupted.mp4
傳統播放器 遇到文件異常時可能直接報錯退出。
5. 資源占用更低
FFmpeg 無圖形界面(如 ffplay),資源消耗更少,適合老舊設備或后臺處理。
傳統播放器 因GUI和附加功能(如皮膚、插件)可能占用更多內存和CPU。
6. 跨平臺一致性
FFmpeg 可在 Windows、Linux、macOS 等系統上運行,命令和功能完全一致。
傳統播放器 通常僅限特定平臺(如 Windows Media Player 僅限 Windows)。
7. 支持流媒體與網絡協議
FFmpeg 可直接播放網絡流(如 RTMP、HLS、HTTP):
ffplay rtsp://example.com/live.stream
傳統播放器 可能需要額外插件或無法支持專業流媒體協議。
8. 開發與調試友好
FFmpeg 提供詳細的日志和調試信息,便于開發者分析問題:
ffplay -v debug input.mp4 # 輸出詳細解碼日志
傳統播放器 日志功能有限,難以排查播放故障。
適用場景對比

05、可以做的更多
上面的動畫還是太簡單了!!!
要是需要做一個更復雜的動效:具備3D效果的視頻該怎么辦呢?比如百度地圖的3D圖層。

看一下下面這個知識架構圖,我們本文主要是把Core這部分做了講解,其他的知識點就是做3D效果的必備知識點,大家可以自行deepseek做進一步的了解。
├── Core
│ ├── Shader(著色器管理)
│ ├── Texture(紋理加載與采樣)
│ ├── Model(模型加載,支持OBJ/FBX)
│ └── Camera(攝像機控制)
├── Rendering
│ ├── ForwardRenderer(前向渲染器)
│ ├── DeferredRenderer(延遲渲染器)
│ └── ShadowRenderer(陰影渲染模塊)
├── Lighting
│ ├── PointLight(點光源)
│ ├── DirectionalLight(平行光)
│ └── PBR(基于物理的渲染)
└── Utils
├── GLM(數學庫)
├── Assimp(模型導入庫)
└── STB(圖像加載庫)上述知識點都掌握后,基本就可以實現3D地圖效果了,這時候再去做視頻的3D動畫原理也是相同,不再有阻礙了!
06結束語
使用視頻文件代替GIF、屬性動畫進行動效實現而言具備下面幾個明顯的優勢:
1. 復雜性限制
動效方案通常更適合簡單或中等復雜程度的動畫,而不是像視頻那樣可以展示復雜場景和高質量的畫面。
2.多樣性和沉浸感
視頻可以提供更豐富的視覺效果和沉浸感,比如動態的場景變化、特效、音效的結合等。
3. 創作靈活性
視頻創作可以使用各種視頻編輯工具進行高級編輯,而動效需要更多手動編碼和調整。
4. 更加滿足業務場景需求
在視頻文件的基礎上,可以進行動效定制,插入特定的效果,翻轉、平移、3D、摳圖等均可,可以做到更高的業務場景契合度。




























