《Android 美顏類相機開發(fā)匯總》第一章 Android OpenGLES 相機預覽

SurfaceView + OpenGLES 預覽相機

使用OpenGLES 預覽相機廊敌,我們可以通過GLSurfaceView 來預覽相機。GLSurfaceView封裝了EGLContext剧罩。關于GLSurfaceView的源碼里面茬贵,GLThread作為單獨的線程處理OpenGL的繪制操作槐沼,但是這里有個問題阶冈,我們可以看看GLThread里面的循環(huán):

while (true) {
    synchronized (sGLThreadManager) {
        while (true) {
            ...   // 處理是否需要刷新
            sGLThreadManager.wait();
        }
    } // end of synchronized(sGLThreadManager)
    if (event != null) {
        event.run();
        event = null;
        continue;
    }
    ...
}

內(nèi)循環(huán)是用來判斷是否需要走繪制循環(huán)。當使用RENDERMODE_WHEN_DIRTY而非RENDERMODE_CONTINUOUSLY時葫慎,如果我們不主動調(diào)用requestRender繪制的話衔彻,它會一直在內(nèi)部等待。然后另外一點就是偷办,當我們調(diào)用queueEvent方法過多的時候艰额,會導致event事件過多,然后需要不斷地循環(huán)處理event事件椒涯,最終并沒有走到刷新畫面的流程柄沮。也就是說,為了保證得到更高的fps废岂,我們需要解決這個問題铡溪。還有另外一個問題就是GLSurfaceView 中的EGL環(huán)境有可能會丟失重建的情況,對后續(xù)利用SharedContext做錄制處理有影響泪喊。
因此棕硫,我沒有使用GLSurfaceView來做繪制操作,用另外一個Looper線程單獨處理OpenGLES 的紋理資源加載袒啼、渲染等操作哈扮。放棄使用GLSurfaceView 的另外一大原因是,為了利用SharedContext實現(xiàn)無丟幀錄制視頻的功能蚓再,GLSurfaceView 有可能會在中途釋放并重新創(chuàng)建EGLContext滑肉,導致SharedContext失效,錄制失敗的情況摘仅。關于這個的話靶庙,可以參考grafika,里面有issue討論過這個問題娃属。
關于SurfaceView + OpenGLES 預覽相機六荒,可以參考本人的文章:
Android Camera SurfaceView OpenGLES 預覽
這篇文章是很久之前寫的护姆,現(xiàn)在CainCamera開源項目已經(jīng)發(fā)生了比較大的改變。這里還是重新介紹一遍吧掏击。不過這次應該是最后一次大改動了卵皂,相機部分的功能基本已經(jīng)完成,只剩一些小功能沒有實現(xiàn)而已砚亭,而且暫時也不會再更新相機部分的功能了灯变。

  • 渲染線程 —— HandlerThread
    通過HandlerThread 創(chuàng)建EGLContext綁定的渲染線程,如下:
