iOS音视频: 拍照和录制视频

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、使用 UIImagePickerController 拍照和视频录制
    • 1、UIImagePickerController 简介
    • 2、UIImagePickerController 属性和方法
    • 3、UIImagePickerController Demo演示
  • 二、使用 AVFoundation 拍照和录制视频
    • 1、AVFoundation 简介
    • 2、摄像头录制工具类提供的属性和方法
    • 3、录制工具类的Session
    • 4、切换前后摄像头
    • 5、使用捕捉设备进行聚焦和曝光
    • 6、实现摄像头手电筒和闪关灯模式的开启关闭
    • 7、静态图片的拍摄
    • 8、视频录制实现
    • 9、实现预览录制内容功能
  • Demo
  • 参考文献

一、使用 UIImagePickerController 拍照和视频录制

1、UIImagePickerController 简介

UIImagePickerController继承于UINavigationController平时主要使用它来选取照片,其实UIImagePickerController的功能不仅如此,它还可以用来拍照和录制视频。当然这个过程中有很多细节可以设置,例如是否显示拍照控制面板,拍照后是否允许编辑等等。要用UIImagePickerController来拍照或者录制视频通常可以分为如下步骤:

  1. 创建UIImagePickerController对象。
  2. 指定拾取源,平时选择照片时使用的拾取源是照片库或者相簿,此刻需要指定为摄像头类型。
  3. 指定摄像头,前置摄像头或者后置摄像头。
  4. 设置媒体类型mediaType,注意如果是录像必须设置,如果是拍照此步骤可以省略,因为mediaType默认包含kUTTypeImage(注意媒体类型定义在MobileCoreServices.framework中)
  5. 指定捕获模式,拍照或者录制视频。(视频录制时必须先设置媒体类型再设置捕获模式)
  6. 展示UIImagePickerController(通常以模态窗口形式打开)。
  7. 拍照和录制视频结束后在代理方法中展示/保存照片或视频。

2、UIImagePickerController 属性和方法

a、枚举类型
数据源类型,sourceType是枚举类型
@property(nonatomic) UIImagePickerControllerSourceType sourceType

UIImagePickerControllerSourceTypePhotoLibrary:照片库,默认值
UIImagePickerControllerSourceTypeCamera:摄像头
UIImagePickerControllerSourceTypeSavedPhotosAlbum:相簿
视频质量,枚举类型
@property(nonatomic) UIImagePickerControllerQualityType videoQuality

UIImagePickerControllerQualityTypeHigh:高清质量
UIImagePickerControllerQualityTypeMedium:中等质量,适合WiFi传输
UIImagePickerControllerQualityTypeLow:低质量,适合蜂窝网传输
UIImagePickerControllerQualityType640x480:640*480
UIImagePickerControllerQualityTypeIFrame1280x720:1280*720
UIImagePickerControllerQualityTypeIFrame960x540:960*540
摄像头捕获模式,捕获模式是枚举类型
@property(nonatomic) UIImagePickerControllerCameraCaptureMode 

UIImagePickerControllerCameraCaptureModePhoto:拍照模式
UIImagePickerControllerCameraCaptureModeVideo:视频录制模式
摄像头设备,cameraDevice是枚举类
@property(nonatomic) UIImagePickerControllerCameraDevice cameraDevice

UIImagePickerControllerCameraDeviceRear:前置摄像头
UIImagePickerControllerCameraDeviceFront:后置摄像头
闪光灯模式,枚举类型
@property(nonatomic) UIImagePickerControllerCameraFlashMode   cameraFlashMode

UIImagePickerControllerCameraFlashModeOff:关闭闪光灯
UIImagePickerControllerCameraFlashModeAuto:闪光灯自动
UIImagePickerControllerCameraFlashModeOn:打开闪光灯

b、常用属性
@property(nonatomic,copy) NSArray  *mediaTypes //媒体类型,默认情况下此数组包含kUTTypeImage,所以拍照时可以不用设置;但是当要录像的时候必须设置,可以设置为kUTTypeVideo(视频,但不带声音)或者kUTTypeMovie(视频并带有声音)
@property(nonatomic) NSTimeInterval videoMaximumDuration //视频最大录制时长,默认为10 s
@property(nonatomic) BOOL showsCameraControls //是否显示摄像头控制面板,默认为YES
@property(nonatomic,retain) UIView *cameraOverlayView //摄像头上覆盖的视图,可用通过这个视频来自定义拍照或录像界面
@property(nonatomic) CGAffineTransform cameraViewTransform //摄像头形变

b、常用方法
类方法
+ (BOOL)isSourceTypeAvailable:(UIImagePickerControllerSourceType)sourceType //指定的源类型是否可用
+ (BOOL)isCameraDeviceAvailable:(UIImagePickerControllerCameraDevice)cameraDevice //指定的摄像头是否可用
+ (NSArray *)availableCaptureModesForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice //获得指定摄像头上的可用捕获模式
+ (NSArray *)availableMediaTypesForSourceType:(UIImagePickerControllerSourceType)sourceType //指定的源设备上可用的媒体类型,一般就是图片和视频
+ (BOOL)isFlashAvailableForCameraDevice:(UIImagePickerControllerCameraDevice)cameraDevice //指定摄像头的闪光灯是否可用
对象方法
- (void)takePicture //编程方式拍照
- (BOOL)startVideoCapture //编程方式录制视频
- (void)stopVideoCapture //编程方式停止录制视频
代理方法
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info //媒体拾取完成
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker //取消拾取
扩展方法(主要用于保存照片、视频到相簿)
UIImageWriteToSavedPhotosAlbum(UIImage *image, id completionTarget, SEL completionSelector, void *contextInfo) //保存照片到相簿
UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(NSString *videoPath) //能否将视频保存到相簿
UISaveVideoAtPathToSavedPhotosAlbum(NSString *videoPath, id completionTarget, SEL completionSelector, void *contextInfo) //保存视频到相簿

