android Camera(一):采集与预览尺寸适配

android系统自带的拍摄有时候并不能满足我们所需要的功能,比如扫描,音视频开发等,此时就需要开发者自定义相机Camera,而开发过相机的同学相信应该都遇到过尺寸适配的问题,这里就讲述如何彻底解决这种适配问题

\color{red}{补充:这里主要是讲述在选择的最佳预览尺寸仍与屏幕的尺寸比例不一致的情况}

一:尺寸适配方案

在自定义相机的时候,相机的预览尺寸与实际拍摄的手机像素尺寸如果宽高比例不一致的时候就会出现图片拉伸变胖或者变瘦, 拍出来的照片或者视频与预览的效果有差异, 而android市面上的机型像素也是各种各样的,例如最常用的拍摄照片1920-1080的预览, 拍摄视频时采用1280-720(视频编码尽量不要使用过大的预览尺寸), 这种预览尺寸也是大部分手机支持, 而8.0以后大部分手机高宽比例已经远超过了1.778:1甚至大多都超过2:1, 而getSupportedPreviewSizes()中又很少有如此比例的预览尺寸,或者实际开发有限制预览尺寸的大小, 因此对于这种尺寸的适配由其重要, 对于尺寸适配有两种方案

方案1:根据预览的尺寸,将屏幕的大小根据预览比例调整界面的布局,此种方式的原理是当屏幕尺寸与预览尺寸比例不一致时,截取屏幕的部分出来与预览一致,如图黑色部分即用来显示Camera的预览
修改屏幕布局不修改预览方案
此种适配方式
缺点:界面截取一部分后多出的布局会让界面显得很丑很丑,极丑(google的camera示例代码就是这样)
\color{red}{优点:无论是拍照还是拍视频,输出的数据可以直接使用,不需要裁剪}
方案2:根据预览的尺寸,将SurfaceView或者TextureView显示时的画布按屏幕进行比例缩放,偏移再展示到SurfaceView或者TextureView中,此种方式的原理与第1种方式相反,当屏幕尺寸与预览尺寸不一致时,截取预览部分出来与屏幕一致, 如图黑色为屏幕显示
修改预览不修改布局方案
此种适配方式
\color{red}{缺点:1:代码逻辑相对复杂,拍出的照片或者是视频数据每一帧都需要进行比例裁剪处理}

\color{red}{MediaCode编码时,每一帧YUV数据的剪切并转换会复杂很多}(后面会一并讲述)

\color{red}{2:手动聚焦时也需要按裁剪比例来计算聚焦点}
\color{red}{3:手机方向感应改变时,此时数据的旋转剪切逻辑会相对复杂}
\color{red}{4:高宽比例不同的手机拍出来的视频高宽比例也会有差异}
\color{red}{优点:界面永远不会出现截取多余的部分,界面漂亮,任何布局都可以兼容}

结论:一款APP是决不允许出现截取多余布局这样丑的界面,所以如果要适配采集数据与预览的适配,坚决是使用第二种适配方式

二:尺寸适配实现(这里只介绍方案二,方案一比较简单)

直接上代码,CameraHelper完整代码,包含裁剪后的手动聚焦区域计算,闪光,缩放,方向感应

/**
 * Created by you on 2018-03-18.
 * 拍照, 视频, H264, 扫描的Camera各版本之间的兼容操作, 并支持 手动聚焦, 缩放, 闪光
 * https://developer.android.google.cn/guide/topics/media/camera#metering-focus-areas,参考
 */
public final class CameraHelper implements Camera.PreviewCallback {

    /**
     * 是否支持相机
     * @return
     */
    public static boolean isSupportCamera() {
        return Camera.getNumberOfCameras() > 0;
    }