class RenderThread extends HandlerThread implements SurfaceTexture.OnFrameAvailableListener,
        Camera.PreviewCallback {

    private static final String TAG = "RenderThread";
    private static final boolean VERBOSE = false;

    // 操作鎖
    private final Object mSynOperation = new Object();
    // 更新幀的鎖
    private final Object mSyncFrameNum = new Object();
    private final Object mSyncFence = new Object();

    private boolean isPreviewing = false;       // 是否預覽狀態(tài)
    private boolean isRecording = false;        // 是否錄制狀態(tài)
    private boolean isRecordingPause = false;   // 是否處于暫停錄制狀態(tài)

    // EGL共享上下文
    private EglCore mEglCore;
    // 預覽用的EGLSurface
    private WindowSurface mDisplaySurface;

    private int mInputTexture;
    private int mCurrentTexture;
    private SurfaceTexture mSurfaceTexture;

    // 矩陣
    private final float[] mMatrix = new float[16];

    // 預覽回調(diào)
    private byte[] mPreviewBuffer;
    // 輸入圖像大小
    private int mTextureWidth, mTextureHeight;

    // 可用幀
    private int mFrameNum = 0;

    // 渲染Handler回調(diào)
    private RenderHandler mRenderHandler;

    // 計算幀率
    private FrameRateMeter mFrameRateMeter;

    // 上下文
    private Context mContext;

    // 正在拍照
    private volatile boolean mTakingPicture;
    // 預覽參數(shù)
    private CameraParam mCameraParam;

    // 渲染管理器
    private RenderManager mRenderManager;

    public RenderThread(Context context, String name) {
        super(name);
        mContext = context;
        mCameraParam = CameraParam.getInstance();
        mRenderManager = RenderManager.getInstance();
        mFrameRateMeter = new FrameRateMeter();
    }

    /**
     * 設置預覽Handler回調(diào)
     * @param handler
     */
    public void setRenderHandler(RenderHandler handler) {
        mRenderHandler = handler;
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {

    }

    private long time = 0;
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        synchronized (mSynOperation) {
            if (isPreviewing || isRecording) {
                mRenderHandler.sendMessage(mRenderHandler
                        .obtainMessage(RenderHandler.MSG_PREVIEW_CALLBACK, data));
            }
        }
        if (mPreviewBuffer != null) {
            camera.addCallbackBuffer(mPreviewBuffer);
        }
        // 計算fps
        if (mRenderHandler != null && mCameraParam.showFps) {
            mRenderHandler.sendEmptyMessage(RenderHandler.MSG_CALCULATE_FPS);
        }
        if (VERBOSE) {
            Log.d("onPreviewFrame", "update time = " + (System.currentTimeMillis() - time));
            time = System.currentTimeMillis();
        }
    }

    /**
     * 預覽回調(diào)
     * @param data
     */
    void onPreviewCallback(byte[] data) {
        if (mCameraParam.cameraCallback != null) {
            mCameraParam.cameraCallback.onPreviewCallback(data);
        }
    }

    /**
     * Surface創(chuàng)建
     * @param holder
     */
    void surfaceCreated(SurfaceHolder holder) {
        mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
        mDisplaySurface = new WindowSurface(mEglCore, holder.getSurface(), false);
        mDisplaySurface.makeCurrent();

        GLES30.glDisable(GLES30.GL_DEPTH_TEST);
        GLES30.glDisable(GLES30.GL_CULL_FACE);

        // 渲染器初始化
        mRenderManager.init(mContext);

        mInputTexture = OpenGLUtils.createOESTexture();
        mSurfaceTexture = new SurfaceTexture(mInputTexture);
        mSurfaceTexture.setOnFrameAvailableListener(this);

        // 打開相機
        openCamera();

    }

    /**
     * Surface改變
     * @param width
     * @param height
     */
    void surfaceChanged(int width, int height) {
        mRenderManager.setDisplaySize(width, height);
        startPreview();
    }

    /**
     * Surface銷毀
     */
    void surfaceDestroyed() {
        mTakingPicture = false;
        mRenderManager.release();
        releaseCamera();
        if (mSurfaceTexture != null) {
            mSurfaceTexture.release();
            mSurfaceTexture = null;
        }
        if (mDisplaySurface != null) {
            mDisplaySurface.release();
            mDisplaySurface = null;
        }
        if (mEglCore != null) {
            mEglCore.release();
            mEglCore = null;
        }
    }

    /**
     * 繪制幀
     */
    void drawFrame() {
        // 如果存在新的幀捅膘,則更新幀
        synchronized (mSyncFrameNum) {
            synchronized (mSyncFence) {
                if (mSurfaceTexture != null) {
                    while (mFrameNum != 0) {
                        mSurfaceTexture.updateTexImage();
                        --mFrameNum;
                    }
                } else {
                    return;
                }
            }
        }

        // 切換渲染上下文
        mDisplaySurface.makeCurrent();
        mSurfaceTexture.getTransformMatrix(mMatrix);

        // 繪制渲染
        mCurrentTexture = mRenderManager.drawFrame(mInputTexture, mMatrix);

        // 是否繪制人臉關鍵點
        mRenderManager.drawFacePoint(mCurrentTexture);

        // 顯示到屏幕
        mDisplaySurface.swapBuffers();

        // 執(zhí)行拍照
        if (mCameraParam.isTakePicture && !mTakingPicture) {
            synchronized (mSyncFence) {
                mTakingPicture = true;
                mRenderHandler.sendEmptyMessage(RenderHandler.MSG_TAKE_PICTURE);
            }
        }

        // 是否處于錄制狀態(tài)
        if (isRecording && !isRecordingPause) {
            HardcodeEncoder.getInstance().frameAvailable();
            HardcodeEncoder.getInstance()
                    .drawRecorderFrame(mCurrentTexture, mSurfaceTexture.getTimestamp());
        }
    }

    /**
     * 拍照
     */
    void takePicture() {
        synchronized (mSyncFence) {
            ByteBuffer buffer = mDisplaySurface.getCurrentFrame();
            mCameraParam.captureCallback.onCapture(buffer,
                    mDisplaySurface.getWidth(), mDisplaySurface.getHeight());
            mTakingPicture = false;
            mCameraParam.isTakePicture = false;
        }
    }

    /**
     * 計算fps
     */
    void calculateFps() {
        // 幀率回調(diào)
        if ((mCameraParam).fpsCallback != null) {
            mFrameRateMeter.drawFrameCount();
            (mCameraParam).fpsCallback.onFpsCallback(mFrameRateMeter.getFPS());
        }
    }

    /**
     * 計算imageView 的寬高
     */
    private void calculateImageSize() {
        if (mCameraParam.orientation == 90 || mCameraParam.orientation == 270) {
            mTextureWidth = mCameraParam.previewHeight;
            mTextureHeight = mCameraParam.previewWidth;
        } else {
            mTextureWidth = mCameraParam.previewWidth;
            mTextureHeight = mCameraParam.previewHeight;
        }
        mRenderManager.setTextureSize(mTextureWidth, mTextureHeight);
    }

    /**
     * 切換邊框模糊
     * @param enableEdgeBlur
     */
    void changeEdgeBlurFilter(boolean enableEdgeBlur) {
        synchronized (mSynOperation) {
            mRenderManager.changeEdgeBlurFilter(enableEdgeBlur);
        }
    }

    /**
     * 切換動態(tài)濾鏡
     * @param color
     */
    void changeDynamicFilter(DynamicColor color) {
        synchronized (mSynOperation) {
            mRenderManager.changeDynamicFilter(color);
        }
    }

    /**
     * 切換動態(tài)彩妝
     * @param makeup
     */
    void changeDynamicMakeup(DynamicMakeup makeup) {
        synchronized (mSynOperation) {
            mRenderManager.changeDynamicMakeup(makeup);
        }
    }

    /**
     * 切換動態(tài)資源
     * @param color
     */
    void changeDynamicResource(DynamicColor color) {
        synchronized (mSynOperation) {
            mRenderManager.changeDynamicResource(color);
        }
    }

    /**
     * 切換動態(tài)資源
     * @param sticker
     */
    void changeDynamicResource(DynamicSticker sticker) {
        synchronized (mSynOperation) {
            mRenderManager.changeDynamicResource(sticker);
        }
    }

    /**
     * 開始錄制
     */
    void startRecording() {
        if (mEglCore != null) {
            // 設置渲染Texture 的寬高
            HardcodeEncoder.getInstance().setTextureSize(mTextureWidth, mTextureHeight);
            // 這里將EGLContext傳遞到錄制線程共享添祸。
            // 由于EGLContext是當前線程手動創(chuàng)建,也就是OpenGLES的main thread
            // 這里需要傳自己手動創(chuàng)建的EglContext
            HardcodeEncoder.getInstance().startRecording(mContext, mEglCore.getEGLContext());
        }
        isRecording = true;
    }

    /**
     * 停止錄制
     */
    void stopRecording() {
        HardcodeEncoder.getInstance().stopRecording();
        isRecording = false;
    }

    /**
     * 請求刷新
     */
    public void requestRender() {
        synchronized (mSyncFrameNum) {
            if (isPreviewing) {
                ++mFrameNum;
                if (mRenderHandler != null) {
                    mRenderHandler.removeMessages(RenderHandler.MSG_RENDER);
                    mRenderHandler.sendMessage(mRenderHandler
                            .obtainMessage(RenderHandler.MSG_RENDER));
                }
            }
        }
    }


    // --------------------------------- 相機操作邏輯 ----------------------------------------------
    /**
     * 打開相機
     */
    void openCamera() {
        releaseCamera();
        CameraEngine.getInstance().openCamera(mContext);
        CameraEngine.getInstance().setPreviewSurface(mSurfaceTexture);
        calculateImageSize();
        mPreviewBuffer = new byte[mTextureWidth * mTextureHeight * 3/ 2];
        CameraEngine.getInstance().setPreviewCallbackWithBuffer(this, mPreviewBuffer);
        // 相機打開回調(diào)
        if (mCameraParam.cameraCallback != null) {
            mCameraParam.cameraCallback.onCameraOpened();
        }
    }

    /**
     * 切換相機
     */
    void switchCamera() {
        mCameraParam.backCamera = !mCameraParam.backCamera;
        if (mCameraParam.backCamera) {
            mCameraParam.cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
        } else {
            mCameraParam.cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
        }
        openCamera();
        startPreview();
    }

    /**
     * 開始預覽
     */
    private void startPreview() {
        CameraEngine.getInstance().startPreview();
        isPreviewing = true;
    }

    /**
     * 釋放相機
     */
    private void releaseCamera() {
        isPreviewing = false;
        CameraEngine.getInstance().releaseCamera();
    }
}