3、Demo演示

运行效果
拍摄视频
点击拍摄视频
拍摄完成后点击使用视频即可存入相册
拍摄相片
点击拍摄照片
拍摄完成后点击使用照片即可存入相册

下面就以一个示例展示如何使用UIImagePickerController来拍照和录制视频,下面的程序中只要将_isVideo设置为YES就是视频录制模式,录制完后在主视图控制器中自动播放;如果将_isVideo设置为NO则为拍照模式,拍照完成之后在主视图控制器中显示拍摄的照片。

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 通过这里设置当前程序是拍照还是录制视频
    self.isVideo = YES;
}
a、需要使用到的头文件和属性
#import <MobileCoreServices/MobileCoreServices.h>
#import <AVFoundation/AVFoundation.h>

@interface UIImagePickerControllerDemo ()<UIImagePickerControllerDelegate,UINavigationControllerDelegate>

@property (assign,nonatomic) BOOL isVideo; //是否录制视频,如果为1表示录制视频,0代表拍照
@property (strong,nonatomic) UIImagePickerController *imagePicker;
@property (strong,nonatomic) UIImageView *photo; //照片展示视图
@property (strong,nonatomic) AVPlayer *player; //播放器,用于录制完视频后播放视频

@end
b、创建 imagePicker

通过懒加载的方式创建了 imagePicker,并配置其属性。

- (UIImagePickerController *)imagePicker
{
    if (!_imagePicker)
    {
        _imagePicker = [[UIImagePickerController alloc] init];
        // 设置image picker的来源,这里设置为摄像头
        _imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
        // 设置使用哪个摄像头,这里设置为后置摄像头
        _imagePicker.cameraDevice = UIImagePickerControllerCameraDeviceRear;
        if (self.isVideo)
        {
            _imagePicker.mediaTypes = @[(NSString *)kUTTypeMovie];
            _imagePicker.videoQuality = UIImagePickerControllerQualityTypeIFrame1280x720;
            // 设置摄像头模式(拍照,录制视频)
            _imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo;
            
        }
        else
        {
            _imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModePhoto;
        }
        // 允许编辑
        _imagePicker.allowsEditing = YES;
        // 设置代理,检测操作
        _imagePicker.delegate=self;
    }
    return _imagePicker;
}
c、UIImagePickerController 的代理方法

取消拍照或者录制视频时调用的方法

- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
    NSLog(@"取消");
}

拍照或者录制视频完成时候调用的方法。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // 获取媒体类型
    NSString *mediaType = [info objectForKey:UIImagePickerControllerMediaType];
    ......
    // 录制完成后退出拍摄界面
    [self dismissViewControllerAnimated:YES completion:nil];
}

实现拍照的代码

if ([mediaType isEqualToString:(NSString *)kUTTypeImage]) //如果是拍照
{
    NSLog(@"拍照");
    UIImage *image;
    // 如果允许编辑则获得编辑后的照片,否则获取原始照片
    if (self.imagePicker.allowsEditing)
    {
        // 获取编辑后的照片
        image = [info objectForKey:UIImagePickerControllerEditedImage];
    }
    else
    {
        // 获取原始照片
        image = [info objectForKey:UIImagePickerControllerOriginalImage];
    }
    // 显示照片
    [self.photo setImage:image];
    // 保存到相簿
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}

实现录制视频的代码

else if([mediaType isEqualToString:(NSString *)kUTTypeMovie]) //如果是录制视频
{
    NSLog(@"录制视频");
    // 视频路径
    NSURL *url = [info objectForKey:UIImagePickerControllerMediaURL];
    NSString *urlStr = [url path];
    if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(urlStr))
    {
        // 保存视频到相簿,注意也可以使用ALAssetsLibrary来保存
        UISaveVideoAtPathToSavedPhotosAlbum(urlStr, self, @selector(video:didFinishSavingWithError:contextInfo:), nil);//保存视频到相簿
    }
}
d、点击拍照按钮触发拍照或者录制视频
- (void)takeClick
{
    [self presentViewController:self.imagePicker animated:YES completion:nil];
}

二、使用 AVFoundation 拍照和录制视频

运行效果
拍照
录制视频
单击聚焦
双击曝光
两个手指双击同时触发聚焦和曝光

1、AVFoundation 简介

a、AVFoundation 的作用

UIImagePickerController确实强大,但由于它的高度封装性,要进行某些自定义工作就比较复杂了。例如要做出一款类似于美颜相机的拍照界面或者抖音小视频和虎牙直播就比较难以实现了,此时就可以考虑使用AVFoundation来实现。因为AVFoundation中包含了很多和底层输入、输出设备打交道的类,依靠这些类开发人员面对的不再是封装好的音频播放器AVAudioPlayer、录音机(AVAudioRecorder)、视频(包括音频)播放器AVPlayer,而是输入设备(例如麦克风、摄像头)、输出设备(图片、视频)等。

b、使用AVFoundation做拍照和视频录制开发用到的相关类
AVCaptureSession

媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession可以有多个输入输出。

AVCaptureDevice

输入设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦、白平衡等)。

