Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜

cover.png

上文中我们已经实现了在纹理上添加滤镜的效果。这编文章就是将OpenGl和相机结合到一起。

预览与拍照


整体流程理解

预览的整体流程.png
  1. Camera中得到的ImageStreamSurfaceTexture接受,并转换成OpenGL ES纹理。
  2. 创建GLSurfaceView。在OpenGL环境下,用GLSurfaceView.Render将这个纹理绘制出来。
  3. 整体的ImageStream的流向就是
Camera ==>SurfaceTexture==>texture(samplerExternalOES) ==>draw to GLSurfaceView

各个部分详解

Camra Api

首先是相机的Api的书写。

Camera Interface

为我们相机的操作定义一个接口。因为我们的相机Api。有Camera2和Camera的区别。这里还是简单的使用Camera来完成。

/**
 * 定义个相机的功能接口
 */
public interface ICamera {
    boolean open(int cameraId);

    /**
     * 设置画面的比例
     */
    void setAspectRatio(AspectRatio aspectRatio);

    /**
     * 开启预览
     */
    boolean preview();

    /**
     * 关闭相机
     *
     * @return
     */
    boolean close();

    /**
     * 使用SurfaceTexture 来作为预览的画面
     *
     * @param surfaceTexture
     */
    void setPreviewTexture(SurfaceTexture surfaceTexture);

    CameraSize getPreviewSize();
    CameraSize getPictureSize();
}

定义一个相机的接口。我们知道。我们需要相机做的几个通常的操作。

CameraApi14
/**
 * for api 14
 * <p>
 * Camera主要涉及参数
 * 1. 预览画面的大小
 * 2. pic图片的大小
 * 3. 对焦模式
 * 4. 闪光灯模式
 */
public class CameraApi14 implements ICamera {
    /*
    当前的相机Id
     */
    private int mCameraId;
    /*
    当前的相机对象
     */
    private Camera mCamera;
    /*
    当前的相机参数
     */
    private Camera.Parameters mCameraParameters;


    //想要的尺寸。
    private int mDesiredHeight = 1920;
    private int mDesiredWidth = 1080;
    private boolean mAutoFocus;
    public CameraSize mPreviewSize;
    public CameraSize mPicSize;
    /*
     * 当前相机的高宽比
     */
    private AspectRatio mDesiredAspectRatio;


    public CameraApi14() {
        mDesiredHeight = 1920;
        mDesiredWidth = 1080;
        //创建默认的比例.因为后置摄像头的比例,默认的情况下,都是旋转了270
        mDesiredAspectRatio = AspectRatio.of(mDesiredWidth, mDesiredHeight).inverse();
    }

    @Override
    public boolean open(int cameraId) {
        /*
            预览的尺寸和照片的尺寸
        */
        final CameraSize.ISizeMap mPreviewSizes = new CameraSize.ISizeMap();
        final CameraSize.ISizeMap mPictureSizes = new CameraSize.ISizeMap();
        if (mCamera != null) {
            releaseCamera();
        }
        mCameraId = cameraId;
        mCamera = Camera.open(mCameraId);
        if (mCamera != null) {
            mCameraParameters = mCamera.getParameters();

            mPreviewSizes.clear();
            //先收集参数.因为每个手机能够得到的摄像头参数都不一致。所以将可能的尺寸都得到。
            for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
                mPreviewSizes.add(new CameraSize(size.width, size.height));
            }

            mPictureSizes.clear();
            for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
                mPictureSizes.add(new CameraSize(size.width, size.height));
            }
            //挑选出最需要的参数
            adJustParametersByAspectRatio2(mPreviewSizes, mPictureSizes);
            return true;
        }
        return false;
    }

    private void adJustParametersByAspectRatio(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
        //得到当前预期比例的size
        SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
        if (sizes == null) {  //表示不支持.
            // TODO: 2018/9/14 这里应该抛出异常?
            return;
        }
        //当前先不考虑Orientation
        CameraSize previewSize;
        mPreviewSize = new CameraSize(mDesiredWidth, mDesiredHeight);
        if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
            mPreviewSize = new CameraSize(mDesiredHeight, mDesiredWidth);
            mCameraParameters.setRotation(90);
        } else {
//            previewSize = mPreviewSize;
        }

        //默认去取最大的尺寸
        mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();

        mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());

        //设置对角和闪光灯
        setAutoFocusInternal(mAutoFocus);
        //先不设置闪光灯