相機的操作也放在該線程寻仗。這樣相機打開關閉操作膝捞、渲染前處理等操作都不會影響到UI的響應了。CameraEngine則是相機引擎單例愧沟,用于控制相機操作的蔬咬。RenderManager 則是渲染操作的單例,如果你不想用SurfaceView沐寺,也可以單獨將RenderManager提取出來林艘,放到GLSurfaceView中。

  • 渲染管理器 —— RenderManager
    RenderManager 的代碼如下:
public final class RenderManager {

    private static class RenderManagerHolder {
        public static RenderManager instance = new RenderManager();
    }

    private RenderManager() {
        mCameraParam = CameraParam.getInstance();
    }

    public static RenderManager getInstance() {
        return RenderManagerHolder.instance;
    }

    // 濾鏡列表
    private SparseArray<GLImageFilter> mFilterArrays = new SparseArray<GLImageFilter>();

    // 坐標緩沖
    private ScaleType mScaleType = ScaleType.CENTER_CROP;
    private FloatBuffer mVertexBuffer;
    private FloatBuffer mTextureBuffer;
    // 用于顯示裁剪的紋理頂點緩沖
    private FloatBuffer mDisplayVertexBuffer;
    private FloatBuffer mDisplayTextureBuffer;

    // 視圖寬高
    private int mViewWidth, mViewHeight;
    // 輸入圖像大小
    private int mTextureWidth, mTextureHeight;

    // 相機參數(shù)
    private CameraParam mCameraParam;
    // 上下文
    private Context mContext;

    /**
     * 初始化
     */
    public void init(Context context) {
        initBuffers();
        initFilters(context);
        mContext = context;
    }

    /**
     * 釋放資源
     */
    public void release() {
        releaseBuffers();
        releaseFilters();
        mContext = null;
    }

    /**
     * 釋放濾鏡
     */
    private void releaseFilters() {
        for (int i = 0; i < mFilterArrays.size(); i++) {
            if (mFilterArrays.get(i) != null) {
                mFilterArrays.get(i).release();
            }
        }
        mFilterArrays.clear();
    }

    /**
     * 釋放緩沖區(qū)
     */
    private void releaseBuffers() {
        if (mVertexBuffer != null) {
            mVertexBuffer.clear();
            mVertexBuffer = null;
        }
        if (mTextureBuffer != null) {
            mTextureBuffer.clear();
            mTextureBuffer = null;
        }
        if (mDisplayVertexBuffer != null) {
            mDisplayVertexBuffer.clear();
            mDisplayVertexBuffer = null;
        }
        if (mDisplayTextureBuffer != null) {
            mDisplayTextureBuffer.clear();
            mDisplayTextureBuffer = null;
        }
    }

    /**
     * 初始化緩沖區(qū)
     */
    private void initBuffers() {
        releaseBuffers();
        mDisplayVertexBuffer = OpenGLUtils.createFloatBuffer(TextureRotationUtils.CubeVertices);
        mDisplayTextureBuffer = OpenGLUtils.createFloatBuffer(TextureRotationUtils.TextureVertices);
        mVertexBuffer = OpenGLUtils.createFloatBuffer(TextureRotationUtils.CubeVertices);
        mTextureBuffer = OpenGLUtils.createFloatBuffer(TextureRotationUtils.TextureVertices);
    }