AVCaptureDeviceInput

设备输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。

AVCaptureOutput

输出数据管理对象,用于接收各类输出数据,该对象将会被添加到AVCaptureSession中管理。通常使用对应的子类AVCaptureAudioDataOutputAVCaptureStillImageOutputAVCaptureVideoDataOutputAVCaptureFileOutput。前面几个对象的输出数据都是NSData类型,而AVCaptureFileOutput代表数据以文件形式输出。

类似的,AVCcaptureFileOutput也不会直接创建使用,通常会使用其子类:AVCaptureAudioFileOutputAVCaptureMovieFileOutput

AVCaptureConnection

当把一个输入或者输出添加到AVCaptureSession之后,AVCaptureSession就会在所有相符的输入、输出设备之间建立连接(AVCaptionConnection)。

AVCaptureVideoPreviewLayer

相机拍摄预览图层,是CALayer的子类,使用该对象可以实时查看拍照或视频录制效果,创建该对象需要指定对应的AVCaptureSession对象。

c、使用 AVFoundation 拍照和录制视频的步骤
  1. 创建AVCaptureSession对象。
  2. 使用AVCaptureDevice的静态方法获得需要使用的设备,例如拍照和录像就需要获得摄像头设备,录音就要获得麦克风设备。
  3. 利用输入设备AVCaptureDevice初始化AVCaptureDeviceInput对象。
  4. 初始化输出数据管理对象,如果要拍照就初始化AVCaptureStillImageOutput对象,如果拍摄视频就初始化AVCaptureMovieFileOutput对象。
  5. 将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中。
  6. 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器中,调用AVCaptureSessionstartRuning方法开始捕获。
  7. 将捕获的音频或视频数据输出到指定文件。
d、在 info.plist 中添加权限说明

需要注意的是访问到相册、麦克风、相机就需要修改 plist 权限,否则会导致项目崩溃。

  • 相机使用权限:Privacy - Camera Usage Description
  • 麦克风使用权限:Privacy - Microphone Usage Description
  • 相册使用权限:Privacy - Photo Library Usage Description

2、摄像头录制工具类提供的属性和方法

a、录制发生错误时用于处理的委托方法

发生错误事件时需要在对象委托上调用一些方法来处理。

@protocol THCameraControllerDelegate <NSObject>

// 设备错误
- (void)deviceConfigurationFailedWithError:(NSError *)error;
// 媒体捕捉错误
- (void)mediaCaptureFailedWithError:(NSError *)error;
// 写入时错误
- (void)assetLibraryWriteFailedWithError:(NSError *)error;

@end

b、录制工具类头文件中提供的可用方法

用于设置、配置视频捕捉会话

- (BOOL)setupSession:(NSError **)error;
- (void)startSession;
- (void)stopSession;

切换不同的摄像头

- (BOOL)switchCameras;//切换不同的摄像头
- (BOOL)canSwitchCameras;//是否能切换
@property (nonatomic, readonly) NSUInteger cameraCount;//摄像头个数
@property (nonatomic, readonly) BOOL cameraHasTorch; //手电筒
@property (nonatomic, readonly) BOOL cameraHasFlash; //闪光灯
@property (nonatomic, readonly) BOOL cameraSupportsTapToFocus; //聚焦
@property (nonatomic, readonly) BOOL cameraSupportsTapToExpose;//曝光
@property (nonatomic) AVCaptureTorchMode torchMode; //手电筒模式
@property (nonatomic) AVCaptureFlashMode flashMode; //闪光灯模式

聚焦、曝光、重设聚焦、曝光的方法

- (void)focusAtPoint:(CGPoint)point;//聚焦
- (void)exposeAtPoint:(CGPoint)point;//曝光
- (void)resetFocusAndExposureModes;//重设聚焦、曝光

实现捕捉静态图片 & 视频的功能

//捕捉静态图片
- (void)captureStillImage;

//开始录制
- (void)startRecording;

//停止录制
- (void)stopRecording;

//获取录制状态
- (BOOL)isRecording;

//录制时间
- (CMTime)recordedDuration;

c、录制工具类扩展里提供的私有属性
@interface THCameraController () <AVCaptureFileOutputRecordingDelegate>

@property (strong, nonatomic) dispatch_queue_t videoQueue; //视频队列
@property (strong, nonatomic) AVCaptureSession *captureSession;// 捕捉会话
@property (weak, nonatomic) AVCaptureDeviceInput *activeVideoInput;//视图输入
@property (strong, nonatomic) AVCaptureStillImageOutput *imageOutput;//图片输出
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieOutput;//视频输出
@property (strong, nonatomic) NSURL *outputURL;//输出路径

@end

3、录制工具类的Session

a、配置Session
- (BOOL)setupSession:(NSError **)error {
}
步骤
  • 初始化
  • 设置分辨率
  • 配置输入设备(注意转化为AVCaptureDeviceInput对象)
  • 配置输入设备包括音频和视频输入
  • 配置输出,包括静态图像输出和视频文件输出
  • 在为Session添加输入输出设备的时候,需要判断能否添加
❶ 初始化

创建捕捉会话。AVCaptureSession 是捕捉场景的中心枢纽。

self.captureSession = [[AVCaptureSession alloc]init];
❷ 设置图像的分辨率
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;

AVCaptureSessionPresetHigh
AVCaptureSessionPresetMedium
AVCaptureSessionPresetLow
AVCaptureSessionPreset640x480
AVCaptureSessionPreset1280x720
AVCaptureSessionPresetPhoto
❸ 添加视频的输入设备

