精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

日本中文字幕影院| 国产在线a不卡| 中文字幕影片免费在线观看| 欧美一级鲁丝片| 国产网站一区二区| 成人免费在线视频网站| 日韩精品久久久久久久| jizzjizz欧美69巨大| 欧美一区二区三区在线| 精品少妇一区二区三区在线| 北岛玲日韩精品一区二区三区| 精品一区二区三区在线观看国产| 久久男人资源视频| wwwww黄色| 国内精品麻豆美女在线播放视频| 在线观看av一区二区| 白白操在线视频| 国产日本在线视频| 成人国产精品免费观看动漫| 国产精品一久久香蕉国产线看观看| 18岁成人毛片| 成人羞羞视频在线看网址| 亚洲成人激情在线观看| 天天爽夜夜爽一区二区三区| 国产盗摄一区二区| 国产精品色在线观看| 精品乱色一区二区中文字幕| 国产精品美女一区| 爽好多水快深点欧美视频| 欧美不卡视频一区发布| 97在线观看免费视频| 国产精品乱战久久久| 欧美男生操女生| 欧美日韩国产精品激情在线播放| 最新国产露脸在线观看| 国产欧美日韩不卡免费| 久久综合九色欧美狠狠| 亚洲欧美另类日韩| 国产美女精品一区二区三区| 国产精品久久久久久久午夜| 六月丁香激情综合| 在线观看不卡| 美日韩精品免费视频| 麻豆视频免费在线播放| 色综合中文网| 欧美视频精品在线| 久草青青在线观看| 国产传媒在线| 香蕉成人啪国产精品视频综合网| 麻豆一区二区三区在线观看| 91精品国产综合久久久久久豆腐| 久久―日本道色综合久久| 国产99在线播放| 中文字幕人成人乱码亚洲电影| 久久资源在线| 国产精品96久久久久久| 亚洲黄网在线观看| 天堂在线亚洲视频| 国产97在线|日韩| 毛片在线免费播放| 日本欧美一区二区| 国产精品香蕉国产| 亚洲综合一区中| 精品一区二区三区av| 国产一区二中文字幕在线看| 日本中文字幕在线观看视频| 奇米精品一区二区三区在线观看一| 国产精品久久久av| 中文字幕在线观看免费| 久久99精品国产| 91久久久久久久一区二区| 国产精品热久久| 激情图片小说一区| 98国产高清一区| 91香蕉一区二区三区在线观看| 九九热精品视频在线观看| 国产精品视屏| 欧美一二区视频| 欧美人与性动交α欧美精品| 日本在线一区二区三区| 精品人伦一区二区色婷婷| 91九色蝌蚪porny| 欧美人妖在线观看| 亚洲欧美在线磁力| av黄色免费在线观看| 亚洲色图网站| 国内免费久久久久久久久久久 | 99久久婷婷国产综合精品电影| 精品午夜一区二区三区| h视频在线播放| 亚洲人成在线播放网站岛国| 无码人妻少妇伦在线电影| 欧美黑人巨大xxxxx| 欧美日韩国产首页在线观看| 师生出轨h灌满了1v1| 神马香蕉久久| 日韩有码在线播放| 99热在线观看免费精品| 老司机精品视频导航| 国产精品手机在线| 91高清在线视频| 亚洲国产aⅴ天堂久久| 黄色片在线免费| 亚洲开心激情| 亚洲午夜av电影| 免费在线一级片| 久久久成人网| 产国精品偷在线| 成人亚洲性情网站www在线观看| 一区二区三区在线视频播放| 久久久久人妻精品一区三寸| 国产日韩欧美中文在线| 亚洲另类图片色| www日韩在线| 久久亚洲欧美| 国产精品三区在线| 久操视频在线播放| 欧美性69xxxx肥| 亚洲av午夜精品一区二区三区| 国语产色综合| 午夜精品一区二区三区在线| 在线免费观看高清视频| 91免费小视频| 99久久免费观看| 色综合.com| 伊人久久五月天| 五月天婷婷久久| 成人自拍视频在线观看| 午夜在线视频免费观看| abab456成人免费网址| 日韩精品亚洲元码| 日产精品久久久久| 国产精品1区2区3区在线观看| 天天爽天天狠久久久| 性欧美又大又长又硬| 精品美女一区二区三区| 欧美黑人性猛交xxx| 免费av成人在线| 欧美黑人3p| 亚洲精品中文字幕| 日韩精品中文字幕有码专区| 久草精品视频在线观看| 丰满白嫩尤物一区二区| 男女爱爱视频网站| 只有精品亚洲| 精品国产拍在线观看| 亚洲特级黄色片| 国产精品久久久久一区二区三区共| 一本久道中文无码字幕av| 亚洲成人一品| 日本精品一区二区三区在线播放视频| 色婷婷中文字幕| 亚洲福中文字幕伊人影院| 不许穿内裤随时挨c调教h苏绵| 中文字幕一区二区三区在线视频| 成人免费观看网址| 成人在线观看免费网站| 在线播放中文字幕一区| 亚洲综合网在线| 粉嫩av一区二区三区| 日韩一区二区高清视频| 黄色成人美女网站| 欧美一区二粉嫩精品国产一线天| 无码国产精品96久久久久| 欧美日韩国产色视频| 国产三级视频网站| 久久久精品日韩| 在线精品亚洲一区二区| 国产精品亚洲欧美一级在线| 欧美激情日韩图片| 天堂在线视频网站| 91福利国产精品| 中文乱码字幕高清一区二区| 国产激情视频一区二区三区欧美| 国产精品69久久久| 在线日韩一区| 国产日韩在线看| 欧美人体视频xxxxx| 亚洲国产高清福利视频| 一级黄色av片| 自拍偷拍亚洲综合| fc2成人免费视频| 久久精品道一区二区三区| 亚洲国产日韩欧美| 日韩精品一区国产| 57pao成人永久免费视频| 国产一级片在线| 制服.丝袜.亚洲.中文.综合| 日本三级2019| 欧美韩国一区二区| 少妇欧美激情一区二区三区| 日韩午夜一区| 亚洲视频在线观看日本a| 亚洲视频一起| 国产精品久久久久久久电影| 羞羞的网站在线观看| 日韩精品在线视频观看| 国产乱人乱偷精品视频a人人澡| 亚洲一本大道在线| 538精品视频| 粉嫩绯色av一区二区在线观看| 成人三级视频在线播放| 综合激情视频| 欧美日韩亚洲一区二区三区四区| 精品国产乱码久久久久久樱花| 国内精品视频一区| 久操视频在线播放| 亚洲人成网站777色婷婷| 国产丝袜在线视频| 色哟哟在线观看一区二区三区| h色网站在线观看| 91免费国产在线观看| 日韩精品视频网址| 日韩精品五月天| 免费国产黄色网址| 亚洲影视一区| 欧美日韩日本网| silk一区二区三区精品视频| 国产精品亚洲网站| 亚洲伦乱视频| 久久久久久尹人网香蕉| av在线免费观看网| 亚洲摸下面视频| 日本美女一级视频| 欧美一区二区免费视频| 一二三区免费视频| 午夜影院久久久| 欧美成人手机视频| 亚洲色图欧洲色图| eeuss中文字幕| 久久这里只有精品首页| 人妻换人妻a片爽麻豆| 国精产品一区一区三区mba视频| 蜜臀视频一区二区三区| 亚洲国产第一| 在线观看av的网址| 午夜久久免费观看| 亚洲在线视频一区二区| 精品国产不卡| 久久香蕉综合色| 国产精品chinese在线观看| 国产专区精品视频| 日本电影久久久| 国产主播欧美精品| 亚洲国产精选| 91精品国产综合久久香蕉的用户体验 | 一级特黄妇女高潮| 91亚洲自偷观看高清| 亚洲三级一区| 999视频精品| 中文字幕人成一区| 97精品国产| 欧美 日韩 国产 在线观看| 91欧美日韩| www.-级毛片线天内射视视| 99久久综合| 性生活免费观看视频| 欧美成人日本| 轻点好疼好大好爽视频| 精品av久久久久电影| 欧美日韩福利在线| 亚洲视频播放| 久久久精品免费免费| 波多野结衣在线免费观看| 久久精品久久综合| 亚洲精品mv在线观看| 国产成人午夜视频| 日本护士做爰视频| 国产性色一区二区| 国产美女高潮视频| 一级做a爱片久久| 日韩黄色a级片| 一道本成人在线| 一区二区三区在线免费观看视频| 欧美老年两性高潮| 亚洲黄色小说网址| 日韩精品在线播放| 午夜看片在线免费| 欧美大片网站在线观看| 色吧亚洲日本| 国产精品第一视频| 久久久久久久久成人| 亚洲一区亚洲二区亚洲三区| 91成人入口| 日本不卡在线观看| 欧美影院一区| 久久久久人妻精品一区三寸| 久久99国产乱子伦精品免费| www.555国产精品免费| 久久久国产精品麻豆| 看黄色录像一级片| 亚洲成人免费在线| 91麻豆精品在线| 欧美一级高清片| 日韩亚洲视频在线观看| 日韩在线观看免费全| bbw在线视频| 国产精品偷伦免费视频观看的| 试看120秒一区二区三区| 蜜桃麻豆91| 中文在线日韩| 男人亚洲天堂网| 国产精品一区不卡| 免费看黄色的视频| 亚洲免费观看高清完整| 亚洲天堂av片| 精品久久久久一区| 日本中文字幕在线看| 91精品国产免费久久久久久| 欧美一级在线| 麻豆91av| 精品91视频| caoporm在线视频| 91麻豆swag| 久草视频免费在线播放| 欧美三日本三级三级在线播放| 欧美 日韩 国产 在线| 精品国偷自产在线视频| 在线日韩影院| 国产美女在线精品免费观看| 国产精品久久久久久| 男人日女人bb视频| 国产不卡免费视频| √天堂中文官网8在线| 在线视频你懂得一区二区三区| 黄色小视频免费在线观看| xxxxxxxxx欧美| 91精品国产66| 欧美激情视频一区二区三区| 黄色av成人| 国产性生活一级片| 国产精品成人免费在线| 亚洲精品久久久久久久蜜桃| 亚洲精品按摩视频| 成人免费一区二区三区牛牛| 91在线网站视频| 色综合五月天| 国产九九热视频| 国产精品亲子乱子伦xxxx裸| 波多野结衣视频在线观看| 亚洲精品视频二区| 波多野结衣亚洲| 久久精品一二三区| 国产欧美精品久久| 亚洲视频在线播放免费| 婷婷中文字幕综合| 日韩在线视频免费| 久久久久久久久亚洲| 成人高潮视频| 日本熟妇人妻xxxx| 99视频在线精品| 少妇一级淫片免费放中国| 日韩av在线免费观看| aa国产成人| 久久99精品久久久水蜜桃| 99精品视频免费观看视频| 国产一卡二卡三卡四卡| 亚洲大尺度视频在线观看| 蜜臀av午夜精品| 欧美亚洲另类制服自拍| 制服丝袜日韩| 最新中文字幕免费视频| 国产精品卡一卡二卡三| 国产精品一二三四五区| 欧美乱大交xxxxx| 草草视频在线一区二区| 乱妇乱女熟妇熟女网站| 国产亚洲精品资源在线26u| 秋霞av一区二区三区| 色一区av在线| 精品三级久久久| 777av视频| 久久久欧美精品sm网站| 91在线你懂的| 久久久久亚洲精品| 久久99国产成人小视频| 亚洲最大成人在线观看| 亚洲欧美另类图片小说| 日韩有码第一页| 国产精品极品美女粉嫩高清在线| 99欧美视频| 亚洲熟女乱综合一区二区三区| 91成人国产精品| 制服丝袜在线播放| 久久国产精品99久久久久久丝袜| 日本欧美一区二区在线观看| 午夜写真片福利电影网| 亚洲精品成人av| 九九热这里有精品| 97视频久久久| 国产精品美日韩| 人成网站在线观看| 国产精品一区二区久久精品| 欧美日韩一区自拍| 亚洲区自拍偷拍| 精品国产一二三| 日本在线视频一区二区| 欧美国产视频一区| 国产精品视频在线看| 日韩永久免费视频|