    /**
     * 初始化濾鏡
     * @param context
     */
    private void initFilters(Context context) {
        releaseFilters();
        // 相機輸入濾鏡
        mFilterArrays.put(RenderIndex.CameraIndex, new GLImageOESInputFilter(context));
        // 美顏濾鏡
        mFilterArrays.put(RenderIndex.BeautyIndex, new GLImageBeautyFilter(context));
        // 彩妝濾鏡
        mFilterArrays.put(RenderIndex.MakeupIndex, new GLImageMakeupFilter(context, null));
        // 美型濾鏡
        mFilterArrays.put(RenderIndex.FaceAdjustIndex, new GLImageFaceReshapeFilter(context));
        // LUT/顏色濾鏡
        mFilterArrays.put(RenderIndex.FilterIndex, null);
        // 貼紙資源濾鏡
        mFilterArrays.put(RenderIndex.ResourceIndex, null);
        // 景深濾鏡
        mFilterArrays.put(RenderIndex.DepthBlurIndex, new GLImageDepthBlurFilter(context));
        // 暗角濾鏡
        mFilterArrays.put(RenderIndex.VignetteIndex, new GLImageVignetteFilter(context));
        // 顯示輸出
        mFilterArrays.put(RenderIndex.DisplayIndex, new GLImageFilter(context));
        // 人臉關鍵點調(diào)試
        mFilterArrays.put(RenderIndex.FacePointIndex, new GLImageFacePointsFilter(context));
    }

    /**
     * 是否切換邊框模糊
     * @param enableEdgeBlur
     */
    public synchronized void changeEdgeBlurFilter(boolean enableEdgeBlur) {
        if (enableEdgeBlur) {
            mFilterArrays.get(RenderIndex.DisplayIndex).release();
            GLImageFrameEdgeBlurFilter filter = new GLImageFrameEdgeBlurFilter(mContext);
            filter.onInputSizeChanged(mTextureWidth, mTextureHeight);
            filter.onDisplaySizeChanged(mViewWidth, mViewHeight);
            mFilterArrays.put(RenderIndex.DisplayIndex, filter);
        } else {
            mFilterArrays.get(RenderIndex.DisplayIndex).release();
            GLImageFilter filter = new GLImageFilter(mContext);
            filter.onInputSizeChanged(mTextureWidth, mTextureHeight);
            filter.onDisplaySizeChanged(mViewWidth, mViewHeight);
            mFilterArrays.put(RenderIndex.DisplayIndex, filter);
        }
    }

    /**
     * 切換動態(tài)濾鏡
     * @param color
     */
    public synchronized void changeDynamicFilter(DynamicColor color) {
        if (mFilterArrays.get(RenderIndex.FilterIndex) != null) {
            mFilterArrays.get(RenderIndex.FilterIndex).release();
            mFilterArrays.put(RenderIndex.FilterIndex, null);
        }
        if (color == null) {
            return;
        }
        GLImageDynamicColorFilter filter = new GLImageDynamicColorFilter(mContext, color);
        filter.onInputSizeChanged(mTextureWidth, mTextureHeight);
        filter.initFrameBuffer(mTextureWidth, mTextureHeight);
        filter.onDisplaySizeChanged(mViewWidth, mViewHeight);
        mFilterArrays.put(RenderIndex.FilterIndex, filter);
    }

    /**
     * 切換動態(tài)濾鏡
     * @param dynamicMakeup
     */
    public synchronized void changeDynamicMakeup(DynamicMakeup dynamicMakeup) {
        if (mFilterArrays.get(RenderIndex.MakeupIndex) != null) {
            ((GLImageMakeupFilter)mFilterArrays.get(RenderIndex.MakeupIndex)).changeMakeupData(dynamicMakeup);
        } else {
            GLImageMakeupFilter filter = new GLImageMakeupFilter(mContext, dynamicMakeup);
            filter.onInputSizeChanged(mTextureWidth, mTextureHeight);
            filter.initFrameBuffer(mTextureWidth, mTextureHeight);
            filter.onDisplaySizeChanged(mViewWidth, mViewHeight);
            mFilterArrays.put(RenderIndex.MakeupIndex, filter);
        }
    }

    /**
     * 切換動態(tài)資源
     * @param color
     */
    public synchronized void changeDynamicResource(DynamicColor color) {
        if (mFilterArrays.get(RenderIndex.ResourceIndex) != null) {
            mFilterArrays.get(RenderIndex.ResourceIndex).release();
            mFilterArrays.put(RenderIndex.ResourceIndex, null);
        }
        if (color == null) {
            return;
        }
        GLImageDynamicColorFilter filter = new GLImageDynamicColorFilter(mContext, color);
        filter.onInputSizeChanged(mTextureWidth, mTextureHeight);
        filter.initFrameBuffer(mTextureWidth, mTextureHeight);
        filter.onDisplaySizeChanged(mViewWidth, mViewHeight);
        mFilterArrays.put(RenderIndex.ResourceIndex, filter);
    }

    /**
     * 切換動態(tài)資源
     * @param sticker
     */
    public synchronized void changeDynamicResource(DynamicSticker sticker) {
        // 釋放舊濾鏡
        if (mFilterArrays.get(RenderIndex.ResourceIndex) != null) {
            mFilterArrays.get(RenderIndex.ResourceIndex).release();
            mFilterArrays.put(RenderIndex.ResourceIndex, null);
        }
        if (sticker == null) {
            return;
        }
        GLImageDynamicStickerFilter filter = new GLImageDynamicStickerFilter(mContext, sticker);
        // 設置輸入輸入大小混坞,初始化fbo等
        filter.onInputSizeChanged(mTextureWidth, mTextureHeight);
        filter.initFrameBuffer(mTextureWidth, mTextureHeight);
        filter.onDisplaySizeChanged(mViewWidth, mViewHeight);
        mFilterArrays.put(RenderIndex.ResourceIndex, filter);
    }