//        mCameraParameters.setFlashMode("FLASH_MODE_OFF");

        //设置到camera中
//        mCameraParameters.setRotation(90);
        mCamera.setParameters(mCameraParameters);
//        mCamera.setDisplayOrientation(90);
//        setCameraDisplayOrientation();
    }

    private void adJustParametersByAspectRatio2(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
        //得到当前预期比例的size
        SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
        if (sizes == null) {  //表示不支持.
            // TODO: 2018/9/14 这里应该抛出异常?
            return;
        }
        //当前先不考虑Orientation
        mPreviewSize = sizes.first();
        //默认去取最大的尺寸
        mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();
        mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());

        mPreviewSize = mPreviewSize.inverse();
        mPicSize = mPicSize.inverse();
        //设置对角和闪光灯
        setAutoFocusInternal(mAutoFocus);
        //先不设置闪光灯
//        mCameraParameters.setFlashMode("FLASH_MODE_OFF");

        //设置到camera中
//        mCameraParameters.setRotation(90);
        mCamera.setParameters(mCameraParameters);
//        mCamera.setDisplayOrientation(90);
//        setCameraDisplayOrientation();
    }

    private boolean setAutoFocusInternal(boolean autoFocus) {
        mAutoFocus = autoFocus;
//        if (isCameraOpened()) {
        final List<String> modes = mCameraParameters.getSupportedFocusModes();
        if (autoFocus && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        } else if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
            mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
        } else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
            mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
        } else {
            mCameraParameters.setFocusMode(modes.get(0));
        }
        return true;
