精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

移動開發 Android
本文旨在提供一個詳細的指導,幫助開發人員掌握如何使用開源MediaPlayer或自定義播放器,并利用OpenGL ES來實現視頻動畫和濾鏡效果。

在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、摳圖等均可,可以做到更高的業務場景契合度。

責任編輯:龐桂玉 來源: vivo互聯網技術
相關推薦

2021-08-04 12:26:27

微軟Windows 10Windows

2021-03-04 06:14:03

CSS webkit-box-動效

2020-07-20 10:40:52

Linux命令Ubuntu

2025-03-11 08:30:00

Pythonretrying代碼

2020-01-03 10:50:16

Python編程語言Mac電腦

2021-07-01 10:03:55

Distroless容器安全

2024-12-12 16:38:44

2024-05-29 05:00:00

2022-04-12 07:37:08

CSS滾動視差效果前端

2025-07-14 06:20:00

Vue3前端動效組件庫

2025-05-13 08:20:00

Vue3前端動效組件庫

2012-04-20 12:42:21

2019-07-24 09:00:19

谷歌Android開發者

2012-05-09 12:25:55

2015-08-12 10:06:12

UI動效

2015-08-12 09:49:38

ui配合設計師

2017-07-18 16:00:09

炫酷動畫開源框架APP

2020-12-08 08:14:11

SQL注入數據庫

2023-07-03 07:55:25

2024-08-02 10:23:20

點贊
收藏

51CTO技術棧公眾號

