视频录制和自定义拍照

最近在学习视频录制方面的东西,在网上找了篇博客,写的非常详细,这里是对这篇文章的学习

词汇介绍

  • AVCaptureSession: 媒体(音频、视频)捕捉会话,负责把捕捉的音视频数据输出到输出设备,一个捕捉会话可以有多个输入输出
  • AVCaptureDevice: 输入设备 包括摄像头、话筒等,通过该对象可以设置物理设备的属性(相机的聚焦白平衡等)
  • AVCaptureDeviceInput: 输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理
  • AVCaptureOutput:输出数据管理对象,用于接受各类输出数据,通常使用其子类AVCaptureAudioDataOutput、AVCaptureStillImageOutput、AVCaptureVideoDataOutput、AVCaptureFileOutput,该对象将会被添加到AVCaptureSession中管理。注意:前面几个对象的输出数据都是NSData类型,而AVCaptureFileOutput代表数据以文件形式输出,类似的,AVCcaptureFileOutput也不会直接创建使用,通常会使用其子类:AVCaptureAudioFileOutput、AVCaptureMovieFileOutput。当把一个输入或者输出添加到AVCaptureSession
  • AVCaptrueVideoPreviewLayer: 相机拍摄预览图层,是CAPlayer的子类,使用该对象可以实时查看拍摄和视频录制的效果,创建该对象需要指定对应的AVCaptureSession对象

使用AVFoundation框架实现拍照和视频录制的一般步骤

  1. 创建AVCaptureSession对象
  2. 使用AVCaptureDevice静态方法获得所需的设备,例如拍照和视频就需要获取摄像头设备,录音就需要获取话筒设备
  3. 利用输入设备AVCaptureDevice创建AVCaptureDeviceInput对象
  4. 初始化数据输出管理对象,如果要拍照就初始化AVCaptureStillImageOutput对象,如果要录制视频就初始化AVCaptureMovieFileOutput对象
  5. 将数据输入对象(AVCaptureDeviceInput)、数据输出对象(AVCaptureOutput)添加进媒体会话AVCaptureSession中
  6. 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器中,并调用AVCaptureSession的startRunning方法开始捕捉
  7. 将捕获的音频或者视频保存到指定文件

自定义拍照

我们将实现摄像头预览,摄像头切换,闪光灯设置,对焦,拍照保存等功能

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    //1.初始化捕捉会话
    //1.1.初始化
    _session = [[AVCaptureSession alloc] init];
    //1.2.设置分辨率
    if ([_session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
        [_session setSessionPreset:AVCaptureSessionPreset1280x720];
    }
    
    //2.获得输入设备 后置摄像头
    AVCaptureDevice *captureDevice = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
    if (captureDevice == nil) {
        NSLog(@"获取后置摄像头失败");
        return;
    }
    NSError *error = nil;
    //3.根据输入设备创建输入数据管理对象
    AVCaptureDeviceInput *captureInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&error];
    self.captureDeviceInput = captureInput;
    if (error) {
        NSLog(@"取得设备输入对象时出错,%@", error.localizedDescription);
        return;
    }
    
    //4.创建输出数据管理对象 用于获得输出数据
    AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];
    NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
    [imageOutput setOutputSettings:outputSettings];//输出设置
    self.imageOutput = imageOutput;
    
    //5.添加输入设备管理对象到捕捉会话
    if ([_session canAddInput:_captureDeviceInput]) {
        [_session addInput:_captureDeviceInput];
    }
    
    //6.添加输出源
    if ([_session canAddOutput:_imageOutput]) {
        [_session addOutput:_imageOutput];
    }
    
    //7.设置预览图层
    _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_session];
    _previewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
    _previewLayer.frame = self.view.bounds;
    [self.view.layer insertSublayer:_previewLayer atIndex:0];
    
    //8.给设备添加通知 监测监控区域的变化
    [self addNotificationToCaptureDevice:captureDevice];
    
    //9.添加手势
    [self addGestureToView];
}



viewWillApper:方法里面创建媒体捕捉会话,并添加输入源、输出源,添加对输入设备的通知,监测设备监控区域的变化(拍照对准的区域发生变化等等),添加手势来聚焦和调整光标位置,在viewDidAppear:方法中开始会话捕捉,在 viewDidDisappear:中停止会话捕捉

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    [_session startRunning];
}
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [_session stopRunning];
}

定义闪光灯开闭及自动模式功能,注意无论是设置闪光灯、白平衡还是其他输入设备属性,在设置之前必须先锁定配置,修改完后解锁。