//        } else {
//            return false;
//        }
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.release();
            mCamera = null;
        }
    }

    @Override
    public void setAspectRatio(AspectRatio aspectRatio) {
        this.mDesiredAspectRatio = aspectRatio;
    }

    @Override
    public boolean preview() {
        if (mCamera != null) {
            mCamera.startPreview();
            return true;
        }
        return false;
    }

    @Override
    public boolean close() {
        if (mCamera != null) {
            try {
                //stop preview时,可能爆出异常
                mCamera.stopPreview();
                mCamera.release();
                mCamera = null;
                return true;
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        return false;
    }

    @Override
    public void setPreviewTexture(SurfaceTexture surfaceTexture) {
        if (mCamera != null) {
            try {
                mCamera.setPreviewTexture(surfaceTexture);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public CameraSize getPreviewSize() {
        return mPreviewSize;
    }

    @Override
    public CameraSize getPictureSize() {
        return mPicSize;
    }
}

这里的代码就是使用Camera来实现上面的功能。

  1. 因为使用我们期望将Camera中得到的数据传递到纹理上,所以需要setPreviewTexture(SurfaceTexture texture)。让这个SurfaceTexture来承载。
@Override
    public void setPreviewTexture(SurfaceTexture surfaceTexture) {
        if (mCamera != null) {
            try {
                mCamera.setPreviewTexture(surfaceTexture);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  1. 选择相机的预览的尺寸和旋转的角度
    相机的parameter的选择,只要选对了对应的想要的比例就行了。没有其他需要的。
    设备坐标和纹理坐标之间的方向不同问题,由后面纹理的矩阵来控制就好了。

SurfaceTexture

可以从图像流中捕获帧作为OpenGL ES纹理。

  1. 直接使用创建的纹理,来创建SurfaceTexture就可以了。
mSurfaceTexture = new SurfaceTexture(mTextureId);
  1. 然后再将其设置给Camera.同时每次SurfaceTexture刷新的时候,都必须刷新GLSurfaceView。
  mCameraApi.setPreviewTexture(mCameraDrawer.getSurfaceTexture());
  //默认使用的GLThread.每次刷新的时候,都强制要求是刷新这个GLSurfaceView
  mCameraDrawer.getSurfaceTexture().setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
            @Override
            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
                requestRender();
            }
        });
注意事项

使用时必须要注意的是

  1. 纹理对象使用GL_TEXTURE_EXTERNAL_OES纹理目标,该目标由GL_OES_EGL_image_externalOpenGL ES扩展定义。
    每次绑定纹理时,它必须绑定到GL_TEXTURE_EXTERNAL_OES目标而不是GL_TEXTURE_2D目标。在OpenGL ES 2.0着色器必须使用
#extension GL_OES_EGL_image_external:require
  1. 着色器还必须使用samplerExternalOES GLSL采样器类型访问纹理。
uniform samplerExternalOES uTexture;

GLSurfaceView.Render

GLSL部分
  • oes_base_vertex.glsl
attribute vec4 aPosition;
attribute vec2 aCoordinate;
uniform mat4 uMatrix;
uniform mat4 uCoordinateMatrix;
varying vec2 vTextureCoordinate;

void main(){
    gl_Position = uMatrix*aPosition;
    vTextureCoordinate = (uCoordinateMatrix*vec4(aCoordinate,0.1,0.1)).xy;
}

顶点着色器对比相对简单。这里需要注意的就是矩阵相乘的顺序问题。

//这个是正确的顺序。相称的顺序相反,图像是反的!!!
gl_Position = uMatrix*aPosition;
  • oes_base_fragment.glsl
    这里就是如上面注意事项中说的。必须使用samplerExternalOES来采样。
#extension GL_OES_EGL_image_external : require
precision mediump float;

varying vec2 vTextureCoordinate;
uniform samplerExternalOES uTexture;
void main() {
    gl_FragColor = texture2D(uTexture,vTextureCoordinate);
}
其他的部分

其他的部分和前几编文章中提到的相差不多。

0. 生成纹理

这里就是上面所说的。只能用GLES11Ext.GL_TEXTURE_EXTERNAL_OES这种纹理。

 private int genOesTextureId() {
        int[] textureObjectId = new int[1];
        GLES20.glGenTextures(1, textureObjectId, 0);
        //绑定纹理
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureObjectId[0]);
        //设置放大缩小。设置边缘测量
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
        return textureObjectId[0];
    }
1. 视图矩阵。

因为设备坐标和纹理的坐标不同。而前置摄像头和后置摄像头的翻转的方向也不同。所以要做下面的处理

   //计算需要变化的矩阵
    private void calculateMatrix() {
        //得到通用的显示的matrix
        Gl2Utils.getShowMatrix(mModelMatrix, mPreviewWidth, mPreviewHeight, this.mSurfaceWidth, this.mSurfaceHeight);

        if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {  //前置摄像头
            Gl2Utils.flip(mModelMatrix, true, false);
            Gl2Utils.rotate(mModelMatrix, 90);
        } else {  //后置摄像头
            int rotateAngle = 270;
            Gl2Utils.rotate(mModelMatrix, rotateAngle);
        }
        mOesFilter.setMatrix(mModelMatrix);
    }
  1. 得到标准的透视视图矩阵
 public static void getShowMatrix(float[] matrix,int imgWidth,int imgHeight,int viewWidth,int
        viewHeight){
        if(imgHeight>0&&imgWidth>0&&viewWidth>0&&viewHeight>0){
            float sWhView=(float)viewWidth/viewHeight;
            float sWhImg=(float)imgWidth/imgHeight;
            float[] projection=new float[16];
            float[] camera=new float[16];
            if(sWhImg>sWhView){
                Matrix.orthoM(projection,0,-sWhView/sWhImg,sWhView/sWhImg,-1,1,1,3);
            }else{
                Matrix.orthoM(projection,0,-1,1,-sWhImg/sWhView,sWhImg/sWhView,1,3);
            }
            Matrix.setLookAtM(camera,0,0,0,1,0,0,0,0,1,0);
            Matrix.multiplyMM(matrix,0,projection,0,camera,0);
        }
    }

这部分就是标准的处理方式了。谁的比例大,用谁的。

  1. 处理不同摄像头的旋转
    如果是前置摄像头的话,需要进行左右的翻转。然后旋转90度。
    后置摄像头的话,只需要旋转270度就可以了。
2. 绘制图形

重温一下绘制整体的流程

//draw step
public void draw() {
   //step0 clear
   onClear();
   //step1 use program
   onUseProgram();
   //step2 active and bind custom data
   onSetExpandData();
   //step3 bind texture
   onBindTexture();
   //step4 normal draw
   onDraw();
 }
  • onClear
   GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
   GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

这里有一个疑问。这里其实不用每次都清除的。只要初始化清除就可以了吧?

  • onUseProgram
    这一步,就是使用我们之前已经创建和link好的program
 private void onUseProgram() {
        GLES20.glUseProgram(mProgram);
    }
  • onSetExpandData
    这里做的其实就是我们额外给glsl添加的属性。应用上面我们变化的矩阵。
 private void onSetExpandData() {
        GLES20.glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
        GLES20.glUniformMatrix4fv(mUCoordMatrix, 1, false, mCoordMatrix, 0);
 }
  • onBindTexture
    接着就是激活和绑定纹理数据.
 private void onBindTexture() {
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + getTextureType());
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, getTextureId());
        GLES20.glUniform1i(mUTexture, getTextureType());
    }

我们的纹理都挂载在GLES20.GL_TEXTURE0 + getTextureType()上。同时一定要注意的是,相机这里需要的是GLES11Ext.GL_TEXTURE_EXTERNAL_OES这种拓展类型的纹理采样。

  • onDraw
    绘制图像的话,同之前相同,只需要绘制一个长方形就可以了。
 private void onDraw() {
        //设置定点数据
        GLES20.glEnableVertexAttribArray(mAPosition);
        GLES20.glVertexAttribPointer(
                mAPosition,
                2,
                GLES20.GL_FLOAT,
                false,
                0,
                mVerBuffer);
        //
        GLES20.glEnableVertexAttribArray(mACoord);
        GLES20.glVertexAttribPointer(
                mACoord,
                2,
                GLES20.GL_FLOAT,
                false,
                0,
                mTextureCoordinate);
        //绘制三角形带
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        GLES20.glDisableVertexAttribArray(mAPosition);
        GLES20.glDisableVertexAttribArray(mACoord);
    }

GLSurfaceView

最后在GLSurfaceView的对应的生命周期内调用方法就可以了~~

录制


整体流程理解

录制的整体流程.png

在原来预览的基础上,我们需要加入MediaCodec进行视频编码。

  1. 图中的EglCore着保存EglContextEglSurfaceEglConfig的配置。WindowSurface就是将EglContextSurface相互关联的帮助类。

  2. Encoder是在EncoderThread中进行。两个线程(和原来的GLTHread)需要共享EGLContext

  3. 使用MediaCodec进行视频编码,只要通过它的InputSurface,将数据输入就可以。所以通过共享的EGLContext,来创建一个WindowSurface。然后再通过在该线程内GL的draw方法,就可以将EGLContext中的Oes纹理,绘制到Surface上。这样MediaCodec就可以得到数据。

  4. 整体流向
    当我们接受到frame时,我们需要

  • GLSurfaceView的渲染线程,将数据渲染到SurfaceView
  • Encoder的线程,将frame渲染到MediacodecInputSurface
Camera==>
  SurfaceTexture.onFrameAvailable==>
  GLSurfaceView.requestRender ==>
  {
    //通知更新下一帧
    mSurfaceTexture.updateTexImage()
    //在`Encoder`的线程,将`frame`渲染到`Mediacodec`的`InputSurface`中。通知编码器线程绘制并编码
    mVideoEncoder.frameAvailable(mSurfaceTexture) ==>
      {
        //通知编码器进行编码
        mVideoEncoder.drainEncoder(false);
        //刷入数据
        mFullScreen.drawFrame(mTextureId, transform);
        //给InputWindow设置时间戳
        mInputWindowSurface.setPresentationTime(timestampNanos);
        //刷新之后,编码器得到数据?
        mInputWindowSurface.swapBuffers();
      }
    //同时Render绘制到屏幕上。在`GLSurfaceView`的渲染线程,将数据渲染到`SurfaceView`
    mOesFilter.draw();
  }

各个部分详解

TextureMovieEncoder

主要还是添加了一个这个类。

理想状态下,我们创建Video Encoder,然后为它创建EGLContext,然后将这个context传入GLSurfaceView来共享。 但是这里的Api做不到这样,所以我们只能反着来。当GLSurfaceView torn down时,(可能时我们旋转了屏幕),EGLContext也会同样被抛弃。这样意味这当它回来的时候,我们就需要重新为Video encoder创建EGLContext.(而且,"preserve EGLContext on pause" 这样的功能,也不启作用。就是上一个暂停状态的EGLContext,在这里也不能用)我们可以通过使用TextureView 来替代GLSurfaceView来做一些简化。但是这样会由一点性能的问题。

创建EGL环境
获取EGLContext

可以直接在GLThread中通过EGL14.eglGetCurrentContext(),就可以得到和线程绑定的EGLContext(EGLContext其实也是存在于ThreadLocal当中)

 private void startRecord() {
        mOutputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "camera-test" + System.currentTimeMillis() + ".mp4");
        Log.d(TAG, "file path = " + mOutputFile.getAbsolutePath());
        // start recording
        mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
                mOutputFile, mPreviewHeight, mPreviewWidth, 1000000, EGL14.eglGetCurrentContext()));
        mRecordingStatus = RECORDING_ON;
    }