    /**
     * 繪制紋理
     * @param inputTexture
     * @param mMatrix
     * @return
     */
    public int drawFrame(int inputTexture, float[] mMatrix) {
        int currentTexture = inputTexture;
        if (mFilterArrays.get(RenderIndex.CameraIndex) == null
                || mFilterArrays.get(RenderIndex.DisplayIndex) == null) {
            return currentTexture;
        }
        if (mFilterArrays.get(RenderIndex.CameraIndex) instanceof GLImageOESInputFilter) {
            ((GLImageOESInputFilter)mFilterArrays.get(RenderIndex.CameraIndex)).setTextureTransformMatrix(mMatrix);
        }
        currentTexture = mFilterArrays.get(RenderIndex.CameraIndex)
                .drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
        // 如果處于對比狀態(tài)狐援,不做處理
        if (!mCameraParam.showCompare) {
            // 美顏濾鏡
            if (mFilterArrays.get(RenderIndex.BeautyIndex) != null) {
                if (mFilterArrays.get(RenderIndex.BeautyIndex) instanceof IBeautify
                        && mCameraParam.beauty != null) {
                    ((IBeautify) mFilterArrays.get(RenderIndex.BeautyIndex)).onBeauty(mCameraParam.beauty);
                }
                currentTexture = mFilterArrays.get(RenderIndex.BeautyIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }

            // 彩妝濾鏡
            if (mFilterArrays.get(RenderIndex.MakeupIndex) != null) {
                currentTexture = mFilterArrays.get(RenderIndex.MakeupIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }

            // 美型濾鏡
            if (mFilterArrays.get(RenderIndex.FaceAdjustIndex) != null) {
                if (mFilterArrays.get(RenderIndex.FaceAdjustIndex) instanceof IBeautify) {
                    ((IBeautify) mFilterArrays.get(RenderIndex.FaceAdjustIndex)).onBeauty(mCameraParam.beauty);
                }
                currentTexture = mFilterArrays.get(RenderIndex.FaceAdjustIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }

            // 繪制顏色濾鏡
            if (mFilterArrays.get(RenderIndex.FilterIndex) != null) {
                currentTexture = mFilterArrays.get(RenderIndex.FilterIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }

            // 資源濾鏡,可以是貼紙究孕、濾鏡甚至是彩妝類型
            if (mFilterArrays.get(RenderIndex.ResourceIndex) != null) {
                currentTexture = mFilterArrays.get(RenderIndex.ResourceIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }

            // 景深
            if (mFilterArrays.get(RenderIndex.DepthBlurIndex) != null) {
                mFilterArrays.get(RenderIndex.DepthBlurIndex).setFilterEnable(mCameraParam.enableDepthBlur);
                currentTexture = mFilterArrays.get(RenderIndex.DepthBlurIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }

            // 暗角
            if (mFilterArrays.get(RenderIndex.VignetteIndex) != null) {
                mFilterArrays.get(RenderIndex.VignetteIndex).setFilterEnable(mCameraParam.enableVignette);
                currentTexture = mFilterArrays.get(RenderIndex.VignetteIndex).drawFrameBuffer(currentTexture, mVertexBuffer, mTextureBuffer);
            }
        }

        // 顯示輸出啥酱,需要調(diào)整視口大小
        mFilterArrays.get(RenderIndex.DisplayIndex).drawFrame(currentTexture, mDisplayVertexBuffer, mDisplayTextureBuffer);

        return currentTexture;
    }

    /**
     * 繪制調(diào)試用的人臉關鍵點
     * @param mCurrentTexture
     */
    public void drawFacePoint(int mCurrentTexture) {
        if (mFilterArrays.get(RenderIndex.FacePointIndex) != null) {
            if (mCameraParam.drawFacePoints && LandmarkEngine.getInstance().hasFace()) {
                mFilterArrays.get(RenderIndex.FacePointIndex).drawFrame(mCurrentTexture, mDisplayVertexBuffer, mDisplayTextureBuffer);
            }
        }
    }

    /**
     * 設置輸入紋理大小
     * @param width
     * @param height
     */
    public void setTextureSize(int width, int height) {
        mTextureWidth = width;
        mTextureHeight = height;
    }

    /**
     * 設置紋理顯示大小
     * @param width
     * @param height
     */
    public void setDisplaySize(int width, int height) {
        mViewWidth = width;
        mViewHeight = height;
        adjustCoordinateSize();
        onFilterChanged();
    }

    /**
     * 調(diào)整濾鏡
     */
    private void onFilterChanged() {
        for (int i = 0; i < mFilterArrays.size(); i++) {
            if (mFilterArrays.get(i) != null) {
                mFilterArrays.get(i).onInputSizeChanged(mTextureWidth, mTextureHeight);
                // 到顯示之前都需要創(chuàng)建FBO,這里限定是防止創(chuàng)建多余的FBO厨诸,節(jié)省GPU資源
                if (i < RenderIndex.DisplayIndex) {
                    mFilterArrays.get(i).initFrameBuffer(mTextureWidth, mTextureHeight);
                }
                mFilterArrays.get(i).onDisplaySizeChanged(mViewWidth, mViewHeight);
            }
        }
    }

    /**
     * 調(diào)整由于surface的大小與SurfaceView大小不一致帶來的顯示問題
     */
    private void adjustCoordinateSize() {
        float[] textureCoord = null;
        float[] vertexCoord = null;
        float[] textureVertices = TextureRotationUtils.TextureVertices;
        float[] vertexVertices = TextureRotationUtils.CubeVertices;
        float ratioMax = Math.max((float) mViewWidth / mTextureWidth,
                (float) mViewHeight / mTextureHeight);
        // 新的寬高
        int imageWidth = Math.round(mTextureWidth * ratioMax);
        int imageHeight = Math.round(mTextureHeight * ratioMax);
        // 獲取視圖跟texture的寬高比
        float ratioWidth = (float) imageWidth / (float) mViewWidth;
        float ratioHeight = (float) imageHeight / (float) mViewHeight;
        if (mScaleType == ScaleType.CENTER_INSIDE) {
            vertexCoord = new float[] {
                    vertexVertices[0] / ratioHeight, vertexVertices[1] / ratioWidth, vertexVertices[2],
                    vertexVertices[3] / ratioHeight, vertexVertices[4] / ratioWidth, vertexVertices[5],
                    vertexVertices[6] / ratioHeight, vertexVertices[7] / ratioWidth, vertexVertices[8],
                    vertexVertices[9] / ratioHeight, vertexVertices[10] / ratioWidth, vertexVertices[11],
            };
        } else if (mScaleType == ScaleType.CENTER_CROP) {
            float distHorizontal = (1 - 1 / ratioWidth) / 2;
            float distVertical = (1 - 1 / ratioHeight) / 2;
            textureCoord = new float[] {
                    addDistance(textureVertices[0], distVertical), addDistance(textureVertices[1], distHorizontal),
                    addDistance(textureVertices[2], distVertical), addDistance(textureVertices[3], distHorizontal),
                    addDistance(textureVertices[4], distVertical), addDistance(textureVertices[5], distHorizontal),
                    addDistance(textureVertices[6], distVertical), addDistance(textureVertices[7], distHorizontal),
            };
        }
        if (vertexCoord == null) {
            vertexCoord = vertexVertices;
        }
        if (textureCoord == null) {
            textureCoord = textureVertices;
        }
        // 更新VertexBuffer 和 TextureBuffer
        mDisplayVertexBuffer.clear();
        mDisplayVertexBuffer.put(vertexCoord).position(0);
        mDisplayTextureBuffer.clear();
        mDisplayTextureBuffer.put(textureCoord).position(0);
    }

    /**
     * 計算距離
     * @param coordinate
     * @param distance
     * @return
     */
    private float addDistance(float coordinate, float distance) {
        return coordinate == 0.0f ? distance : 1 - distance;
    }
}

這里由于渲染層數(shù)是有限并且是固定的镶殷,因此使用SparseArray來存儲渲染的濾鏡列表,這比用Hashmap的效率要高一點微酬。其中CameraParam是存儲相機參數(shù)的單例绘趋,UI層可以通過改變CameraParam的數(shù)據(jù)調(diào)節(jié)濾鏡渲染的流程。

  • 預覽渲染器 —— PreviewRenderer
    為了方便使用颗管,我們將RenderThread封裝到預覽渲染器單例中:
public final class PreviewRenderer {

    private PreviewRenderer() {
        mCameraParam = CameraParam.getInstance();
    }

    private static class RenderHolder {
        private static PreviewRenderer instance = new PreviewRenderer();
    }

    public static PreviewRenderer getInstance() {
        return RenderHolder.instance;
    }

    // 相機渲染參數(shù)
    private CameraParam mCameraParam;

    // 渲染Handler
    private RenderHandler mRenderHandler;
    // 渲染線程
    private RenderThread mPreviewRenderThread;
    // 操作鎖
    private final Object mSynOperation = new Object();

    private WeakReference<SurfaceView> mWeakSurfaceView;

    /**
     * 設置相機回調(diào)
     * @param callback
     * @return
     */
    public RenderBuilder setCameraCallback(OnCameraCallback callback) {
        return new RenderBuilder(this, callback);
    }

    /**
     * 初始化渲染器
     */
    void initRenderer(Context context) {
        synchronized (mSynOperation) {
            mPreviewRenderThread = new RenderThread(context, "RenderThread");
            mPreviewRenderThread.start();
            mRenderHandler = new RenderHandler(mPreviewRenderThread);
            // 綁定Handler
            mPreviewRenderThread.setRenderHandler(mRenderHandler);
        }
    }

    /**
     * 銷毀渲染器
     */
    public void destroyRenderer() {
        synchronized (mSynOperation) {
            if (mWeakSurfaceView != null) {
                mWeakSurfaceView.clear();
                mWeakSurfaceView = null;
            }
            if (mRenderHandler != null) {
                mRenderHandler.removeCallbacksAndMessages(null);
                mRenderHandler = null;
            }
            if (mPreviewRenderThread != null) {
                mPreviewRenderThread.quitSafely();
                try {
                    mPreviewRenderThread.join();
                } catch (InterruptedException e) {

                }
                mPreviewRenderThread = null;
            }
        }
    }

    /**
     * 綁定需要渲染的SurfaceView
     * @param surfaceView
     */
    public void setSurfaceView(SurfaceView surfaceView) {
        mWeakSurfaceView = new WeakReference<>(surfaceView);
        surfaceView.getHolder().addCallback(mSurfaceCallback);
    }

    /**
     * Surface回調(diào)
     */
    private SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            if (mRenderHandler != null) {
                mRenderHandler.sendMessage(mRenderHandler
                        .obtainMessage(RenderHandler.MSG_SURFACE_CREATED, holder));
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            surfaceSizeChanged(width, height);
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (mRenderHandler != null) {
                mRenderHandler.sendMessage(mRenderHandler
                        .obtainMessage(RenderHandler.MSG_SURFACE_DESTROYED));
            }
        }
    };

    /**
     * Surface大小發(fā)生變化
     * @param width
     * @param height
     */
    public void surfaceSizeChanged(int width, int height) {
        if (mRenderHandler != null) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_SURFACE_CHANGED, width, height));
        }
    }

    /**
     * 請求渲染
     */
    public void requestRender() {
        if (mPreviewRenderThread != null) {
            mPreviewRenderThread.requestRender();
        }
    }

    /**
     * 切換邊框模糊功能
     * @param enableEdgeBlur
     */
    public void changeEdgeBlurFilter(boolean enableEdgeBlur) {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_CHANGE_EDGE_BLUR, enableEdgeBlur));
        }
    }

    /**
     * 切換濾鏡
     * @param color
     */
    public void changeDynamicFilter(DynamicColor color) {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_CHANGE_DYNAMIC_COLOR, color));
        }
    }

    /**
     * 切換彩妝
     * @param makeup
     */
    public void changeDynamicMakeup(DynamicMakeup makeup) {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_CHANGE_DYNAMIC_MAKEUP, makeup));
        }
    }

    /**
     * 切換動態(tài)資源
     * @param color
     */
    public void changeDynamicResource(DynamicColor color) {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_CHANGE_DYNAMIC_RESOURCE, color));
        }
    }

    /**
     * 切換動態(tài)資源
     * @param sticker
     */
    public void changeDynamicResource(DynamicSticker sticker) {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_CHANGE_DYNAMIC_RESOURCE, sticker));
        }
    }

    /**
     * 開始錄制
     */
    public void startRecording() {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendMessage(mRenderHandler
                    .obtainMessage(RenderHandler.MSG_START_RECORDING));
        }
    }

    /**
     * 停止錄制
     */
    public void stopRecording() {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendEmptyMessage(RenderHandler.MSG_STOP_RECORDING);
        }
    }

    /**
     * 拍照
     */
    public void takePicture() {
        synchronized (mSynOperation) {
            if (!mCameraParam.isTakePicture) {
                mCameraParam.isTakePicture = true;
            }
        }
    }

    /**
     * 切換相機
     */
    public void switchCamera() {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendEmptyMessage(RenderHandler.MSG_SWITCH_CAMERA);
        }
    }

    /**
     * 重新打開相機
     */
    public void reopenCamera() {
        if (mRenderHandler == null) {
            return;
        }
        synchronized (mSynOperation) {
            mRenderHandler.sendEmptyMessage(RenderHandler.MSG_REOPEN_CAMERA);
        }
    }

    /**
     * 是否需要進行對比
     * @param enable
     */
    public void enableCompare(boolean enable) {
        synchronized (mSynOperation) {
            mCameraParam.showCompare = enable;
        }
    }
}