/* 定义闪光灯开闭及自动模式功能,注意无论是设置闪光灯、白平衡还是其他输入设备属性,在设置之前必须先锁定配置,修改完后解锁。 */
/* 改变设备属性 进行的是锁住设备操作 通过block返回输入设备 */
- (void)changeDeviceProperty:(PropertyChangeBlock)block {
    //1.获得设备
    AVCaptureDevice *device = self.captureDeviceInput.device;
    NSError *error;
    //2.锁住设备
    BOOL success = [device lockForConfiguration:&error];
    if (success) {
        //1.锁定成功 通过block返回输入设备
        block(device);
        //2.解锁
        [device unlockForConfiguration];
    }
    else {
        NSLog(@"设置设备属性过程发生错误,错误信息%@", error.localizedDescription);
    }
    
}

添加通知的方法

- (void)addNotificationToCaptureDevice:(AVCaptureDevice *)device {
    //1.先锁住输入设备
    [self changeDeviceProperty:^(AVCaptureDevice *device) {
        //添加区域改变捕获通知必须首先设置设备允许捕获
        //表明接收方是否应该监控领域的变化(如照明变化,实质移动等) 可以通过AVCaptureDeviceSubjectAreaDidChangeNotification通知监测 我们可以希望重新聚焦,调整曝光白平衡等的主题区域 在设置该属性之前必须调用lockForConfiguration方法锁定设备配置
        device.subjectAreaChangeMonitoringEnabled = YES;
    }];
    
    //2.监测通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(areaChanged:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:device];
    
}

在通知方法里打印下log

#pragma mark -设备捕获区域发生变化
- (void)areaChanged:(NSNotification *)notification {
       NSLog(@"捕获区域改变...");
}

移除通知的方法

/* 移除监控输入设备通知 */
- (void)removeNotificationFromCaptureDevice:(AVCaptureDevice *)device {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:device];
}

添加手势


- (void)addGestureToView {    
 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapScreen:)];
    [self.view addGestureRecognizer:tap];
}
-(void)tapScreen:(UITapGestureRecognizer *)tapGesture {
    CGPoint point = [tapGesture locationInView:self.view];
    //UI坐标转换成摄像头坐标
    CGPoint cameraPoint = [self.previewLayer captureDevicePointOfInterestForPoint:point];
    [self setFocusPoint:cameraPoint];
    
    [self focusWithMode:AVCaptureFocusModeAutoFocus exposureMode:AVCaptureExposureModeAutoExpose atPoint:cameraPoint];
}

在手势方法里面设置光标位置(光标是一个imageView),并设置摄像头的聚焦模式和曝光模式

设置光标位置的方法

/* 设置光标位置 */
- (void)setFocusPoint:(CGPoint)point {
    self.imageView.center = point;
    
    self.imageView.transform = CGAffineTransformMakeScale(1.5, 1.5);
    self.imageView.alpha = 1;
    
    [UIView animateWithDuration:1.0 animations:^{
        self.imageView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        self.imageView.alpha = 0;
    }];
    
}

设置聚焦模式和曝光模式的方法

/* s设置聚焦和曝光模式 */
- (void)focusWithMode:(AVCaptureFocusMode)focusMode
         exposureMode:(AVCaptureExposureMode)exposeMode
              atPoint:(CGPoint)point
{
    //设置曝光模式和聚焦模式 先锁住输入设置
    [self changeDeviceProperty:^(AVCaptureDevice *device) {
        if ([device isFocusModeSupported:focusMode]) {
            [device setFocusMode:focusMode];
        }
        
        if ([device isExposureModeSupported:exposeMode]) {
            [device setExposureMode:exposeMode];
        }
        
        if ([device isFocusPointOfInterestSupported]) {
            [device setFocusPointOfInterest:point];
        }
        
        if ([device isExposurePointOfInterestSupported]) {
            [device setExposurePointOfInterest:point];
        }
        
    }];
}

拍照的方法

#pragma mark -拍照
- (IBAction)takePhoto:(id)sender {
    //1.根据数据输出管理对象(输出源)获得链接
    AVCaptureConnection *connection = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];
    //2.根据连接取得输出数据
    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
        //获取图像数据
        NSData *imageData=[AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
        UIImage *image=[UIImage imageWithData:imageData];
        //存入相册
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
    }];
}

切换摄像头

切换摄像头就是移除原有的输入源,添加新的输入源