    public static boolean isSupportFrontCamera() {
        final int cameraCount = Camera.getNumberOfCameras();
        Camera.CameraInfo info = new Camera.CameraInfo();
        for (int i = 0; i < cameraCount; i++) {
            Camera.getCameraInfo(i, info);
            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
                return true;
            }
        }
        return false;
    }

    /**
     * camera
     */
    private Camera mCamera;
    //预览字节缓存
    private byte[] buffer;
    //预览回调,在此回调中处理数据
    private Camera.PreviewCallback callback;
    //预览大小
    private Camera.Size preSize;
    /**
     * 当前相机的相关信息
     */
    private Camera.CameraInfo cameraInfo;

    public CameraHelper() {
        cameraInfo = new Camera.CameraInfo();
    }

    public void setPreviewCallback(Camera.PreviewCallback callback) {
        this.callback = callback;
    }

    public Camera.Size getPreSize() {
        return preSize;
    }

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if (callback != null) {
            callback.onPreviewFrame(data, camera);
        }
        camera.addCallbackBuffer(buffer);
    }

    public Matrix openPicCamera(SurfaceTexture texture, int cameraId, int w, int h,
                                SizeFilter filter, int orientation) {
        return openCamera(texture, cameraId, w, h, filter,
                Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, -1, -1);
    }

    public Matrix openVideoCamera(SurfaceTexture texture, int cameraId, int w, int h,
                                  SizeFilter filter, int minFps, int maxFps) {
        return openCamera(texture, cameraId, w, h, filter,
                Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, minFps, maxFps);
    }

    public Matrix openScanCamera(SurfaceTexture texture, int w, int h,
                                 SizeFilter filter, int minFps, int maxFps) {
        return openCamera(texture, Camera.CameraInfo.CAMERA_FACING_BACK, w, h, filter,
                Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, minFps, maxFps);
    }

    /**
     *
     * @param texture
     * @param cameraId {@link Camera.CameraInfo#CAMERA_FACING_BACK}
     *                  or {@link Camera.CameraInfo#CAMERA_FACING_FRONT}
     * @param w
     * @param h
     * @param filter
     * @param focusMode 聚焦模式
     * @param minFps h264时的最小帧率, 不需要时可传-1
     * @param maxFps h264时的最大帧率, 不需要时可传-1
     * @return
     */
    public Matrix openCamera(SurfaceTexture texture, int cameraId, int w, int h,
                             SizeFilter filter, String focusMode, int minFps, int maxFps) {
        if (mCamera == null) {
            mCamera = Camera.open(cameraId);

            try {
                mCamera.setDisplayOrientation(90);//默认竖直拍照
                Camera.getCameraInfo(cameraId, cameraInfo);

                Camera.Parameters parameters = mCamera.getParameters();
                //设置聚焦类型
                List<String> focusModes = parameters.getSupportedFocusModes();
                if (focusMode != null && focusModes.contains(focusMode)) {
                    parameters.setFocusMode(focusMode);
                }
                //设置Pic, Pre尺寸, 视频时可以忽略Pic尺寸
                Camera.Size picSize = filter.findOptimalPicSize(parameters.getSupportedPictureSizes(), w, h);
                if (picSize != null) {//拍摄图片时不可null
                    LogUtils.i("Camera Parameters picSize %d - %d", picSize.width, picSize.height);
                    parameters.setPictureSize(picSize.width, picSize.height);
                    //设置生成图片的旋转角度,只对拍摄照片时有效.,考虑到此方法对部分机型没有效果,拍照后再统一旋转
                    //parameters.setRotation(getCameraRotation(orientation));
                    parameters.setPictureFormat(ImageFormat.JPEG);
                }
                preSize = filter.findOptimalPreSize(parameters.getSupportedPreviewSizes(), picSize, w, h);
                if (preSize == null) throw new IllegalArgumentException("There is no matching presize");
                LogUtils.i("Camera Parameters preSize %d - %d", preSize.width, preSize.height);
                parameters.setPreviewSize(preSize.width, preSize.height);
                parameters.setPreviewFormat(ImageFormat.NV21);
                //设置帧率
                if (minFps > 0 && maxFps > 0) {
                    initPreviewFpsRange(parameters, minFps, maxFps);
                }
                mCamera.setParameters(parameters);

                buffer = new byte[preSize.width * preSize.height * 3 / 2];
                mCamera.setPreviewTexture(texture);
                mCamera.startPreview();
                mCamera.setPreviewCallbackWithBuffer(this);
                mCamera.addCallbackBuffer(buffer);
                return transformSurface(w, h, preSize.height, preSize.width);
            } catch (Exception e) {
                LogUtils.e(e);
                closeCamera();
            }
        }
        return null;
    }

    /**
     * 获取当前相机设备对应的旋转角度
     * @param orientation {@link OrientationHelper#getOrientation()}
     * @return 0, 90, 180, 270
     * @hide
     */
    public int getCameraRotation(int orientation) {
        if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            return (cameraInfo.orientation - orientation + 360) % 360;
        } else {
            return (cameraInfo.orientation + orientation) % 360;
        }
    }

    /**
     * 支持的闪光效果, 在开启相机之后获取
     * @return
     */
    public List<String> getSupportedFlashModes() {
        if (mCamera != null) {
            return mCamera.getParameters().getSupportedFlashModes();
        }
        return null;
    }

    /**
     * 设置当前闪光效果
     * @param flashMode
     */
    public void setFlashMode(String flashMode) {
        if (mCamera != null) {
            Camera.Parameters parameters = mCamera.getParameters();
            if (flashMode.equals(parameters.getFlashMode())) return;
            parameters.setFlashMode(flashMode);
            mCamera.setParameters(parameters);
        }
    }

    /**
     * 释放摄像头
     */
    public void closeCamera() {
        buffer = null;
        preSize = null;
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }

    /**
     * 拍摄照片
     * @param callback
     */
    public void takePicture(Camera.PictureCallback callback) {
        if (mCamera != null) {
            mCamera.takePicture(null, null, callback);
        }
    }

    /**
     * 缩放
     * @param targetRatio 放大的比例
     */
    public void handleZoom(float targetRatio) {
        if (mCamera == null) return;
        Camera.Parameters parameters = mCamera.getParameters();
        if (!parameters.isZoomSupported()) return;
        List<Integer> zoomRatios = parameters.getZoomRatios();
        if (zoomRatios == null || zoomRatios.isEmpty()) return;
        int zoom = indexByBinary(zoomRatios, targetRatio * 100);
        if (zoom == parameters.getZoom()) return;
        parameters.setZoom(zoom);
        mCamera.setParameters(parameters);
    }

    /**
     * 手动聚焦
     */
    public void handleFocus(PointF scalePointF) {
        handleFocus(scalePointF, 200, 300);
    }

    /**
     * 手动聚焦
     * @param scalePointF 聚焦点坐标与聚焦点所预览的界面宽高的比例
     * @param fsize 聚焦方形区域大小
     * @param msize 测光方形区域大小
     */
    public void handleFocus(PointF scalePointF, int fsize, int msize) {
        if (mCamera == null) return;
        Camera.Parameters parameters = mCamera.getParameters();
        //一般使用能自动聚焦的即可
        String autoFocusMode = findFocusbackMode(parameters);
        if (autoFocusMode == null) return;
        mCamera.cancelAutoFocus();
        if (parameters.getMaxNumFocusAreas() > 0) {//聚焦区域
            List<Camera.Area> focusAreas = new ArrayList<>();
            //聚焦区域
            Rect focusRect = calculateTapArea(scalePointF, fsize);
            focusAreas.add(new Camera.Area(focusRect, 800));
            parameters.setFocusAreas(focusAreas);
        } else {
            LogUtils.i("focus areas not supported");
        }
        if (parameters.getMaxNumMeteringAreas() > 0) {//测光区域
            List<Camera.Area> meteringAreas = new ArrayList<>();
            Rect meteringRect = calculateTapArea(scalePointF, msize);
            meteringAreas.add(new Camera.Area(meteringRect, 800));
            parameters.setMeteringAreas(meteringAreas);
        } else {
            LogUtils.i("metering areas not supported");
        }

        final String currentFocusMode = parameters.getFocusMode();
        parameters.setFocusMode(autoFocusMode);
        mCamera.setParameters(parameters);
        mCamera.autoFocus((success, camera) -> {
            Camera.Parameters params = camera.getParameters();
            params.setFocusMode(currentFocusMode);
            camera.setParameters(params);
            //如果有设置自动对焦回调时不可设置为null
            camera.autoFocus(null);
            LogUtils.i("autoFocus..." + success);
        });
    }

    /**
     * 获取最大支持的缩放比例, 最小就为1.0f初始大小
     * @return
     */
    public float getMaxZoomScale() {
        if (mCamera != null) {
            Camera.Parameters parameters = mCamera.getParameters();
            if (parameters.isZoomSupported()) {
                //getZoomRatios源码The list is sorted from small to large
                List<Integer> zoomRatios = parameters.getZoomRatios();
                if (zoomRatios != null && zoomRatios.size() == parameters.getMaxZoom() + 1) {
                    int minZoom = zoomRatios.get(0);
                    float maxZoom = zoomRatios.get(zoomRatios.size() - 1);
                    return maxZoom / minZoom;
                }
            }
        }
        return 1.0f;
    }

    /**
     * 根据实际预览的尺寸来计算Surface的缩放与移动大小
     * 必须把Surface显示的与实现拍摄的预览界面的比较一致, 先调整比例,再调整偏移, 这样预览的效果即不会拉伸,拍出来的效果也与实际一致
     * @param sw surface的宽
     * @param sh surface的高
     * @param prew Camera.Size 预览宽,注意相机的旋转90
     * @param preh Camera.Size 预览高,注意相机的旋转90
     * @return 返回的Matrix中包含着X轴或者Y轴的缩放比例与偏移
     */
     private Matrix transformSurface(int sw, int sh, int prew, int preh) {
        Matrix matrix = new Matrix();
        float preScale = preh / (float) prew;
        float viewScale = sh / (float) sw;
        if (preScale != viewScale) {//宽高比例不一样,才需要做处理
            if (preScale > viewScale) {//将高宽比例较大的放到屏幕上显示, 所以需要截掉预览的一部分高, 即Y轴偏移
                //按预览的宽与需要显示的宽比例调整预览的高度
                float scalePreY = sw * preScale;// preHeight * (sWidth / preWidth);
                //Y轴需要放大的比例
                matrix.preScale(1.0f, scalePreY / sh);
                float translateY = (sh - scalePreY) / 2;
                matrix.postTranslate(0, translateY);
//                LogUtils.i("transY %f , %f", scalePreY, translateY);
            } else {//屏幕显示高宽尺寸比例较小的,即X轴偏移
                float scalePreX = sh / preScale; //preWidth * (sHeight / preHeight);
                //x轴需要放大的比例
                matrix.preScale(scalePreX / sw, 1.0f);
                float translateX = (sw - scalePreX) / 2;
                matrix.postTranslate(translateX, 0);
//                LogUtils.i("transX %f , %f", scalePreX, translateX);
            }
        }
        return matrix;
    }

    /**
     * 初始相机预览帧率, 一般相机都支持7K~30K, 可以设置15~25, 帧率过高时, 旋转裁剪低配手机编码速度容易跟不上
     * @param parameters
     * @param minFps 最小帧率 K
     * @param maxFps 最大帧率 K
     */
    private void initPreviewFpsRange(Camera.Parameters parameters, int minFps, int maxFps) {
        List<int[]> supportedPreviewFpsRange = parameters.getSupportedPreviewFpsRange();
        if (supportedPreviewFpsRange == null || supportedPreviewFpsRange.isEmpty()) return;
        int[] suitableFPSRange = null;
        for (int[] fpsRange : supportedPreviewFpsRange) {
//            LogUtils.i("supportPreviewFps %d - %d", fpsRange[0], fpsRange[1]);
            if (fpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] >= minFps
                    && fpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] <= maxFps) {
                suitableFPSRange = fpsRange;
                break;
            }
        }
        if (suitableFPSRange != null) {
            int[] currentFpsRange = new int[2];
            parameters.getPreviewFpsRange(currentFpsRange);
            if (Arrays.equals(currentFpsRange, suitableFPSRange)) {
                return;
            }
            parameters.setPreviewFpsRange(suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
                    suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
        } else {
            LogUtils.i("No suitable FPS range in %d - %d", minFps, maxFps);
        }
    }

    /**
     * 焦点区域坐标点  (-1000, -1000, 1000, 1000),根据点坐标x,y轴与实际大小w,h比例计算出该点的区域大小
     * @param scalePointF 聚焦点坐标与聚焦点所预览的界面宽高的比例
     * @param areaSize 聚焦方形区域大小
     * @return
     */
    private Rect calculateTapArea(PointF scalePointF, int areaSize) {
        int centerX = (int) (scalePointF.x * 2000 - 1000);
        int centerY = (int) (scalePointF.y * 2000 - 1000);

        int left = clamp(centerX - areaSize / 2, -1000, 1000);
        int top = clamp(centerY - areaSize / 2, -1000, 1000);
        int right = clamp(left + areaSize, -1000, 1000);
        int bottom = clamp(top + areaSize, -1000, 1000);
        return new Rect(left, top, right, bottom);
    }

    /**
     * x值不能超出min~max范围
     */
    private int clamp(int x, int min, int max) {
        if (x > max) return max;
        if (x < min) return min;
        return x;
    }

    /**
     * 查找能回调出{@link Camera#autoFocus(Camera.AutoFocusCallback)}的聚焦模式
     * 源码注释中指出FOCUS_MODE_AUTO 与 FOCUS_MODE_MACRO 支持, 优先使用前者
     * @param parameters
     * @return
     */
    private String findFocusbackMode(Camera.Parameters parameters) {
        List<String> supportedFocusModes =  parameters.getSupportedFocusModes();
        if (supportedFocusModes != null && !supportedFocusModes.isEmpty()) {
            if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) return Camera.Parameters.FOCUS_MODE_AUTO;
            if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_MACRO)) return Camera.Parameters.FOCUS_MODE_MACRO;
        }
        return null;
    }

    /**
     * 二分查找最接近的
     * @param ints
     * @param target
     * @return
     */
    private int indexByBinary(List<Integer> ints, float target) {
        int low = 0;
        int high = ints.size() - 1;
        if (ints.size() == 1) return 0;
        if (target <= ints.get(low)) return low;
        if (target >= ints.get(high)) return high;
        int middle = 0;
        float left, right;
        while(low <= high) {
            middle = (low + high)/2;
            right = Math.abs(ints.get(middle + 1) - target);
            left = Math.abs(ints.get(middle) - target);
            if(right > left) {
                high = middle - 1;
            } else {
                low = middle + 1;
            }
        }
        right = Math.abs(ints.get(middle + 1) - target);
        left = Math.abs(ints.get(middle) - target);
        return right > left ? middle : middle + 1;
    }
}