這樣陷遮,我們就可以在預覽頁面里面,通過PreviewRenderer 來控制OpenGLES 渲染參數(shù)垦江、相機操作邏輯帽馋、以及切換濾鏡、貼紙等操作邏輯了。

  • 預覽頁面Builder封裝
    為了方便進入預覽頁面時绽族,傳入預覽的寬高比姨涡、是否顯示調(diào)試關鍵點等參數(shù),我們通過Builder 模式來控制:
public final class PreviewBuilder {

    private PreviewEngine mPreviewEngine;
    private CameraParam mCameraParam;

    public PreviewBuilder(PreviewEngine engine, AspectRatio ratio) {
        mPreviewEngine = engine;
        mCameraParam = CameraParam.getInstance();
        mCameraParam.setAspectRatio(ratio);
    }

    /**
     * 是否顯示人臉關鍵點
     * @param show
     * @return
     */
    public PreviewBuilder showFacePoints(boolean show) {
        mCameraParam.drawFacePoints = show;
        return this;
    }

    /**
     * 是否顯示fps
     * @param show
     * @return
     */
    public PreviewBuilder showFps(boolean show) {
        mCameraParam.showFps = show;
        return this;
    }

    /**
     * 期望預覽幀率
     * @param fps
     * @return
     */
    public PreviewBuilder expectFps(int fps) {
        mCameraParam.expectFps = fps;
        return this;
    }