#pragma mark -切换摄像头
/* 切换摄像头的过程就是将原有的输入源移除 添加新的输入源到会话中 */
- (IBAction)switchCamera:(id)sender {
    //1.获取原来的输入设备 根据数据输入管理对象获取
    AVCaptureDevice *oldCaptureDevice = [self.captureDeviceInput device];
    //2.移除输入设备的通知
    [self removeNotificationFromCaptureDevice:oldCaptureDevice];
    
    //3.切换摄像头的位置
    //3.1.获取当前的位置
    AVCaptureDevicePosition currentPosition = oldCaptureDevice.position;
    //3.2.获取要切换的位置
    AVCaptureDevicePosition targetPosition = AVCaptureDevicePositionFront;
    if (currentPosition == AVCaptureDevicePositionFront || AVCaptureDevicePositionUnspecified) {
        targetPosition = AVCaptureDevicePositionBack;
    }
    
    //4.根据摄像头的位置获取当前的输入设备
    AVCaptureDevice *currentCaptureDevice = [self getCameraDeviceWithPosition:targetPosition];
    
    //5.添加对当前输入设备的通知
    [self addNotificationToCaptureDevice:currentCaptureDevice];
    
    //6.创建当前设备的数据输入管理对象
    AVCaptureDeviceInput *currentInput = [[AVCaptureDeviceInput alloc] initWithDevice:currentCaptureDevice error:nil];
    
    //7.添加新的数据管理对象到捕捉会话
    //7.1.开始设置
    [_session beginConfiguration];
    //7.2.移除原有的输入源
    [_session removeInput:self.captureDeviceInput];
    
    //7.3.添加新的输入源
    if ([_session canAddInput:currentInput]) {
        [_session addInput:currentInput];
        //.标记当前的输入源
        self.captureDeviceInput = currentInput;
    }
    
    
    //7.4.提交设置
    [_session commitConfiguration];
}

根据摄像头的位置(前置/后置)创建输入设备

/* 根据摄像头位置来获取摄像头 */
- (AVCaptureDevice *)getCameraDeviceWithPosition:(AVCaptureDevicePosition)position {
    NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device in cameras) {
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}

需要说明的是在beginConfiguration方法需要和commitConfiguration方法配套使用,我们可以在这之间可以添加或删除输出,更改sessionPreset或配置单个AVCaptureInput或Output属性

这里基本上就已经实现了拍照功能,还有一些就是设置闪光灯的模式了
这里闪光模式一共有三种


typedef NS_ENUM(NSInteger, AVCaptureFlashMode) {
    AVCaptureFlashModeOff  = 0,
    AVCaptureFlashModeOn   = 1,
    AVCaptureFlashModeAuto = 2
} NS_AVAILABLE(10_7, 4_0) __TVOS_PROHIBITED;

设置闪光模式

#pragma mark - 设置闪光灯模式
- (void)setFlashMode:(AVCaptureFlashMode)mode {
    //先锁住设备
    [self changeDeviceProperty:^(AVCaptureDevice *device) {
        if ([device isFlashModeSupported:mode]) {//如果支持该模式
            [device setFlashMode:mode];
        }
    }];
}

大致效果为

拍照.gif

录制视频

录制视频跟拍照大致差不多,比拍照要多一个麦克风的数据输入管理对象(麦克风输入源),拍照的话是创建AVCaptureStillImageOutput类型的输出缘,录制视频的话是AVCaptureMovieFileOutput类型

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    //初始化捕捉会话
    _session = [[AVCaptureSession alloc] init];
    if ([_session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
        [_session setSessionPreset:AVCaptureSessionPreset1280x720];
    }
    
    //获得相机输入设备
    AVCaptureDevice *cameraDevice = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];
    if (cameraDevice == nil) {
        NSLog(@"获取后置摄像头失败");
        return;
    }
    
    //根据相机输入设备创建相机输入源
    NSError *error = nil;
    _cameraDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:cameraDevice error:&error];
    if (error) {
        NSLog(@"%@", error.localizedDescription);
        return;
    }
    
    //获得话筒输入设备
    AVCaptureDevice *audioDevice = [AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio].firstObject;
    //创建话筒输入源
    _audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];
    
    //创建数据输出管理对象
    _movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
    
    //添加输入源
    if ([_session canAddInput:_cameraDeviceInput]) {
        [_session addInput:_cameraDeviceInput];
    }
    AVCaptureConnection *connection = [_movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
    if ([connection isVideoStabilizationSupported]) {
        
        connection.preferredVideoStabilizationMode=AVCaptureVideoStabilizationModeAuto;//通过将preferredVideoStabilizationMode属性设置为AVCaptureVideoStabilizationModeOff以外的值,当模式可用时,流经接收器的视频会稳定
    }
    
    if ([_session canAddInput:_audioDeviceInput]) {
        [_session addInput:_audioDeviceInput];
    }
    
    //添加输出源
    if ([_session canAddOutput:_movieFileOutput]) {
        [_session addOutput:_movieFileOutput];
    }
    //创建预览图层
    _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_session];
    _previewLayer.frame = self.view.bounds;
    _previewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
    
    [self.view.layer insertSublayer:_previewLayer atIndex:0];
    
}