拿到视频捕捉设备。iOS系统默认返回后置摄像头,但是美颜相机的开发者使用的是前置摄像头。

AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

将捕捉设备封装成AVCaptureDeviceInput。需要注意的是,如果想要为会话添加捕捉设备,那么必须将设备封装成AVCaptureDeviceInput对象,因为设备和会话之间不能产生之间联系,需要使用AVCaptureDeviceInput作为中介。

AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];

判断videoInput是否有效,再通过canAddInput:测试是否能被添加到会话中,倘若能则将videoInput 添加到 captureSession中。在为Session添加输入输出设备时候,需要注意一定要判断能否添加,原因是摄像头是公共设备,并不隶属于任何一个APP,所以当别人的APP正在使用摄像头的时候,我们的APP再强制添加的话就会崩溃,此时不能添加。

//判断videoInput是否有效
if (videoInput)
{
    //canAddInput:测试是否能被添加到会话中
    if ([self.captureSession canAddInput:videoInput])
    {
        //将videoInput 添加到 captureSession中
        [self.captureSession addInput:videoInput];
        //记录此时活跃的视图输入设备,用于区分是前置还是后置摄像头
        self.activeVideoInput = videoInput;
    }
}
else
{
    return NO;
}
❹ 添加音频的输入设备

选择默认音频捕捉设备,即返回一个内置麦克风。

AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

为这个设备创建一个捕捉设备输入。

AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];

判断规则同上。注意因为麦克风只有一个,所以不需要添加使用self.activeVideoInput区分前置和后置摄像头的操作。

//判断audioInput是否有效
if (audioInput) 
{
    //canAddInput:测试是否能被添加到会话中
    if ([self.captureSession canAddInput:audioInput])
    {
        //将audioInput 添加到 captureSession中
        [self.captureSession addInput:audioInput];
    }
}
else
{
    return NO;
}
❺ 配置输出的图片

使用的是AVCaptureStillImageOutput 实例,用于从摄像头捕捉静态图片

self.imageOutput = [[AVCaptureStillImageOutput alloc]init];

配置字典:希望捕捉到JPEG格式的图片

self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};

输出连接。判断是否可用,可用则添加到输出连接中去。

if ([self.captureSession canAddOutput:self.imageOutput])
{
    [self.captureSession addOutput:self.imageOutput];
}
❻ 配置输出的视频

创建一个AVCaptureMovieFileOutput实例,用于将Quick Time 电影录制到文件系统。

self.movieOutput = [[AVCaptureMovieFileOutput alloc]init];

输出连接。判断是否可用,可用则添加到输出连接中去。

if ([self.captureSession canAddOutput:self.movieOutput])
{
    [self.captureSession addOutput:self.movieOutput];
}

创建视频队列

self.videoQueue = dispatch_queue_create("XieJiapei.VideoQueue", NULL);

b、开启或者停止 Session
❶ 开启 Session 进行捕捉

检查是否处于运行状态,未运行则开始进行捕捉。使用同步调用会损耗一定的时间,则用异步的方式处理。

- (void)startSession
{
    //检查是否处于运行状态
    if (![self.captureSession isRunning])
    {
        //使用同步调用会损耗一定的时间,则用异步的方式处理
        dispatch_async(self.videoQueue, ^{
            [self.captureSession startRunning];
        });
    }
}
❷ 停止 Session 捕捉

检查是否处于运行状态,处于运行状态则停止捕捉。使用异步方式,停止运行。

- (void)stopSession
{
    //检查是否处于运行状态
    if ([self.captureSession isRunning])
    {
        //使用异步方式,停止运行
        dispatch_async(self.videoQueue, ^{
            [self.captureSession stopRunning];
        });
    }
}

4、切换前后摄像头

a、切换前后摄像头需要使用到的辅助方法
❶ 寻找指定摄像头设备

首先获取所有可用视频设备(包括前置和后置摄像头),接着遍历可用的视频设备并返回 position 参数值。

- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position
{
    //获取可用视频设备
    NSArray *devicess = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    
    //遍历可用的视频设备 并返回position 参数值
    for (AVCaptureDevice *device in devicess)
    {
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}
❷ 获取当前活跃的设备

默认活跃的设备是后置摄像头。返回当前捕捉会话对应的摄像头的device属性。

- (AVCaptureDevice *)activeCamera
{
    return self.activeVideoInput.device;
}
❸ 返回当前未激活的摄像头

通过查找当前激活摄像头的反向摄像头获得未激活的摄像头。如果设备只有1个摄像头,则返回nil

- (AVCaptureDevice *)inactiveCamera
{    
    //通过查找当前激活摄像头的反向摄像头获得未激活的摄像头
    AVCaptureDevice *device = nil;
    if (self.cameraCount > 1)
    {
        // 当前是后置摄像头
        if ([self activeCamera].position == AVCaptureDevicePositionBack)
        {
            // 需要通过 cameraWithPosition 方法才能根据位置拿到设备
            device = [self cameraWithPosition:AVCaptureDevicePositionFront];
        }
        // 当前是前置摄像头
        else
        {
            device = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }
    }
    
    // 如果设备只有1个摄像头,则返回nil
    return device;
}
❹ 能否切换摄像头

判断是否有超过1个摄像头可用,有则可以切换。

- (BOOL)canSwitchCameras
{
    return self.cameraCount > 1;
}
❺ 可用视频捕捉设备的数量
- (NSUInteger)cameraCount
{
    return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}

b、进行切换摄像头
- (BOOL)switchCameras
{
    .......
}
❶ 判断是否有多个摄像头
if (![self canSwitchCameras])
{
    return NO;
}
❷ 获取当前设备的反向设备
NSError *error;
AVCaptureDevice *videoDevice = [self inactiveCamera];
❸ 将输入设备封装成 AVCaptureDeviceInput
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
❹ 判断 videoInput 是否为 nil

nil则表示创建 AVCaptureDeviceInput 出现错误,需要通知委托来处理该错误。

if (videoInput)
{
    .......
}
else
{
    [self.delegate deviceConfigurationFailedWithError:error];
    return NO;
}
❺ 标注原始配置要发生改变

倘若videoInput存在,则标注原始配置要发生改变。

[self.captureSession beginConfiguration];
❻ 再将捕捉会话中原本的捕捉输入设备移除

判断新的设备是否能加入之前,一定要进行前面的移除旧设备操作,否则无法切换摄像头。

[self.captureSession removeInput:self.activeVideoInput];
❼ 添加新的捕捉输入设备

如果新设备无法加入,那么此时旧设备没有了,新设备又用不了,则将原本的视频捕捉设备重新加入到捕捉会话中。

//判断新的设备是否能加入
if ([self.captureSession canAddInput:videoInput])
{
    //能加入成功,则将videoInput 作为新的视频捕捉设备
    [self.captureSession addInput:videoInput];
    
    //将活跃设备修改为 videoInput
    self.activeVideoInput = videoInput;
}
else
{
    //如果新设备无法加入,则将原本的视频捕捉设备重新加入到捕捉会话中
    [self.captureSession addInput:self.activeVideoInput];
}
❽ 提交配置

配置完成后,AVCaptureSession commitConfiguration 会分批的将所有变更整合在一起进行提交

[self.captureSession commitConfiguration];

5、使用捕捉设备进行聚焦和曝光

AVCapture Device 定义了很多方法,让开发者控制 ios 设备上的摄像头。可以独立调整和锁定摄像头的焦距、曝光、白平衡。对焦和曝光可以基于特定的兴趣点进行设置,使其在应用中实现点击对焦、点击曝光的功能。还可以让你控制设备的LED作为拍照的闪光灯或手电筒的使用。

每当修改摄像头设备时,一定要先测试修改动作是否能被设备支持。并不是所有的摄像头都支持所有功能,例如前置摄像头就不支持对焦操作,因为它和目标距离一般在一臂之长的距离。但大部分后置摄像头是可以支持全尺寸对焦。尝试应用一个不被支持的动作,会导致异常崩溃。

a、点击聚焦方法的实现
❶ 询问激活中的摄像头是否支持兴趣点对焦
- (BOOL)cameraSupportsTapToFocus
{
    return [[self activeCamera]isFocusPointOfInterestSupported];
}
❷ 对兴趣点进行对焦

判断是否支持兴趣点对焦和自动对焦模式。

- (void)focusAtPoint:(CGPoint)point
{
    AVCaptureDevice *device = [self activeCamera];
    
    //是否支持兴趣点对焦 & 是否支持自动对焦模式
    if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus])
    {
        .....
    }
}

因为配置的时候,不能让多个对象对它进行更改,所以需要锁定设备进行配置。倘若锁定设备发生了错误,则返回给错误处理代理。

NSError *error;
if ([device lockForConfiguration:&error])
{
    ......
}
else
{
    [self.delegate deviceConfigurationFailedWithError:error];
}

倘若成功锁定,则修改聚焦位置为兴趣点,聚焦模式为自动模式,再释放该锁定。

//聚焦位置
device.focusPointOfInterest = point;

//聚焦模式
device.focusMode = AVCaptureFocusModeAutoFocus;

//释放该锁定
[device unlockForConfiguration];

b、点击曝光的方法实现
❶ 询问设备是否支持对一个兴趣点进行曝光
- (BOOL)cameraSupportsTapToExpose
{
    return [[self activeCamera] isExposurePointOfInterestSupported];
}
❷ 对兴趣点进行曝光

判断是否支持自动曝光模式

- (void)exposeAtPoint:(CGPoint)point
{
    AVCaptureDevice *device = [self activeCamera];
    AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
    if (device.isExposurePointOfInterestSupported && [device isExposureModeSupported:exposureMode])
    {
        ......
    }
}

锁定设备准备配置。倘若锁定设备发生了错误,则返回给错误处理代理。

if ([device lockForConfiguration:&error])
{
    .......
}
else
{
    [self.delegate deviceConfigurationFailedWithError:error];
}

倘若成功锁定,则配置期望值。

device.exposurePointOfInterest = point;

判断设备是否支持锁定曝光的模式,倘若支持,则使用kvo确定设备的adjustingExposure属性的状态

static const NSString *THCameraAdjustingExposureContext;

if ([device isExposureModeSupported:AVCaptureExposureModeLocked])
{
    [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&THCameraAdjustingExposureContext];
}

释放该锁定

[device unlockForConfiguration];

c、重写observeValueForKeyPath方法,观察属性状态

判断context(上下文)是否为THCameraAdjustingExposureContext,倘若不是则调用父类的observeValueForKeyPath方法,不进行重写。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    //判断 context(上下文)是否为 THCameraAdjustingExposureContext
    if (context == &THCameraAdjustingExposureContext)
    {
        .....
    }
    else
    {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

通过object获取device,再判断设备是否不再调整曝光等级,确认设备的exposureMode是否可以设置为AVCaptureExposureModeLocked

AVCaptureDevice *device = (AVCaptureDevice *)object;

if(!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked])
{
    ......
}