    /**
     * 期望寬度
     * @param width
     * @return
     */
    public PreviewBuilder expectWidth(int width) {
        mCameraParam.expectWidth = width;
        return this;
    }

    /**
     * 期望高度
     * @param height
     * @return
     */
    public PreviewBuilder expectHeight(int height) {
        mCameraParam.expectHeight = height;
        return this;
    }

    /**
     * 是否高清拍照
     * @param highDefinition
     * @return
     */
    public PreviewBuilder highDefinition(boolean highDefinition) {
        mCameraParam.highDefinition = highDefinition;
        return this;
    }

    /**
     * 是否打開后置攝像頭
     * @param backCamera
     * @return
     */
    public PreviewBuilder backCamera(boolean backCamera) {
        mCameraParam.backCamera = backCamera;
        if (mCameraParam.backCamera) {
            mCameraParam.cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
        }
        return this;
    }

    /**
     * 對焦權重
     * @param weight
     * @return
     */
    public PreviewBuilder focusWeight(int weight) {
        mCameraParam.setFocusWeight(weight);
        return this;
    }

    /**
     * 是否允許錄制
     * @param recordable
     * @return
     */
    public PreviewBuilder recordable(boolean recordable) {
        mCameraParam.recordable = recordable;
        return this;
    }

    /**
     * 錄制時間
     * @param recordTime
     * @return
     */
    public PreviewBuilder recordTime(int recordTime) {
        mCameraParam.recordTime = recordTime;
        return this;
    }

    /**
     * 是否錄制音頻
     * @param recordAudio
     * @return
     */
    public PreviewBuilder recordAudio(boolean recordAudio) {
        mCameraParam.recordAudio = recordAudio;
        return this;
    }

    /**
     * 延時拍攝
     * @param takeDelay
     * @return
     */
    public PreviewBuilder takeDelay(boolean takeDelay) {
        mCameraParam.takeDelay = takeDelay;
        return this;
    }