TextureView部分代码,只要setTransform(matrix)即可,对于SurfaceView可能没有直接提供此方法,只需要将matrix的缩放与偏移值拆分成setTranslationX(); setScaleX();或者setTranslationY(); setScaleY()即可,原理还是一样

    //开启相机
    private void startCamera() {
        matrix = helper.openPicCamera(cv_camera.getSurfaceTexture(), cameraId,
                cv_camera.getWidth(), cv_camera.getHeight(), filter, orientationHelper.getOrientation());

        if (matrix != null) {
            cv_camera.setTransform(matrix);// TextureView cv_camera;
        }
        cv_camera.setMaxScale(helper.getMaxZoomScale());
        fv_flash.setFlashModes(helper.getSupportedFlashModes(), cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT);
    }
补充:这里的采集回调采用缓冲的方式,Camera在设置好预览尺寸后,采集的数据大小byte[]都是一样大,使用缓冲addCallbackBuffer的方式可以避免内存的反复快速创建与回收抖动,在使用MediaCodec时也应当考虑对需要处理的byte[]进行缓存处理
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.addCallbackBuffer(buffer);
有一部分手机在拍照片时,parameters.setRotation()不能见效,对于这种情况其实也好处理,Camera的parameters不设置此属性, 统一在拍照生成的data[]里进行旋转即可
parameters.setPictureSize(picSize.width, picSize.height);
//设置生成图片的旋转角度,只对拍摄照片时有效.,考虑到此方法对部分机型没有效果,拍照后再统一旋转
 //parameters.setRotation(getCameraRotation(orientation));
parameters.setPictureFormat(ImageFormat.JPEG);

\color{red}{Camera拍照,视频,MediaCodec编码,二维码/条码 扫描兼容操作,方向感应操作}
\color{red}{自动聚焦, 手动聚焦(预览适配剪切后的聚焦点处理), 缩放, 闪光}

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

推荐阅读更多精彩内容