移除作为adjustingExposureself,就不会得到后续变更的通知。

[object removeObserver:self forKeyPath:@"adjustingExposure" context:&THCameraAdjustingExposureContext];

异步方式调回主队列修改exposureMode,再释放该锁定。

dispatch_async(dispatch_get_main_queue(), ^{
    NSError *error;
    if ([device lockForConfiguration:&error])
    {
        
        //修改exposureMode
        device.exposureMode = AVCaptureExposureModeLocked;
        
        //释放该锁定
        [device unlockForConfiguration];
    }
    else
    {
        [self.delegate deviceConfigurationFailedWithError:error];
    }
});

d、重新设置对焦&曝光

判断对焦兴趣点和连续自动对焦模式是否被支持,再确认曝光度是否可以被重设

- (void)resetFocusAndExposureModes
{
    AVCaptureDevice *device = [self activeCamera];
    
    AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;
    
    //获取对焦兴趣点和连续自动对焦模式是否被支持
    BOOL canResetFocus = [device isFocusPointOfInterestSupported]&& [device isFocusModeSupported:focusMode];
    
    AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
    
    //确认曝光度是否可以被重设
    BOOL canResetExposure = [device isFocusPointOfInterestSupported] && [device isExposureModeSupported:exposureMode];
    ......
}

捕捉设备空间左上角(0,0),右下角(1,1) 中心点则(0.5,0.5)。我们将中点作为默认聚焦的位置。

CGPoint centPoint = CGPointMake(0.5f, 0.5f);

锁定设备,准备配置。焦点可设,则修改。曝光度可设,则设置为期望的曝光模式。

//锁定设备,准备配置
if ([device lockForConfiguration:&error])
{
    //焦点可设,则修改
    if (canResetFocus)
    {
        device.focusMode = focusMode;
        device.focusPointOfInterest = centPoint;
    }
    
    //曝光度可设,则设置为期望的曝光模式
    if (canResetExposure)
    {
        device.exposureMode = exposureMode;
        device.exposurePointOfInterest = centPoint;
    }
    
    //释放锁定
    [device unlockForConfiguration];
    
}
else
{
    [self.delegate deviceConfigurationFailedWithError:error];
} 

6、实现摄像头手电筒和闪关灯模式的开启关闭

a、实现摄像头的闪关灯模式的开启关闭
❶ 判断是否有闪光灯
- (BOOL)cameraHasFlash
{
    return [[self activeCamera] hasFlash];
}
❷ 获取闪光灯模式
- (AVCaptureFlashMode)flashMode
{    
    return [[self activeCamera] flashMode];
}
❸ 设置闪光灯

判断是否支持闪光灯模式。如果支持,则锁定设备,再修改闪光灯模式,最后解锁释放设备。