    /**
     * 是否開啟夜光補償
     * @param luminousEnhancement
     * @return
     */
    public PreviewBuilder luminousEnhancement(boolean luminousEnhancement) {
        mCameraParam.luminousEnhancement = luminousEnhancement;
        return this;
    }

    /**
     * 設置拍照監(jiān)聽器
     * @param listener
     * @return
     */
    public PreviewBuilder setPreviewCaptureListener(OnPreviewCaptureListener listener) {
        mCameraParam.captureListener = listener;
        return this;
    }

    /**
     *
     * @param listener
     * @return
     */
    public PreviewBuilder setGalleryListener(OnGallerySelectedListener listener) {
        mCameraParam.gallerySelectedListener = listener;
        return this;
    }

    /**
     * 打開預覽
     * @param requestCode
     */
    public void startPreviewForResult(int requestCode) {
        Activity activity = mPreviewEngine.getActivity();
        if (activity == null) {
            return;
        }
        Intent intent = new Intent(activity, CameraActivity.class);
        Fragment fragment = mPreviewEngine.getFragment();
        if (fragment != null) {
            fragment.startActivityForResult(intent, requestCode);
        } else {
            activity.startActivityForResult(intent, requestCode);
        }
    }

    /**
     * 打開預覽
     */
    public void startPreview() {
        Activity activity = mPreviewEngine.getActivity();
        if (activity == null) {
            return;
        }
        Intent intent = new Intent(activity, CameraActivity.class);
        Fragment fragment = mPreviewEngine.getFragment();
        if (fragment != null) {
            fragment.startActivity(intent);
        } else {
            activity.startActivity(intent);
        }
    }
}

然后我們暴露一個預覽引擎對象:

public final class PreviewEngine {

    private WeakReference<Activity> mWeakActivity;
    private WeakReference<Fragment> mWeakFragment;

    private PreviewEngine(Activity activity) {
        this(activity, null);
    }

    private PreviewEngine(Fragment fragment) {
        this(fragment.getActivity(), fragment);
    }

    private PreviewEngine(Activity activity, Fragment fragment) {
        mWeakActivity = new WeakReference<>(activity);
        mWeakFragment = new WeakReference<>(fragment);
    }

    public static PreviewEngine from(Activity activity) {
        return new PreviewEngine(activity);
    }

    public static PreviewEngine from(Fragment fragment) {
        return new PreviewEngine(fragment);
    }

    /**
     * 設置長寬比
     * @param ratio
     * @return
     */
    public PreviewBuilder setCameraRatio(AspectRatio ratio) {
        return new PreviewBuilder(this, ratio);
    }

    public Activity getActivity() {
        return mWeakActivity.get();
    }

    public Fragment getFragment() {
        return mWeakFragment.get();
    }
}
  • 打開預覽頁面的調(diào)用方式
    這樣项秉,我們就可以在使用的時候打開預覽頁面時傳入不同的預覽參數(shù)配置绣溜,使用如下所示:
PreviewEngine.from(this)
                .setCameraRatio(AspectRatio.Ratio_16_9)
                .showFacePoints(false)
                .showFps(true)
                .setGalleryListener(new OnGallerySelectedListener() {
                    @Override
                    public void onGalleryClickListener(GalleryType type) {
                        scanMedia(type == GalleryType.ALL);
                    }
                })
                .setPreviewCaptureListener(new OnPreviewCaptureListener() {
                    @Override
                    public void onMediaSelectedListener(String path, GalleryType type) {
                        if (type == GalleryType.PICTURE) {
                            Intent intent = new Intent(MainActivity.this, ImageEditActivity.class);
                            intent.putExtra(ImageEditActivity.PATH, path);
                            startActivity(intent);
                        } else if (type == GalleryType.VIDEO) {
                            Intent intent = new Intent(MainActivity.this, VideoEditActivity.class);
                            intent.putExtra(VideoEditActivity.PATH, path);
                            startActivity(intent);
                        }
                    }
                })
                .startPreview();

預覽整體流程慷彤,這里就介紹完了娄蔼。
詳細實現(xiàn)過程請參照本人的項目:
CainCamera

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市底哗,隨后出現(xiàn)的幾起案子岁诉,更是在濱河造成了極大的恐慌,老刑警劉巖跋选,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涕癣,死亡現(xiàn)場離奇詭異,居然都是意外死亡前标,警方通過查閱死者的電腦和手機坠韩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炼列,“玉大人只搁,你說我怎么就攤上這事〖蠹猓” “怎么了氢惋?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長稽犁。 經(jīng)常有香客問我焰望,道長,這世上最難降的妖魔是什么已亥? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任熊赖,我火速辦了婚禮,結果婚禮上虑椎,老公的妹妹穿的比我還像新娘秫舌。我一直安慰自己,他們只是感情好绣檬,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布足陨。 她就那樣靜靜地躺著,像睡著了一般娇未。 火紅的嫁衣襯著肌膚如雪墨缘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音镊讼,去河邊找鬼宽涌。 笑死,一個胖子當著我的面吹牛蝶棋,可吹牛的內(nèi)容都是我干的卸亮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼玩裙,長吁一口氣:“原來是場噩夢啊……” “哼兼贸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吃溅,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤溶诞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后决侈,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螺垢,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年赖歌,在試婚紗的時候發(fā)現(xiàn)自己被綠了枉圃。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡庐冯,死狀恐怖孽亲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肄扎,我是刑警寧澤墨林,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站犯祠,受9級特大地震影響旭等,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜衡载,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一搔耕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痰娱,春花似錦弃榨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至坡贺,卻和暖如春官辈,著一層夾襖步出監(jiān)牢的瞬間箱舞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工拳亿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晴股,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓肺魁,卻偏偏與公主長得像电湘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鹅经,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容