线程的通信是通过Handler来完成。
public void startRecording(EncoderConfig config) {
        Log.d(TAG, "Encoder: startRecording()");
        //mReadyFence 这个锁是来锁这个线程的所有操作的。包括开始。停止。绘制。
        synchronized (mReadyFence) {
            if (mRunning) {
                Log.w(TAG, "Encoder thread already running");
                return;
            }
            mRunning = true;
            new Thread(this, "TextureMovieEncoder").start();
            while (!mReady) {
                try {
                    mReadyFence.wait();
                } catch (InterruptedException ie) {
                    // ignore
                }
            }
        }

        mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
    }
创建WindowSurface。将EGLContextEncoder.InputSurface关联在一起
 private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate,
                                File outputFile) {
        try {
            //这个就算MediaCodec的封装。包括MediaCodec进行编码。MediaMuxer进行视频封装
            mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
        //通过EglContext创建EglCore
        mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
        //创建inputWindowSurface
        mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
        //在完成EGL的初始化之后,需要通过eglMakeCurrent()函数来将当前的上下文切换,这样opengl的函数才能启动作用。
        mInputWindowSurface.makeCurrent();

        mFullScreen = new FullFrameRect(
                new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
    }

这里进行了一系列的初始化工作。

  • 初始化了VideoEncoderCore。它是MediaCodecMediaMuxer的封装。
public VideoEncoderCore(int width, int height, int bitRate, File outputFile)
            throws IOException {
        //MediaCodec的BufferInfo的缓存。通过这个BufferInfo不断的运输数据。(原始=>编码后的)
        mBufferInfo = new MediaCodec.BufferInfo();
        //创建MediaFormat MIME_TYPE = "video/avc"
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);

        //设置我们想要的参数。如果参数不合法的话,在configure时,就会报错的
        //这个ColorFormat很重要,这里一定要设置COLOR_FormatSurface
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        //这里的设置5 seconds 在相邻的 I-frames,why?
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        if (VERBOSE) Log.d(TAG, "format: " + format);

        //创建编码器
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //得到对应InputSureface
        mInputSurface = mEncoder.createInputSurface();
        //启动
        mEncoder.start();

        //创建MediaMuxer。我们不能直接在这里开始muxer.因为MediaFormat 还没得到输入。必须要在编码器得到输入之后,才能添加。这里先不添加音频。
        mMuxer = new MediaMuxer(outputFile.toString(),
                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        mTrackIndex = -1;
        mMuxerStarted = false;
    }
  • 初始化了EglCore。主要是管理EGL的state,包括 (display, context, config)。
  //主要是初始化display 和EglContext
  public EglCore(EGLContext sharedContext, int flags) {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("EGL already set up");
        }

        if (sharedContext == null) {
            sharedContext = EGL14.EGL_NO_CONTEXT;
        }
        //先创建一个默认的Display
        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("unable to get EGL14 display");
        }
        //检查是否创建成功
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            mEGLDisplay = null;
            throw new RuntimeException("unable to initialize EGL14");
        }

        // Try to get a GLES3 context, if requested.
        if ((flags & FLAG_TRY_GLES3) != 0) {
            //Log.d(TAG, "Trying GLES 3");
            EGLConfig config = getConfig(flags, 3);
            if (config != null) {
                int[] attrib3_list = {
                        EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
                        EGL14.EGL_NONE
                };
                EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
                        attrib3_list, 0);

                if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
                    //Log.d(TAG, "Got GLES 3 config");
                    mEGLConfig = config;
                    mEGLContext = context;
                    mGlVersion = 3;
                }
            }
        }
        if (mEGLContext == EGL14.EGL_NO_CONTEXT) {  // GLES 2 only, or GLES 3 attempt failed
            //Log.d(TAG, "Trying GLES 2");
            //获取GL的Config
            EGLConfig config = getConfig(flags, 2);
            if (config == null) {
                throw new RuntimeException("Unable to find a suitable EGLConfig");
            }
            int[] attrib2_list = {
                    EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                    EGL14.EGL_NONE
            };
            //获取EGLContext关键方法就是它,EGL14.eglCreateContext
            EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
                    attrib2_list, 0);
            checkEglError("eglCreateContext");
            mEGLConfig = config;
            mEGLContext = context;
            mGlVersion = 2;
        }

        // Confirm with query.
        int[] values = new int[1];
        EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
                values, 0);
        Log.d(TAG, "EGLContext created, client version " + values[0]);
    }
    
    //EGL的配置。类似键值对的数组。
   private EGLConfig getConfig(int flags, int version) {
        int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
        if (version >= 3) {
            renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
        }

        // The actual surface is generally RGBA or RGBX, so situationally omitting alpha
        // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()
        // when reading into a GL_RGBA buffer.
        int[] attribList = {
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                //EGL14.EGL_DEPTH_SIZE, 16,
                //EGL14.EGL_STENCIL_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, renderableType,
                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]
                EGL14.EGL_NONE
        };
        if ((flags & FLAG_RECORDABLE) != 0) {
            attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
            attribList[attribList.length - 2] = 1;
        }
        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
                numConfigs, 0)) {
            Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
            return null;
        }
        return configs[0];
    }
  • 初始化了WindowSurface。并通过eglMakeCurrent()函数,切换到当前的上下文。
    /**
     * 将EGL和原生的window surface关联在一起
     * 如果传入releaseSurface为true的话,当你调用release方法时,这个Surface就会自动被release。
     * 但时如果是使用了SurfaceView的Surface等,Android框架创建的Surface时需要注意,
     * 它会干涉原生框架的调用,比如上述的SurfaceView的Surface,release之后,surfaceDestroyed()回调将不会再收到
     */
    public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
        super(eglCore);
        createWindowSurface(surface);
        mSurface = surface;
        mReleaseSurface = releaseSurface;
    }
    /**
     * 创建 window surface.我们的之前的信息提前就保存再EglCore内了
     * <p>
     * @param surface May be a Surface or SurfaceTexture.
     */
    public void createWindowSurface(Object surface) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglCore.createWindowSurface(surface);
    }

    /**
     * 如果我们是为了MediaCodec创建,那么EGLConfig需要有"recordable"的attribute.这个部分,在上面初始化EglCore时,已经完成了EGLConfig和EGLDisplay的配置
     */
    public EGLSurface createWindowSurface(Object surface) {
        if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
            throw new RuntimeException("invalid surface: " + surface);
        }

        // Create a window surface, and attach it to the Surface we received.
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        //这就是我们想要的EGLSurface的创建方式  EGL14.eglCreateWindowSurface
        EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
                surfaceAttribs, 0);
        checkEglError("eglCreateWindowSurface");
        if (eglSurface == null) {
            throw new RuntimeException("surface was null");
        }
        return eglSurface;
    }
  • 初始化FullFrameRect。它是OpenGl绘制命令等的封装。