开始录制视频

- (IBAction)startRecording:(UIButton *)sender {
   
   if ([self.movieFileOutput isRecording]) {
       [self.movieFileOutput stopRecording];
       sender.selected = YES;
       return;
   }
   
   //获得连接
   AVCaptureConnection *connection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo];
   connection.videoOrientation = self.previewLayer.connection.videoOrientation;
   
   NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingPathComponent:@"myVideo.mp4"];
   NSURL *url = [NSURL fileURLWithPath:filePath];
   //开始录制 并设置代理
   [self.movieFileOutput startRecordingToOutputFileURL:url recordingDelegate:self];
}

遵守AVCaptureFileOutputRecordingDelegate协议并实现代理方法

下面这个方法是必须实现的

#pragma mark - AVCaptureFileOutputRecordingDelegate
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
    NSLog(@"视频录制完成");
    //将视频存入到相簿
    ALAssetsLibrary *assetsLibrary=[[ALAssetsLibrary alloc]init];
    [assetsLibrary writeVideoAtPathToSavedPhotosAlbum:outputFileURL completionBlock:^(NSURL *assetURL, NSError *error) {
        if (error) {
            NSLog(@"保存视频到相簿过程中发生错误,错误信息:%@",error.localizedDescription);
        }
        
        NSLog(@"成功保存视频到相簿.");
    }];
}

官方文档是这么介绍该方法的

This method is called when the file output has finished writing all data to a file whose recording was stopped, either because startRecordingToOutputFileURL:recordingDelegate: or stopRecording were called, or because an error, described by the error parameter, occurred (if no error occurred, the error parameter will be nil). This method will always be called for each recording request, even if no data is successfully written to the file.

大致意思是

当文件输出已完成将所有数据写入记录已停止的文件时,或者因为调用了startRecordingToOutputFileURL:recordingDelegate:或stopRecording,或者因为发生了由错误参数描述的错误(如果没有发生错误, 错误参数将为nil)。 即使没有数据成功写入文件,也会为每个记录请求调用此方法。

开始录制时的代理方法

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
    NSLog(@"开始录制视频");
}

切换摄像头的方法

- (IBAction)switchCamera:(id)sender {
    //1.获得原来的输入设备
    AVCaptureDevice *oldCaptureDevice = [self.cameraDeviceInput device];
    //2.移除原来输入设备的通知
    [self removeNotificationFromDevice:oldCaptureDevice];
    //3.获得现在的输入设备
    //3.1.获得原来的设备的位置
    AVCaptureDevicePosition oldPosition = oldCaptureDevice.position;
    //3.2.获得现在的设备的位置
    AVCaptureDevicePosition currentPosition = AVCaptureDevicePositionFront;
    if (oldPosition == AVCaptureDevicePositionFront || oldPosition == AVCaptureDevicePositionUnspecified) {
        currentPosition = AVCaptureDevicePositionBack;
    }
    //3.3.根据位置创建当前的输入设备
    AVCaptureDevice *currnetCaptureDevice = [self getCameraDeviceWithPosition:currentPosition];
    //3.4.给当前设备添加通知
    [self addNotificationToCaptureDevice:currnetCaptureDevice];
    
    //4.根据现在的输入设备创建输入源
    NSError *error = nil;
    AVCaptureDeviceInput *currentInput = [AVCaptureDeviceInput deviceInputWithDevice:currnetCaptureDevice error:&error];
    if (error) {
        NSLog(@"%@",error.localizedDescription);
        return;
    }
    
    //5.更换输入源
    //5.1.开启设置
    [_session beginConfiguration];
    //5.2.移除原来的输入源
    [_session removeInput:self.cameraDeviceInput];
    //5.3.添加现在的输入源
    if ([_session canAddInput:currentInput]) {
        [_session addInput:currentInput];
        self.cameraDeviceInput = currentInput;
    }
    //5.4.提交设置
    [_session commitConfiguration];
}

可以看出跟拍照的切换是一致的

大致效果

视频.gif

还有一点需要注意的是因为这里调用了相机和话筒设备,所以需要在info.plist中添加相应的字段

CCF99CA8-86F7-4721-A89C-9A3F7986CD74.png

代码地址:点击这里

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

推荐阅读更多精彩内容