- (void)setFlashMode:(AVCaptureFlashMode)flashMode
{
    //获取会话
    AVCaptureDevice *device = [self activeCamera];
    
    //判断是否支持闪光灯模式
    if ([device isFlashModeSupported:flashMode])
    {
    
        //如果支持,则锁定设备
        NSError *error;
        if ([device lockForConfiguration:&error])
        {

            //修改闪光灯模式
            device.flashMode = flashMode;
            //修改完成,解锁释放设备
            [device unlockForConfiguration];
            
        }
        else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

b、实现摄像头手电筒的开启关闭
❶ 是否支持手电筒
- (BOOL)cameraHasTorch
{
    return [[self activeCamera] hasTorch];
}
❷ 获取手电筒模式
- (AVCaptureTorchMode)torchMode
{
    return [[self activeCamera] torchMode];
}
❸ 设置是否打开手电筒
- (void)setTorchMode:(AVCaptureTorchMode)torchMode
{
    AVCaptureDevice *device = [self activeCamera];
    
    if ([device isTorchModeSupported:torchMode])
    {
        NSError *error;
        if ([device lockForConfiguration:&error])
        {
            device.torchMode = torchMode;
            [device unlockForConfiguration];
        }
        else
        {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

7、静态图片的拍摄

AVCaptureStillImageOutputAVCaptureOutput的子类,用于捕捉图片。

a、捕捉图片
- (void)captureStillImage
{
    ......
}

获取连接,将图片和视频连接起来,因为图片也属于视频的一种。

AVCaptureConnection *connection = [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];

程序只支持纵向,但是如果用户横向拍照时,需要调整结果照片的方向。

//判断是否支持设置视频方向
if (connection.isVideoOrientationSupported)
{
    //获取方向值
    connection.videoOrientation = [self currentVideoOrientation];
}

定义一个 handler 块,会返回1个图片的NSData数据。

id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error) {
    .......
};

如果缓存区中的 sampleBuffer 非空,则将缓存区中的数据转化为图片数据,既然成功捕捉到了图片,那么就需要将图片传递出去。

if (sampleBuffer != NULL)
{
    NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
    UIImage *image = [[UIImage alloc] initWithData:imageData];
    
    //重点:捕捉图片成功后,将图片传递出去
    [self writeImageToAssetsLibrary:image];
}
else
{
    NSLog(@"NULL sampleBuffer:%@",[error localizedDescription]);
}

进行捕捉静态图片,完成之后调用handler 块。

[self.imageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];

b、获取方向值

通过获取 UIDeviceorientation来获取捕捉设备的方向。

- (AVCaptureVideoOrientation)currentVideoOrientation
{
    AVCaptureVideoOrientation orientation;
    
    switch ([UIDevice currentDevice].orientation)
    {
        case UIDeviceOrientationPortrait:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationLandscapeRight:
            orientation = AVCaptureVideoOrientationLandscapeLeft;
            break;
        case UIDeviceOrientationPortraitUpsideDown:
            orientation = AVCaptureVideoOrientationPortraitUpsideDown;
            break;
        default:
            orientation = AVCaptureVideoOrientationLandscapeRight;
            break;
    }
    
    return orientation;
}

c、将图片写入到 Library

Assets Library 框架用来让开发者通过代码方式访问相册。

- (void)writeImageToAssetsLibrary:(UIImage *)image
{
    .......
}

创建 ALAssetsLibrary 实例对象。

#import <AssetsLibrary/AssetsLibrary.h>//用于将文件写入到资源库中

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
  • 图片参数:参数为CGImageRef,所以使用 image.CGImage
  • 方向参数:将方向转为NSUInteger
  • 回调参数:写入成功、失败处理
[library writeImageToSavedPhotosAlbum:image.CGImage
                         orientation:(NSUInteger)image.imageOrientation
                     completionBlock:^(NSURL *assetURL, NSError *error) {

                     }];

成功后,发送捕捉图片成功通知,用于绘制相机左下角的缩略图。失败则打印错误信息。

if (!error)
{
    [self postThumbnailNotifification:image];
}
else
{
    id message = [error localizedDescription];
    NSLog(@"%@",message);
}

d、发送缩略图通知

工具类只是个controller控制类,不负责展示内容,所以只是回到主队列使用通知的方式将图片发送过去。

NSString *const THThumbnailCreatedNotification = @"THThumbnailCreated";

- (void)postThumbnailNotifification:(UIImage *)image
{
    //回到主队列
    dispatch_async(dispatch_get_main_queue(), ^{
        //发送请求
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc postNotificationName:THThumbnailCreatedNotification object:image];
    });
}

8、视频录制实现

a、视频内容捕捉的原理

对于视频内容的捕捉,当设置捕捉会话时,会添加一个名为AVCaptureMovieFileOutput的输出,这个输出会将QuickTime影片捕捉到磁盘。

AVCaptureMovieFileOutput类大多数核心功能继承于超类AVCaptureFileOutput。这个超类定义了许多实用功能,比如录制到最长时限或录制到特定文件大小时为止。超类还支持可以配置成保留最小可用的磁盘空间,这一点在存储空间有限的移动设备上录制视频时非常重要。

通常当QuickTime影片准备发布时,影片头的元数据处于文件的开始位置。这样可以让视频播放器快速读取头包含信息来确定文件的内容、结构和其包含的多个样本的位置。不过,当录制一个QuickTime影片时,直到所有的样片都完成捕捉后才能创建信息头。当录制结束时,创建头数据并将它附在文件结尾处。

将创建头的过程放在所有影片样本完成捕捉之后存在一个问题,尤其是在移动设备的情况下问题更为明显。如果遇到崩溃或其他中断,比如有电话拨入,则影片头就不会被正确写入,会在磁盘生成一个不可读的影片文件。对此,AVCaptureMovieFileOutput 提供一个核心功能就是分段捕捉 QuickTime 影片。

当录制开始时,在文件最前面写入一个最小化的头信息,随着录制的进行,片段按照一定的周期写入,最终创建完整的头信息。默认状态下,每10秒写入一个片段,不过这个时间的间隔可以通过修改捕捉设备输出的movieFragentInterval 属性来改变。我们用默认的间隔来做这demo,但是如果你可以在你的的APP修改这个值。通过写入片段的方式可以逐步创建完整的 QuickTime 影片头,这样确保了当遇到应用程序崩溃或中断时,影片仍然会以最好的一个写入片段为终点进行保存。


b、辅助视频录制的方法
❶ 判断是否录制状态
- (BOOL)isRecording
{
    return self.movieOutput.isRecording;
}
❷ 录制的时间
- (CMTime)recordedDuration
{
    return self.movieOutput.recordedDuration;
}
❸ 提供写入视频唯一文件系统URL
- (NSURL *)uniqueURL
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    //创建一个临时唯一命名的目录
    //临时文件在写入过程中不会产生后缀名,只有当全部写入完成后,才会产生后缀名
    NSString *dirPath = [fileManager temporaryDirectoryWithTemplateString:@"kamera.XXXXXX"];
    
    if (dirPath)
    {
        //mov是视频封装容器,和视频编码格式存在区别
        NSString *filePath = [dirPath stringByAppendingPathComponent:@"kamera_movie.mov"];
        return  [NSURL fileURLWithPath:filePath];
        
    }
    
    return nil;
}
❹ 停止录制
- (void)stopRecording
{
    //是否正在录制
    if ([self isRecording])
    {
        [self.movieOutput stopRecording];
    }
}

c、开始录制视频

录制之前需要判断当前是否已经处于录制状态,未录制才允许开始录制视频。

- (void)startRecording
{
    if (![self isRecording])
    {
        ......
    }
}

获取当前视频捕捉连接信息,用于为捕捉视频数据配置一些核心属性。

AVCaptureConnection * videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];

判断是否支持设置videoOrientation属性,倘若支持则修改当前视频的方向。

if([videoConnection isVideoOrientationSupported])
{
    //支持则修改当前视频的方向
    videoConnection.videoOrientation = [self currentVideoOrientation];
}

判断是否支持视频稳定。视频稳定只会在录制视频文件涉及,可以显著提高视频的质量。

if([videoConnection isVideoStabilizationSupported])
{
    videoConnection.enablesVideoStabilizationWhenAvailable = YES;
}

判断当前活跃的摄像头是否可以进行平滑对焦模式操作。平滑对焦模式即减慢摄像头镜头对焦速度,这样当用户移动拍摄时摄像头会尝试快速自动对焦。

AVCaptureDevice *device = [self activeCamera];

if (device.isSmoothAutoFocusEnabled)
{
    NSError *error;
    if ([device lockForConfiguration:&error])
    {
        device.smoothAutoFocusEnabled = YES;
        [device unlockForConfiguration];
    }
    else
    {
        //设备错误
        [self.delegate deviceConfigurationFailedWithError:error];
    }
}

查找写入捕捉视频的唯一文件系统URL

self.outputURL = [self uniqueURL];

摄像头的相关配置已经完成,也已经获取到路径,则开始进行录制。直播/小视频APP捕捉到视频信息后就会通过(AAC/H264)进行压缩。但是本Demo为了简便,就将录制成的QuickTime视频文件直接存储到相册,其实这个过程中也会涉及到编码,不过是由AVFoundation提供的硬编码。

//在捕捉输出上调用方法 
//参数1:录制保存路径  参数2:代理
[self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];

d、AVCaptureFileOutputRecordingDelegate
❶ 捕捉录制的输出文件
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray *)connections
                error:(NSError *)error
{
    //错误
    if (error)
    {
        [self.delegate mediaCaptureFailedWithError:error];
    }
    else
    {
        //视频写入到相册
        [self writeVideoToAssetsLibrary:[self.outputURL copy]];
    }
    
    //录制完成后将路径清空
    self.outputURL = nil;
}
❷ 写入捕捉到的视频到相册
- (void)writeVideoToAssetsLibrary:(NSURL *)videoURL
{
    ......
}