frameAvailable

在接受到Frame时,进行编码

mVideoEncoder.frameAvailable(mSurfaceTexture);
  • 时间戳和tranfrom矩阵
  float[] transform = new float[16];      // TODO - avoid alloc every frame
        st.getTransformMatrix(transform);
        long timestamp = st.getTimestamp();
        if (timestamp == 0) {
            // Seeing this after device is toggled off/on with power button.  The
            // first frame back has a zero timestamp.
            //
            // MPEG4Writer thinks this is cause to abort() in native code, so it's very
            // important that we just ignore the frame.
            Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero");
            return;
        }

        mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
                (int) (timestamp >> 32), (int) timestamp, transform));
  • 编码
  private void handleFrameAvailable(float[] transform, long timestampNanos) {
        if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform);
        //视频编码
        mVideoEncoder.drainEncoder(false);
        mFullScreen.drawFrame(mTextureId, transform);
        //设置时间戳
        mInputWindowSurface.setPresentationTime(timestampNanos);
        mInputWindowSurface.swapBuffers();
    }

  /**
     * 从encoder中得到数据,再写入到muxer中。
     * 下面这段代码就是通用的编码的代码了
     */
    public void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;
        if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");
        
        //如果通知编码器结束,就会signalEndOfInputStream
        if (endOfStream) {
            if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
            mEncoder.signalEndOfInputStream();
        }
        
        //得到outputBuffer
        ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
        //不断循环,当读取所有数据时
        while (true) {
            //上面换成的BufferInfo,送入到Encoder中,去查询状态
            int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            //如果时继续等待,就暂时不用处理。大多数情况,都是从这儿跳出循环
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
                }
            //outputBuffer发生变化了。就重新去获取
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not expected for an encoder
                encoderOutputBuffers = mEncoder.getOutputBuffers();
            //格式发生变化。这个第一次configure之后也会调用一次。在这里进行muxer的初始化
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                if (mMuxerStarted) {
                    throw new RuntimeException("format changed twice");
                }
                MediaFormat newFormat = mEncoder.getOutputFormat();
                Log.d(TAG, "encoder output format changed: " + newFormat);

                // now that we have the Magic Goodies, start the muxer
                mTrackIndex = mMuxer.addTrack(newFormat);
                mMuxer.start();
                mMuxerStarted = true;
            } else if (encoderStatus < 0) {
                Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
                        encoderStatus);
                // let's ignore it
            } else {
                //写入数据
                ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                if (encodedData == null) {
                    throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
                            " was null");
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    if (!mMuxerStarted) {
                        throw new RuntimeException("muxer hasn't started");
                    }
                    //切到对应的位置,进行书写
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    encodedData.position(mBufferInfo.offset);
                    encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
                    //写入
                    mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                    if (VERBOSE) {
                        Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
                                mBufferInfo.presentationTimeUs);
                    }
                }
                //重新释放,为了下一次的输入
                mEncoder.releaseOutputBuffer(encoderStatus, false);

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    //到达最后了,就跳出循环
                    if (!endOfStream) {
                        Log.w(TAG, "reached end of stream unexpectedly");
                    } else {
                        if (VERBOSE) Log.d(TAG, "end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

添加滤镜


整体流程理解

添加滤镜后的整体流程.png

上面,我们是直接绘制OES的纹理。这里,因为要添加滤镜的效果。所以我们需要将纹理进行处理。

离屏绘制

离屏绘制.png

先将OES纹理,绑定到FrameBuffer上。同时会在FrameBuffer上绑定一个新的textureId(这里命名为OffscreenTextureId)。然后调用绘制OES纹理的方法,数据就会传递到FBO上。而我们可以通过绑定在其上的OffscreenTextureId得到其数据。通常情况下,我们把绑定FrameBuffer和绘制这个新的OffscreenTextureId代表的纹理的过程,称为离屏绘制。

绑定和生成FrameBuffer的时机

创建FrameBuffer。因为RenderBuffer的存储大小要和当前的显示的宽和高相关。所以会在onSurfaceChanged生命周期方法时候调用。

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //在这里监听到尺寸的改变。做出对应的变化
        prepareFramebuffer(width, height);
        //...
    }

    //生成frameBuffer的时机
    private void prepareFramebuffer(int width, int height) {
        int[] values = new int[1];
        //申请一个与FrameBuffer绑定的textureId
        GLES20.glGenTextures(1, values, 0);
        mOffscreenTextureId = values[0];
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mOffscreenTextureId);
         // Create texture storage.
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

        // Set parameters.  We're probably using non-power-of-two dimensions, so
        // some values may not be available for use.
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
                GLES20.GL_NEAREST);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
                GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
                GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
                GLES20.GL_CLAMP_TO_EDGE);

        //创建FrameBuffer Object并且绑定它
        GLES20.glGenFramebuffers(1, values, 0);
        mFrameBuffer = values[0];
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);

        // 创建RenderBuffer Object并且绑定它
        GLES20.glGenRenderbuffers(1, values, 0);
        mRenderBuffer = values[0];
        GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderBuffer);

        //为我们的RenderBuffer申请存储空间
        GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);

        // 将renderBuffer挂载到frameBuffer的depth attachment 上。就上面申请了OffScreenId和FrameBuffer相关联
        GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mRenderBuffer);
        // 将text2d挂载到frameBuffer的color attachment上
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mOffscreenTextureId, 0);

        // See if GLES is happy with all this.
        int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
        if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
            throw new RuntimeException("Framebuffer not complete, status=" + status);
        }
        // 先不使用FrameBuffer,将其切换掉。到开始绘制的时候,在绑定回来
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    }
    
    //在onDrawFrame中添加代码
    @Override
    public void onDrawFrame(GL10 gl) {
        //...省略

        //重新切换到FrameBuffer上。
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
        //这里的绘制,就会将数据挂载到FrameBuffer上了。
        mOesFilter.draw();
        //解除绑定,结束FrameBuffer部分的数据写入
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
        
        //....省略
    }


  • FrameBuffer 帧缓冲对象
    openGL绘制流程.png