国产综合自拍| jazzjazz国产精品麻豆| 成人免费在线观看入口| 国产精华一区| 日韩国产亚洲欧美| 亚洲精品97| 亚洲深夜福利视频| 青青草精品在线| 欧美性猛交xxx高清大费中文| 国产精品欧美一区喷水| 岛国视频一区| 中文字幕日产av| 1024精品一区二区三区| 精品国产视频在线| 免费a级黄色片| 欧美天堂一区| 欧美性xxxx在线播放| av不卡在线免费观看| 日本一卡二卡四卡精品| 国产精品69久久久久水密桃| 日本欧美黄网站| 国产真实的和子乱拍在线观看| 国内成人自拍| 亚洲精品动漫100p| 精品人妻无码中文字幕18禁| 成人国产精品一区二区免费麻豆 | 欧美欧美欧美欧美首页| 精品视频在线观看一区| 99热国产在线| 国产精品久久久久永久免费观看| 欧美二区三区| 人妻精品无码一区二区| 国产一区二区看久久| 国产精品三级在线| 男操女视频网站| 亚洲在线一区| 97在线观看视频国产| 精品欧美一区二区久久久久| 国产精品伦理久久久久久| 亚洲系列中文字幕| 日韩中文字幕电影| 午夜精品影视国产一区在线麻豆| 精品国产3级a| 日批免费观看视频| 日日夜夜精品视频| 51精品国自产在线| 天天摸天天舔天天操| 欧美天堂一区| 91精品国产综合久久小美女| 手机免费av片| 91国产精品| 7777精品伊人久久久大香线蕉最新版| 天美星空大象mv在线观看视频| 精品国产第一福利网站| 色综合 综合色| 可以在线看的黄色网址| 精品欧美一区二区三区在线观看 | 黄在线观看网站| 激情视频网站在线播放色| 午夜精品久久久久久久久久| 精品国产av无码一区二区三区| 国产啊啊啊视频在线观看| 亚洲香肠在线观看| 久久精品视频16| 欧美freesex| 欧美丝袜丝nylons| 亚洲18在线看污www麻豆| 蜜桃精品视频| 精品国精品自拍自在线| 一区二区视频观看| 国产精品视频一区二区三区四蜜臂| 亚洲人高潮女人毛茸茸| 国产在线免费av| 91成人精品视频| 久久久亚洲国产| 少妇一级淫片免费放中国| 欧美一级视频| 成人高清视频观看www| 国产深喉视频一区二区| 成人精品小蝌蚪| 欧美一区二区三区精美影视| 在线看的av网站| 亚洲综合清纯丝袜自拍| 久久无码高潮喷水| 日本久久一区| 亚洲护士老师的毛茸茸最新章节 | 亚洲男同1069视频| 精品视频免费在线播放| jizz欧美| 亚洲国产成人久久综合| 韩国三级hd中文字幕| 亚洲国产精品久久久天堂| 97久久精品人人澡人人爽缅北| 中文字幕黄色片| 国产精品影视在线| 欧美久久久久久一卡四| 影院在线观看全集免费观看| 精品国产成人av| 欧美一级特黄a| 懂色av一区二区| 色悠悠久久88| 韩国av中文字幕| 国产一区在线不卡| 欧美日韩在线不卡一区| 天堂亚洲精品| 欧美日本一区二区三区四区| 老熟妇精品一区二区三区| 日韩夫妻性生活xx| 91av在线视频观看| h狠狠躁死你h高h| 国产欧美va欧美不卡在线| 妞干网在线观看视频| 国产精品麻豆成人av电影艾秋| 精品88久久久久88久久久| 成人欧美一区二区三区黑人一 | 日韩av片专区| 国产精品一线天粉嫩av| 欧美极品少妇全裸体| 亚洲一区二区视频在线播放| 久久嫩草精品久久久精品一| 97在线免费视频观看| 色综合视频一区二区三区日韩| 日韩电影中文 亚洲精品乱码| 尤物在线免费视频| 秋霞电影网一区二区| 久久人人九九| av人人综合网| 日韩视频永久免费| 操她视频在线观看| 日本一区中文字幕| 久久久久资源| 成人观看网址| 亚洲黄色www网站| 欧美高清视频一区二区三区| 久久99精品久久久久久| 亚洲精品高清视频| av免费在线一区| 国产一区二区三区久久精品 | 免费在线中文字幕| 91精品国产综合久久久久久漫画| sm捆绑调教视频| 麻豆成人免费电影| 亚洲免费视频一区| 国产91在线精品| 在线亚洲国产精品网| 波多野结衣午夜| 国产日产精品1区| 免费国产成人av| 精品久久影院| 国产精品黄页免费高清在线观看| 韩国福利在线| 欧美午夜精品久久久久久孕妇| av网站免费在线看| 蜜桃av一区二区三区电影| 亚洲国产午夜伦理片大全在线观看网站 | 成人黄色一级大片| 欧美一区在线看| 国产成人av一区二区三区| 韩国日本一区| 亚洲激情自拍图| 国产精品久久久久久久久久久久久久久久久 | 中文字幕在线观看视频www| 国产精品不卡| 91九色在线观看| 欧美videosex性欧美黑吊| 精品剧情在线观看| 九九热在线视频播放| 国产亚洲欧美中文| 欧美视频国产视频| 禁久久精品乱码| 蜜桃精品久久久久久久免费影院 | 国产99在线免费| 牛牛精品一区二区| 中文字幕免费精品一区高清| 国产精品欧美亚洲| 一区二区高清免费观看影视大全| 日本一级大毛片a一| 久久aⅴ国产紧身牛仔裤| 一本色道婷婷久久欧美 | 精品国产乱码久久久久久1区2区 | 黄色a级片在线观看| 国产白丝精品91爽爽久久| 红桃av在线播放| 我不卡影院28| 免费成人av网站| 亚洲高清影院| 8050国产精品久久久久久| av福利精品| 欧美精品一区二区在线观看| 国产性生活视频| 亚洲精品国产a| 中国黄色a级片| 国产一区二区三区黄视频| 国产91在线免费| 性欧美69xoxoxoxo| 久久久久成人精品免费播放动漫| 外国成人毛片| 欧美一级淫片aaaaaaa视频| 毛片在线播放a| 日韩美女av在线| 精品国产av 无码一区二区三区| 偷窥少妇高潮呻吟av久久免费| 波多野结衣一二三四区| 成+人+亚洲+综合天堂| 在线观看免费视频高清游戏推荐| 亚洲激情一区| 97超碰免费观看| 精品国产精品国产偷麻豆| 99久久99久久| 性欧美video另类hd尤物| 2019最新中文字幕| 国精一区二区三区| www亚洲精品| 国产区av在线| 日韩成人在线视频网站| 国内精品久久久久久久久久久| 欧美三级电影一区| 久久国产黄色片| 亚洲自拍与偷拍| 日本黄色片免费观看| 国产日韩影视精品| 国产成人无码一区二区在线观看| 国产乱码精品一区二区三| 亚洲免费999| 免费高清在线一区| 日韩欧美xxxx| 久久久精品性| 国产精品沙发午睡系列| 亚洲精品韩国| 日韩五码在线观看| 精品999日本| 成人国产一区二区三区| 欧美一区视频| 日本福利视频网站| 欧美91大片| 大胆欧美熟妇xx| 亚洲手机在线| 久久人人爽人人爽人人av| 午夜精品久久久久99热蜜桃导演 | 日韩av在线直播| 四虎永久在线精品免费网址| 亚洲成人久久网| 少妇又色又爽又黄的视频| 日韩一区二区免费在线观看| 国产不卡精品视频| 日韩一区二区高清| www.久久精品.com| 精品久久久久久最新网址| www.我爱av| 亚洲精品一区二区三区99| 人妻无码中文字幕| 日韩黄色在线免费观看| 黄色av网址在线免费观看| 亚洲视频日韩精品| √天堂资源地址在线官网| 中文字幕亚洲一区二区三区五十路| fc2在线中文字幕| 色狠狠久久aa北条麻妃| 激情在线小视频| 欧美激情欧美激情| 国产直播在线| 国产精品极品美女在线观看免费 | 欧美日韩高清不卡| 国产理论视频在线观看| 日韩欧美激情四射| 无码精品黑人一区二区三区| 亚洲人成网在线播放| 伊人免费在线| 欧美人成在线视频| 午夜激情在线播放| 国产精品丝袜视频| 视频一区在线| 乱色588欧美| 久久人体视频| 激情小视频网站| 肉肉av福利一精品导航| www.污网站| 99精品久久久久久| 蜜桃av免费观看| 一二三区精品福利视频| 亚洲免费黄色网址| 欧美精品久久99久久在免费线| h片在线免费看| 亚洲美女在线观看| 99在线播放| 国产97免费视| 美女精品久久| 欧美性天天影院| 欧美高清不卡| 成年人免费大片| 成人午夜碰碰视频| 91香蕉国产视频| 欧美日韩国产在线看| 国产精品亚洲欧美在线播放| 亚洲国产日韩欧美在线图片| 免费在线看a| 91a在线视频| 亚洲视频国产| 亚洲国产精品综合| 99riav1国产精品视频| 中文字幕第一页在线视频| 91在线观看视频| 国产黄色的视频| 欧洲激情一区二区| 无码国产精品高潮久久99| 色一情一乱一区二区| 成人午夜视屏| 国产另类自拍| 中文字幕午夜精品一区二区三区| 欧美黄色一级片视频| 成人动漫中文字幕| 欧美偷拍第一页| 精品视频一区二区不卡| 深夜福利在线视频| 久久国产精品偷| 成人全视频在线观看在线播放高清 | 伊人成人在线观看| 亚洲欧洲在线看| 99爱在线视频| 成人在线免费网站| 亚洲a在线视频| 亚洲高清免费在线观看| 久久色成人在线| 偷偷操不一样的久久| 亚洲第一精品久久忘忧草社区| 国产1区在线| 成人午夜在线影院| 日韩精品首页| 日本在线一二三区| 国产日韩欧美综合在线| 亚洲 欧美 中文字幕| 精品小视频在线| 国产在线观看www| 国产一区二区免费电影| 狠狠色狠狠色综合日日tαg| 国产精品91av| 亚洲一区二区四区蜜桃| 亚洲国产成人精品一区二区三区| 久久精品国产一区二区电影| 国产精品99久久久久久董美香| 日本视频一区二区不卡| 久久午夜精品| 性猛交ⅹxxx富婆video| 欧美自拍偷拍一区| 国产露出视频在线观看| 国产精品久久久久久久久久99| 欧美丝袜激情| 一区二区三区四区毛片| 亚洲欧美日韩一区二区三区在线观看| 国产一区二区在线视频聊天 | 亚洲电影网站| 免费成人在线观看视频| 婷婷国产成人精品视频| 制服丝袜成人动漫| 特级毛片在线| 国产精品日本一区二区| 亚洲欧美成人| 极品人妻videosss人妻| 欧美日韩精品三区| 成人短视频在线观看| 国产精品v欧美精品∨日韩| 国产欧美欧美| 日韩福利在线视频| 8v天堂国产在线一区二区| 欧美亚洲系列| 你懂的网址一区二区三区| 日韩精品电影一区亚洲| 成人一级黄色大片| 日韩欧美卡一卡二| 小视频免费在线观看| 视频二区一区| 国产精品亚洲专一区二区三区| 中文字幕第28页| 亚洲欧美国产精品久久久久久久| 成人黄色毛片| 麻豆传媒网站在线观看| 99国内精品久久| 一级片视频网站| 久久久欧美精品| 欧美偷拍自拍| 中国黄色片视频| 欧美在线观看视频一区二区三区| 秋霞a级毛片在线看| 国产精品一区二区av| 日av在线不卡| 国产精品999久久久| 伊人久久免费视频| 国产厕拍一区| av中文字幕网址| 欧美午夜影院在线视频| 成人看片免费| 日本福利一区二区三区| 国产成人一区二区精品非洲| 99精品人妻国产毛片| 欧美理论片在线观看| 奇米亚洲欧美| 中文字幕在线观看91| 欧美三级日韩在线| 999精品网| 成年人黄色在线观看|