写资源库写入前,检查视频是否可被写入 (写入前尽量养成判断的习惯)。

if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoURL])
{
    .......
}

创建block块。视频在拍摄完成后也会在左下角有个缩略图。

ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
completionBlock = ^(NSURL *assetURL,NSError *error)
{
    if (error)
    {
        [self.delegate assetLibraryWriteFailedWithError:error];
    }
    else
    {
        //用于界面展示视频缩略图
        [self generateThumbnailForVideoAtURL:videoURL];
    }
};

执行实际写入资源库的动作。

[library writeVideoAtPathToSavedPhotosAlbum:videoURL completionBlock:completionBlock];

e、获取视频左下角缩略图

在异步队列上生成缩略图。

- (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL
{
    //在 videoQueue 上
    dispatch_async(self.videoQueue, ^{
        ......
    });
}

根据 videoURL生成 AVAsset实例对象,再根据生成的asset去创建AVAssetImageGenerator实例对象。

AVAsset *asset = [AVAsset assetWithURL:videoURL];
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];

设置 maximumSize 宽为100,高为0。系统会根据视频的宽高比来计算图片的高度。

imageGenerator.maximumSize = CGSizeMake(100.0f, 0.0f);

捕捉视频缩略图会考虑视频的变化(如视频的方向变化)。如果不设置,缩略图的方向可能出错。

imageGenerator.appliesPreferredTrackTransform = YES;

获取第一帧的CGImageRef图片。注意需要自己管理它的创建和释放。

CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:nil];

将图片转化为UIImage

UIImage *image = [UIImage imageWithCGImage:imageRef];

已经拿到左下角的缩略图,则释放CGImageRef imageRef 防止内存泄漏。

CGImageRelease(imageRef);

回到主线程。因为ImageView添加图片并不在该工具类中,所以通过通知的方式将图片传递过去。

//回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
    
    //发送通知,传递最新的image
    [self postThumbnailNotifification:image];
});

9、实现预览录制内容功能

方式一:使用新建 View 的 layer 作为预览视图
❶ 重写layerClass 类方法可以让开发者创建视图实例自定义图层
+ (Class)layerClass 
{
    return [AVCaptureVideoPreviewLayer class];
}
❷ 重写 session 方法,返回捕捉会话
- (AVCaptureSession*)session 
{
    return [(AVCaptureVideoPreviewLayer*)self.layer session];
}
❸ 重写session属性的访问方法

setSession:方法中访问了视图layer属性。使用AVCaptureVideoPreviewLayer实例设置AVCaptureSession 将捕捉数据直接输出到图层中,确保与会话状态同步。

- (void)setSession:(AVCaptureSession *)session 
{
    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}
❹ 转换摄像头坐标系

将屏幕坐标系上的触控点转换为摄像头上的坐标系点。这是个私有方法,用于支持该类定义的不同触摸处理方法。

- (CGPoint)captureDevicePointForPoint:(CGPoint)point 
{
    AVCaptureVideoPreviewLayer *layer = (AVCaptureVideoPreviewLayer *)self.layer;
    return [layer captureDevicePointOfInterestForPoint:point];
}
方式二:将预览图层添加到 viewContainer 自带的 layer
❶ 创建视频预览层,用于实时展示摄像头状态
_captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];

CALayer *layer = self.viewContainer.layer;
layer.masksToBounds = YES;

_captureVideoPreviewLayer.frame = layer.bounds;
_captureVideoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;//填充模式
❷ 将视频预览层添加到界面中
[layer insertSublayer:_captureVideoPreviewLayer below:self.focusCursor.layer];

Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

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

推荐阅读更多精彩内容