我们自己创建的FrameBuffer其实只是一个容器。所以我们要将数据挂载上去,它才算是完整。

FrameBuffer.png

所以,我们可以看到申请FrameBuffer需要进行下面的三步

  1. 生成一个FrameBuffer
  2. 申请一个RenderBuffer,并且挂载GL_DEPTH_ATTACHMENT上。

RenderBuffer也是一个渲染缓冲区对象。RenderBuffer对象是新引入的用于离屏渲染。它允许将场景直接渲染到Renderbuffer对象,而不是渲染到纹理对象。
Renderbuffer只是一个包含可渲染内部格式的单个映像的数据存储对象。它用于存储没有相应纹理格式的OpenGL逻辑缓冲区,如模板或深度缓冲区。

  1. 申请一个textureId,挂载到GL_COLOR_ATTACHMENT0上。
  2. 重新切换到FrameBuffer上(绑定),然后绘制。

我们就可以通过这个纹理,得到保存在FBO上的数据了

添加滤镜的绘制
添加滤镜.png

我们可以通过FBO,进行滤镜处理。我们将得到的数据,再次进行绘制,在这次的绘制中,我们就可以添加上我们想要的滤镜处理了。

但是这里不仅仅是要绘制到屏幕上,同时要在开启录制的时候,输入给Encoder进行视频的编码和封装。
所以我们需要将数据再写写入一个新的FrameBuffer中,然后再其输出的outputTexture中,就可以得到应用了纹理的数据了。

  • 滤镜处理
    就算将上面的OffscreenTextureId作为这里滤镜的输入Id.
    @Override
    public void onDrawFrame(GL10 gl) {
         //...省略
        //经过路径处理
        mColorFilter.setTextureId(mOffscreenTextureId);
        mColorFilter.onDrawFrame();
        int outputTextureId = mColorFilter.getOutputTextureId();
        //...省略
    }

同时滤镜内,也按照上面的FrameBuffer的处理流程。将数据挂载到FrameBuffer上。得到挂载在FrameBuffer上的outputTextureId

代码同上,省略
将应用了滤镜的纹理分别绘制到GLViewEncoder当中
image.png
@Override
    public void onDrawFrame(GL10 gl) {
        //省略...
        //经过滤镜处理
        mColorFilter.setTextureId(mOffscreenTextureId);
        mColorFilter.onDrawFrame();
        int outputTextureId = mColorFilter.getOutputTextureId();

        //将得到的outputTextureId,输入encoder,进行编码
        mVideoEncoder.setTextureId(outputTextureId);

        mVideoEncoder.frameAvailable(mSurfaceTexture);

        //将得到的outputTextureId,再次Draw,因为没有FrameBuffer,所以这次Draw的数据,就直接到了Surface上了。
        mShowFilter.setTextureId(outputTextureId);
        mShowFilter.onDrawFrame();
    }
  1. 将得到的outputTextureId,输入EncoderInputSurface中,通知内部进行draw 和进行编码。
  2. 将得到的outputTextureId,再次Draw,因为没有FrameBuffer,所以这次draw的数据,就直接到了Surface上了。也就是直接绘制到了我们的GLSurfaceView上了。

小结

  1. 对比之前绘制流程。上文是直接将纹理绘制到了GLView上显示,而这里是将纹理绘制到绑定的FrameBuffer中,而且
    绘制的结果不直接显示出来。所以可以形象的理解离屏绘制,就是将绘制的结果保存在与FrameBuffer绑定的一个新的textureIdOffscreenTextureId)中,不直接绘制到屏幕上。
  2. 把握好整体流程之后,这部分的处理也会变得简单起来。后面就可以如何添加更加炫酷的滤镜和玩法了。

最后

整编文章就重要的部分还是在理解整个纹理中数据传递的路线。
后续,会对这里的相机的预览添加其他的滤镜。
Demo位置
https://github.com/deepsadness/ZeroToOpenGL

系列文章地址
Android OpenGL ES(一)-开始描绘一个平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面图形
Android OpenGL ES(四)-为平面图添加滤镜
Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜
Android OpenGL ES(六) - 将输入源换成视频
Android OpenGL ES(七) - 生成抖音照片电影

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容