ios直播:自定义相机采集(GPUImage)、美颜、硬/软编码(ffmpreg+x264)、流协议(rtmp和hls),做一个直播项目

目前自己工作三年了,但是自己却没有做过直播项目,一致对直播充满了无限的好奇,所以自己在工作之余花了接近一个月的时间研究了一下,与大家共勉,如有错误或者不到位的地方,请指正。

直播的流程:

  • 视频的采集 - 美颜与否 - 对视频进行编码 - 服务器的流分发支持各个平台 - 客户端进行对流进行解码显示播放。
    我用一个图片来进行表示。


    image.png

    解释:下面我会对每一个部分进行说明,并且贴上我自己写的代码

  • 1.视频的采集:
    视频的采集对于我们iOS而言就是通过我们手机的前置和后置摄像头采集到我们画面还有通过麦克风采集到声音。
  • 1.1 我们通过系统的方法进行采集,系统的方法进行采集是没有美颜效果的,首先苹果很注重用户的隐私也就是我们需要在我们的plist文件中添加使用相机的key 和使用麦克风的key否则我们运行程序会报错。怎么添加key 我用图片的方式进行表示:


    image.png

    添加key 我们还应该调用代码来判断是否用户允许我们使用相机等具体代码是:

 switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:
            {
                [self initVideoAndAudio];
            }
            break;
        case AVAuthorizationStatusNotDetermined:// 没有选择直接退出app 再次进入直接提示
        {
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    [self initVideoAndAudio];
                }else{
                    NSLog(@"这个用户脑袋有病,没有给出同意");
                }
            }];
        }
            break;
        case AVAuthorizationStatusRestricted:{ // 这种情况可能开始同意来 ,后来自己又到设置里边给关了
            
        }
        case AVAuthorizationStatusDenied:{ // 明确拒绝了
            
        }
        default:
            break;
    }

下面就是我们视频、音频的采集。
先说几个名词,我用我网络上和文档中找的图片来进行显示


image.png

其中一个AVCaptureSession来进行管理输入和输出,其中这个session 是一个单例 ,他也可以管理多个输入和输出。
其中session可以设置分辨率

if ([captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) {
    captureSession.sessionPreset = AVCaptureSessionPresetHigh;
}

分辨率对应的表 是这样的 也就是 设置分辨路属性在不同的机器上是不一样的,而且前后摄像头设置同样的属性有时候也不一样,因为前后硬件有时候硬件不一样。


image.png

对于session还需要设置我们的预览涂层,没有预览涂层我们只能听见声音不能看到画面,预览涂层可以设置frame 用来显示我们能看到画面的大小,值得说明的是有时候我们设置frame为全屏,但是显示的画面可能不是全屏,那是因为分辨率的原因我们只需要把分辨率设置高点就好了 (具体为什么我也不知道)。
session 有一个delegate 那里面有一个sampleBuffer ,其中sampleBuffer就是每一帧的视频或者音频,大家都知道视频其实就是有一张张图片组成,苹果会给我们返回每一帧的数据,到时候我们就用每一帧的数据进行编码。
将我们的视频的输入输出和音频的输出输出添加到我们到session中,然后开始进行采集,具体代码如下:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>


@interface ViewController ()<AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate,AVCaptureFileOutputRecordingDelegate>

/**
 管理者
 */
@property (strong, nonatomic) AVCaptureSession *session;
/**
 音频的输入、输出
 */
@property (strong, nonatomic) AVCaptureDeviceInput *videoInput;
/**
 视频输出
 */
@property (strong, nonatomic) AVCaptureVideoDataOutput *videoOutput;
/**
 视频的文件的写入
 */
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieFileOut;
/**
 预览涂层
 */
@property (strong, nonatomic) AVCaptureVideoPreviewLayer *previewLayer;



@end

@implementation ViewController
-(AVCaptureSession *)session {
    if (!_session) {
        _session = [[AVCaptureSession alloc] init];
    }
    return _session;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
//    AVAuthorizationStatusNotDetermined = 0,
//    AVAuthorizationStatusRestricted    = 1,
//    AVAuthorizationStatusDenied        = 2,
//    AVAuthorizationStatusAuthorized    = 3,
    // 先获取权限
    switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
        case AVAuthorizationStatusAuthorized:
            {
                [self initVideoAndAudio];
            }
            break;
        case AVAuthorizationStatusNotDetermined:// 没有选择直接退出app 再次进入直接提示
        {
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    [self initVideoAndAudio];
                }else{
                    NSLog(@"这个用户脑袋有病,没有给出同意");
                }
            }];
        }
            break;
        case AVAuthorizationStatusRestricted:{ // 这种情况可能开始同意来 ,后来自己又到设置里边给关了
            
        }
        case AVAuthorizationStatusDenied:{ // 明确拒绝了
            
        }
        default:
            break;
    }
}

/**
 初始化音视频的输出 等等的操作
 */
- (void)initVideoAndAudio{
    // 设置分辨率
    if ([self.session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
        [self.session setSessionPreset:AVCaptureSessionPresetHigh];
    }
    [self.session beginConfiguration];
    
    // 初始化视频的输入和输出
    [self setUpVideoInputOutput];
    // 初始化音频的输入和输出
    [self setUpAudioInputOutput];
    // 设置previewlayer
    [self setUpPreviewLayer];
    
    [self.session commitConfiguration];
}
/**
 初始化视频的输入和输出
 */
- (void)setUpVideoInputOutput {
    
    // 设备
    AVCaptureDevice *videoDevice = [self captureDevice:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
    AVCaptureDeviceInput *videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:nil];
    // 输出
    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    [videoOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    // 添加输入输出
    [self addInput:videoInput addOutput:videoOutput];
    self.videoInput = videoInput;
    self.videoOutput = videoOutput;

}
/**
 初始化音频的输入输出
 */
- (void)setUpAudioInputOutput{
    // 设备
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    // 输入
    AVCaptureDeviceInput *audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:nil];
    // 输出
    AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    [audioOutput setSampleBufferDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
    // 添加输入输出
    [self addInput:audioInput addOutput:audioOutput];
}

/**
 创建预览涂层
 */
- (void)setUpPreviewLayer{
    // 创建预览涂层
    AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
    previewLayer.frame = self.view.bounds;
    [self.view.layer insertSublayer:previewLayer atIndex:0];
    self.previewLayer = previewLayer;
    
}

/**
 开始写入视频到本地
 */
- (void)startRecordVideo{
    
    // 设置输出
    [self.session removeOutput:self.movieFileOut];
    AVCaptureMovieFileOutput *movieFileOut = [[AVCaptureMovieFileOutput alloc] init];
    self.movieFileOut = movieFileOut;
    // 设置connnection
    AVCaptureConnection *connection = [self.movieFileOut connectionWithMediaType:AVMediaTypeVideo];
    connection.automaticallyAdjustsVideoMirroring = YES;
    
    
    if ([self.session canAddOutput:self.movieFileOut]) {
        [self.session addOutput:self.movieFileOut];
        // 视频稳定设置
        if ([connection isVideoStabilizationSupported]) {
            connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
        }
//        if ([connection isVideoMirroringSupported]) {
//            connection.videoMirrored = YES;
//        }
        connection.videoScaleAndCropFactor = connection.videoMaxScaleAndCropFactor;
    }
    // 开始录制设置delegate
    NSString *pathStr = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.mp4"];
    NSURL *pathUrl = [NSURL fileURLWithPath:pathStr];
    [self.movieFileOut startRecordingToOutputFileURL:pathUrl recordingDelegate:self];
    
}
/**
 添加输入和输出

 @param input input
 @param output output
 */
- (void)addInput:(AVCaptureDeviceInput *)input addOutput:(AVCaptureOutput *)output{
    if([self.session canAddInput:input]){
        [self.session addInput:input];
    }
    if ([self.session canAddOutput:output]) {
        [self.session addOutput:output];
    }
}
/**
 创建capturedevice

 @param mediaType 类型
 @param position 位置
 @return 返回对象本身
 */
- (AVCaptureDevice *)captureDevice:(AVMediaType)mediaType preferringPosition:(AVCaptureDevicePosition)position{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:mediaType];
    AVCaptureDevice *captureDevice = devices.firstObject;
    for (AVCaptureDevice *device in devices) {
        if (device.position == position) {
            captureDevice = device;
        }
    }
    return captureDevice;
}
#pragma mark - 一些其他方法的响应 主要是点击事件的响应
/**
 开始采集
 */
- (IBAction)startCollection {
    // 开始采集
    [self.session startRunning];
    // 开始录制
  //  [self startRecordVideo];
}
/**
 停止采集
 */
- (IBAction)stopCollection {
    
    [self.session stopRunning];
}
/**
 保存到沙河
 */
- (IBAction)rotateDevice {
    // 先拿到之前的旋转摄像头
    if (!self.videoInput) {
        return;
    }
    AVCaptureDeviceInput *obtainInput;
    if (self.videoInput.device.position == AVCaptureDevicePositionFront) {
      AVCaptureDevice *device = [self captureDevice:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionBack];
      obtainInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];
    }else{
      AVCaptureDevice *device = [self captureDevice:AVMediaTypeVideo preferringPosition:AVCaptureDevicePositionFront];
      obtainInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];
    }
    [self.session removeInput:self.videoInput];
    if ([self.session canAddInput:obtainInput]) {
        [self.session addInput:obtainInput];
    }
    self.videoInput = obtainInput;

}
#pragma mark - delegate 的相关的事件

/**
 视频和音频的采集都经过这个方法

 @param output 输出
 @param sampleBuffer 每一帧
 @param connection 链接
 */
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
  
    AVCaptureConnection *obtainConnection = [self.videoOutput connectionWithMediaType:AVMediaTypeVideo];
    if (obtainConnection == connection) {
        NSLog(@"开始采集视频了");
    }else{
        NSLog(@"音频");
    }
}
#pragma mark 录制的想干的delegate
- (void)captureOutput:(AVCaptureFileOutput *)output didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections{
    NSLog(@"开始录制");
}
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(nullable NSError *)error{
    NSLog(@"暂停录制");
}

美颜相机就是我们对主播的磨皮、高亮、曝光等等的处理,一句话使丑的变帅变美。但是我们系统的方法目前是实现不了的,所以我使用的是GPUImage这个框架。下面我主要介绍的是美颜相机的做法和对图片的一些处理

  • 美颜相机
    观看GPUImage的官方文档以及网上的一些资料可以看到做美颜相机大致的思路就是:设置预览涂层、将预览涂层添加到滤镜上、将滤镜添加到我们的源上然后进行展示,当然苹果的plist文件添加隐私的key就不说了,因为上一篇文章有说到。我下面的这个例子,通过一个滤镜组来进行显示,滤镜组中包含了美白、 饱和、曝光、磨皮 主播可以自己调试,让自己显得更美。下面是我的代码:
#import "ViewController.h"
#import <GPUImage/GPUImage.h>
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController ()<GPUImageVideoCameraDelegate>

// 创建摄像头
@property (strong, nonatomic) GPUImageVideoCamera *camera;
@property (strong, nonatomic) GPUImageView *previewLayer;
// 创建几个滤镜
/**
 摩皮
 */
@property (strong, nonatomic) GPUImageBilateralFilter *bilaterFilter;
/**
 曝光
 */
@property (strong, nonatomic) GPUImageExposureFilter *exposureFilter;
/**
 美白
 */
@property (strong, nonatomic) GPUImageBrightnessFilter *brigtnessFilter;
/**
 饱和
 */
@property (strong, nonatomic) GPUImageSaturationFilter *saturationFilter;
/**
 创建写入的文件
 */
@property (strong, nonatomic) GPUImageMovieWriter *movieWriter;

// 底部的view
@property (weak, nonatomic) IBOutlet UIView *bottomView;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomViewBottomConstaton;
@property (strong, nonatomic) MPMoviePlayerController *moviePlayer;
@property (copy, nonatomic) NSString *moviePath;
@end
@implementation ViewController
-(GPUImageVideoCamera *)camera {
    if (!_camera) {
        _camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront];
    }
    return _camera;
}
-(GPUImageView *)previewLayer {
    if (!_previewLayer) {
        _previewLayer = [[GPUImageView alloc] initWithFrame:self.view.bounds];
    }
    return _previewLayer;
}
-(GPUImageMovieWriter *)movieWriter {
    if (!_movieWriter) {
        _movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:[self obtainUrl] size:[UIScreen mainScreen].bounds.size];
    }
    return _movieWriter;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //初始化一些滤镜
    self.bilaterFilter = [[GPUImageBilateralFilter alloc] init];
    self.exposureFilter = [[GPUImageExposureFilter alloc] init];
    self.brigtnessFilter = [[GPUImageBrightnessFilter alloc] init];
    self.saturationFilter = [[GPUImageSaturationFilter alloc] init];
    // 调整摄像头的方向
    self.camera.outputImageOrientation = UIInterfaceOrientationPortrait;
    // 调整摄像头的镜像 自己动的方向和镜子中的方向一致
    self.camera.horizontallyMirrorFrontFacingCamera = YES;
    // 创建过滤层
    GPUImageFilterGroup *filterGroup = [self obtainFilterGroup];
    [self.camera addTarget:filterGroup];
    // 将imageview 添加到过滤层上
    [filterGroup addTarget:self.previewLayer];
    [self.view insertSubview:self.previewLayer atIndex:0];
    // 开始拍摄
    [self.camera startCameraCapture];
#pragma mark - 开始写入视频
    self.movieWriter.encodingLiveVideo = YES;
    [filterGroup addTarget:self.movieWriter];
    self.camera.delegate = self;
    self.camera.audioEncodingTarget = self.movieWriter;
    // 开始录制
    [self.movieWriter startRecording];
}
/**
 获取缓存的路径

 @return 获取到自己想要的url
 */
- (NSURL *)obtainUrl{
  
    NSString *pathStr = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"456.mp4"];
    self.moviePath = pathStr;
    // 判断路径是否存在
    if ([[NSFileManager defaultManager] fileExistsAtPath:pathStr]) {
        [[NSFileManager defaultManager] removeItemAtPath:pathStr error:nil];
    }
    NSURL *url = [NSURL fileURLWithPath:pathStr];
    return url;
}
/**
 创建过滤组
 */
- (GPUImageFilterGroup *)obtainFilterGroup{
    
    GPUImageFilterGroup *group = [[GPUImageFilterGroup alloc] init];
    // 按照顺序组成一个链
    [self.bilaterFilter addTarget:self.exposureFilter];
    [self.exposureFilter addTarget:self.brigtnessFilter];
    [self.brigtnessFilter addTarget:self.saturationFilter];
    // 将滤镜添加到滤镜组中(开始和结尾)
    group.initialFilters = @[self.bilaterFilter];
    group.terminalFilter = self.saturationFilter;
    
    return group;
}
#pragma mark - 相关按钮的点击事件

/**
 结束直播相关的事件

 @param sender 按钮
 */
- (IBAction)endLiveAction:(UIButton *)sender {
   
    [self.camera stopCameraCapture];
    [self.previewLayer removeFromSuperview];
    [self.movieWriter finishRecording];
}
/**
 开始播放视频

 @param sender 按钮
 */
- (IBAction)startPlayAction:(UIButton *)sender {
    
    MPMoviePlayerController *moviePlayer = [[MPMoviePlayerController alloc] initWithContentURL:[NSURL fileURLWithPath:self.moviePath]];
    moviePlayer.view.frame = self.view.bounds;
    moviePlayer.fullscreen = YES;
    [self.view addSubview:moviePlayer.view];
    [moviePlayer play];
    self.moviePlayer = moviePlayer;
    
}

/**
 点击弹出需要设备的美颜参数

 @param sender 按钮
 */
- (IBAction)beautufulViewAction:(UIButton *)sender {
    if (self.bottomViewBottomConstaton.constant == -250) {
        self.bottomViewBottomConstaton.constant = 0;
    }else{
        self.bottomViewBottomConstaton.constant = -250;
    }
    [UIView animateWithDuration:0.25 animations:^{
        [self.view layoutIfNeeded];
    }];
}

/**
 切换前后摄像头

 @param sender 按钮
 */
- (IBAction)switchFontAndBehindCameraAction:(UIButton *)sender {
    
    [self.camera rotateCamera];
    
}

/**
 开启或者关闭美颜

 @param sender 按钮
 */
- (IBAction)closeOrOpenBeautifulAction:(UISwitch *)sender {
    if (sender.isOn) {
        [self.camera removeAllTargets];
        GPUImageFilterGroup *group = [self obtainFilterGroup];
        [self.camera addTarget:group];
        [group addTarget:self.previewLayer];
        
    }else{
        [self.camera removeAllTargets];
        [self.camera addTarget:self.previewLayer];
    }
}
/**
 磨皮的slider的事件

 @param sender 按钮
 */
- (IBAction)mopiSliderAction:(UISlider *)sender {
    
    self.bilaterFilter.distanceNormalizationFactor = sender.value * 0.3;
    
}
/**
 曝光的按钮的点击事件

 @param sender 按钮
 */
- (IBAction)baoguangSliderAction:(UISlider *)sender {
    
    self.exposureFilter.exposure = sender.value;
    
}

/**
 美白的按钮的点击事件

 @param sender 按钮
 */
- (IBAction)meibaiSliderAction:(UISlider *)sender {
    
    self.brigtnessFilter.brightness = sender.value;
}

/**
 饱和的按钮的点击事件

 @param sender 按钮
 */
- (IBAction)baoheSliderAction:(UISlider *)sender {
    
    self.saturationFilter.saturation = sender.value;
    
}
#pragma mark - camera 的 delegate

- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    NSLog(@"+++++++++++++++++");
}
@end

有没有发现 使用GPUIImage做的相机 是不是很简单,我曾经大致看一下他的原码,里面他也是封装系统的方法 只不过他加了自己的一些矩阵 等等的算法和一些相关的图形处理的相关的知识(这个人虽然看他的写的代码风格一般但是他的专功知识还是挺六的,看了他的git 这个10000多个星,其他的他的框架星星很少)

  • 下面我做的图片增加滤镜的 ,实现思路和上边一样,代码写的一般只是为了实现功能,并没有做过多的抽取以及封装。
#import "ViewController.h"
#import "GPUImage-umbrella.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *displayImageView;


@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
   
    
}

/**
 褐色的按钮的点击事件

 @param sender 阿牛
 */
- (IBAction)brownAction:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageSepiaFilter *brownFilter = [[GPUImageSepiaFilter alloc] init];
    // 设置渲染区域
    [brownFilter forceProcessingAtSize:self.displayImageView.image.size];
    [brownFilter useNextFrameForImageCapture];
    // 创建数据源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 将滤镜添加到数据源上
    [imageSource addTarget:brownFilter];
    // 开始渲染
    [imageSource processImage];
    // 生成新的图片
    UIImage *image = [brownFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = image;
    
    
//    self.displayImageView.image = [UIImage imageNamed:@"test"];
//    // 添加黑白素描滤镜
//    GPUImageSketchFilter *stillFilter = [[GPUImageSketchFilter alloc] init];
//    // 设置渲染区域
//    [stillFilter forceProcessingAtSize:self.displayImageView.image.size];
//    [stillFilter useNextFrameForImageCapture];
//    // 获取数据源
//    GPUImagePicture *stilImageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
//    // 添加滤镜
//    [stilImageSource addTarget:stillFilter];
//    // 开始渲染
//    [stilImageSource processImage];
//    // 生成新的图片
//    UIImage *newImage = [stillFilter imageFromCurrentFramebuffer];
//    self.displayImageView.image = newImage;
    
}
/**
 卡通的事件

 @param sender 按钮
 */
- (IBAction)cartoon:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageToonFilter *cartoolFilter = [[GPUImageToonFilter alloc] init];
    // 创建数据源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 设置渲染区域
    [cartoolFilter forceProcessingAtSize:self.displayImageView.image.size];
    [cartoolFilter useNextFrameForImageCapture];
    // 将滤镜添加到数据源上
    [imageSource addTarget:cartoolFilter];
    // 开始渲染
    [imageSource processImage];
    // 生成新的图片
    UIImage *image = [cartoolFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = image;

    
}
/**
 素描的事件

 @param sender 按钮
 */
- (IBAction)sketch:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageSketchFilter *sketchFileter = [[GPUImageSketchFilter alloc] init];
    [self filterImage:sketchFileter];
    
    
}

/**
 浮雕的事件

 @param sender 按钮
 */
- (IBAction)reliefAction:(UIButton *)sender {
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    GPUImageEmbossFilter *embossFilter = [[GPUImageEmbossFilter alloc] init];
    // 设置渲染区域
    [embossFilter forceProcessingAtSize:self.displayImageView.image.size];
    [embossFilter useNextFrameForImageCapture];
    // 创建数据源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 将滤镜添加到数据源上
    [imageSource addTarget:embossFilter];
    // 开始渲染
    [imageSource processImage];
    // 生成新的图片
    UIImage *image = [embossFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = image;
}

- (UIImage *)filterImage:(GPUImageSobelEdgeDetectionFilter *)filter {
    
    // 创建数据源
    GPUImagePicture *imageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 设置渲染区域
    [filter forceProcessingAtSize:self.displayImageView.image.size];
    [filter useNextFrameForImageCapture];
    // 将滤镜添加到数据源上
    [imageSource addTarget:filter];
    // 开始渲染
    [imageSource processImage];
    // 生成新的图片
    UIImage *image = [filter imageFromCurrentFramebuffer];
    return image;
}
/**
 创建一个黑白的素描的图片
 */
- (void)createSketchImage {
   
    
    self.displayImageView.image = [UIImage imageNamed:@"test"];
    // 添加黑白素描滤镜
    GPUImageSketchFilter *stillFilter = [[GPUImageSketchFilter alloc] init];
    // 设置渲染区域
    [stillFilter forceProcessingAtSize:self.displayImageView.image.size];
    [stillFilter useNextFrameForImageCapture];
    // 获取数据源
    GPUImagePicture *stilImageSource = [[GPUImagePicture alloc] initWithImage:self.displayImageView.image];
    // 添加滤镜
    [stilImageSource addTarget:stillFilter];
    // 开始渲染
    [stilImageSource processImage];
    // 生成新的图片
    UIImage *newImage = [stillFilter imageFromCurrentFramebuffer];
    self.displayImageView.image = newImage;
}

@end

正常的图片:


image.png

加了褐色滤镜的:


image.png

加了卡通滤镜的:
image.png

素描的滤镜:


image.png

浮雕的滤镜:
image.png

基本的简单使用就是这些 ,但是要想好好的掌握GPUImage 这些远远不够,可以去官方文档上去看说的也挺详细的 ,还有大家网上一搜 很多的各种滤镜的解释都有 我们主要的目的是直播的原理 以及流程这里不在做多的说明。
  • 我们录制的视频经过美颜之后我们就应该对视频进行编码,编码方式其中分为硬编码和软编码,其中苹果在ios8之后出了一套框架,我们利用这个框架可以进行硬编码,不过基本上是纯c的
    首先我们需要明白做直播的视频为什么要经过编码:因为正常的没有经过编码的视频是非常非常大的,我们本来做直播需要把视频传递给服务器 然后让服务器进行流分发,如果过大 即使在wifi的情况下 还是会卡顿 或者造成视频的显示问题。
    其次我们需要研究视频构成或者说视频有哪些是我们所谓说的冗余
  • 其中视频的冗余有:时间、空间、还有我们的人眼冗余
    1.时间上的:比如多张图片基本一样 只有一个细小的差别,这种我们只需要记录一张共同的图片和其他的不同点就可以了 ,比如这个就是时间上的冗余:


    image.png
  1. 空间上的冗余:
    空间上的冗余是很多的点都一样的,比图我这张图片:


    image.png
  2. 还有就是我们的视觉冗余:
    人体的视觉对高频信息不敏感,对运动、高对比度信息等等更敏感,所以我们编码视频可以去掉高频的信息。
    如:


    image.png
  • 下面我们可以对视频进行压缩编码,但是压缩编码我们需要按照一定的标准否则到时候进行解码解码不了,现在我说一下一些编码标准:
  1. h.26X系列的
    1.1 h.261 主要在老的视频会议和老的视频电话中使用。
    1.2 h.263 主要用电视频会议和视频电话中和一些网络视频上。
    1.3 h.264 是一种高精度的压缩标准、广泛被使用在视频的录制、压缩和发布格式
    1.4 h.265 是一种更高效率的压缩标准、可支持4K分辨率甚至到超高画质电视,最高分辨率可达到8192×4320(8K分辨率),但是现在市场上没有被大众普遍使用,而且稳定性不确定,所以目前最常用的是h.264
  2. MPEG系列(由ISO[国际标准组织机构]下属的MPEG[运动图象专家组]开发)
    2.1MPEG-1第二部分:MPEG-1第二部分主要使用在VCD上,有些在线视频也使用这种格式
    2.2 MPEG-2第二部分(MPEG-2第二部分等同于H.262,使用在DVD、SVCD和大多数数字视频广播系统中.
  • h.264的编码大致流程:
    个人理解 首先他会生成一个图像帧a,然后他会生成图像帧b ,但是生成b的时候他只会生成与生成与a 有差别的地方,同理图像帧c也是这个道理,以此类推,这样的一组图像帧我们称之为一个序列,序列就是有相同特点的图像帧,当发现图像帧和a的差别很大 那就从新生成一个序列。
  1. h .264定义了三种帧:
    i帧:生成的完整的帧叫i帧
    p帧:参考之前a帧生成的只包含差异的帧叫p帧
    b帧:由i帧和p帧生成的帧叫b帧
  2. h.264参考的核心算法是帧间压缩和帧内压缩
    帧内压缩:主要对i帧
    帧间压缩:主要是p帧和b帧
  3. h.264分层设计:
    3.1
    视频编码层:
    负责高效率的内容表示。
    网络提取层:
    负责网络恰当的方式进行打包和传送。这样视频封装和网络友好行分别由vcl和nal分别完成。我们之前学习的编码方式都是vcl。
    nal设计的目的:
    根据不同的网络环境,将vcl产生的比特字符串友好的适配到各个网络环境中。
    nal的组成:
    有一个nal序列单元组成,分别为nal的头和nal的体组成。
    nal的头通常由00 00 00 01担任,并且作为一个新的nal的开始
    nal的体中疯转vcl的编码信息或者其他的信息。
    封装过程:
    i帧p帧或者b帧 都被封装成一个nal单元或者n个nal单元进行存储。
    i帧开始之前也有非vcl的nal单元,用于保存其他的信息 比如:pps (图像参数集)、sps(序列参数集)。
    一般的流程都是先编辑pps、sps 然后是i帧 、p帧、b帧。
  • 编码方式:
    硬编码:使用非cpu进行编码,如显卡gpu 、专用的dsp等。
    软编码:使用cpu进行编码,一般都是ffmpeg+x264.
    两种方式对比:
    软编码修改参数简单,实现简单,升级易但是耗费cpu的资源大,手机容易发烫。
    硬编码消耗cpu资源少,但是参数调整不如软编码方便,代码基本固定。
  • 了解了编码方式以及编码的标准,下面是我的硬编码的实现:
VideoEncoder.h 文件:
#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>

@interface VideoEncoder : NSObject

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;

@end
VideoEncoder.m文件:
#import "VideoEncoder.h"

@interface VideoEncoder()

/** 记录当前的帧数 */
@property (nonatomic, assign) NSInteger frameID;

/** 编码会话 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSession;

/** 文件写入对象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;

@end

@implementation VideoEncoder

- (instancetype)init {
    if (self = [super init]) {
        // 1.初始化写入文件的对象(NSFileHandle用于写入二进制文件)
        [self setupFileHandle];
        
        // 2.初始化压缩编码的会话
        [self setupVideoSession];
    }
    
    return self;
}

- (void)setupFileHandle {
    // 1.获取沙盒路径
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.h264"];
    
    // 2.如果原来有文件,则删除
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    
    // 3.创建对象
    self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}


- (void)setupVideoSession {
    // 1.用于记录当前是第几帧数据(画面帧数非常多)
    self.frameID = 0;
    
    // 2.录制视频的宽度&高度
    int width = [UIScreen mainScreen].bounds.size.width;
    int height = [UIScreen mainScreen].bounds.size.height;
    
    // 3.创建CompressionSession对象,该对象用于对画面进行编码
    // kCMVideoCodecType_H264 : 表示使用h.264进行编码
    // didCompressH264 : 当一次编码结束会在该函数进行回调,可以在该函数中将数据,写入文件中
    VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &_compressionSession);
    
    // 4.设置实时编码输出(直播必然是实时输出,否则会有延迟)
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    
    // 5.设置期望帧率(每秒多少帧,如果帧率过低,会造成画面卡顿)
    int fps = 30;
    CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
    
    // 6.设置码率(码率: 编码效率, 码率越高,则画面越清晰, 如果码率较低会引起马赛克 --> 码率高有利于还原原始画面,但是也不利于传输)
    int bitRate = 800*1024;
    CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
    // 这是一个算法
    NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    
    // 7.设置关键帧(GOPsize)间隔
    int frameInterval = 30;
    CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
    VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
    // 8.基本设置结束, 准备进行编码
    VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
}


// 编码完成回调
void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
    // 1.判断状态是否等于没有错误
    if (status != noErr) {
        return;
    }
    
    // 2.根据传入的参数获取对象
    VideoEncoder* encoder = (__bridge VideoEncoder*)outputCallbackRefCon;
    
    // 3.判断是否是关键帧
    bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
    // 判断当前帧是否为关键帧
    // 获取sps & pps数据
    if (isKeyframe)
    {
        // 获取编码后的信息(存储于CMFormatDescriptionRef中)
        CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
        
        // 获取SPS信息
        size_t sparameterSetSize, sparameterSetCount;
        const uint8_t *sparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
        
        // 获取PPS信息
        size_t pparameterSetSize, pparameterSetCount;
        const uint8_t *pparameterSet;
        CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
        
        // 装sps/pps转成NSData,以方便写入文件
        NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
        NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
        
        // 写入文件
        [encoder gotSpsPps:sps pps:pps];
    }
    
    // 获取数据块
    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
    size_t length, totalLength;
    char *dataPointer;
    OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
    if (statusCodeRet == noErr) {
        size_t bufferOffset = 0;
        static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
        
        // 循环获取nalu数据
        while (bufferOffset < totalLength - AVCCHeaderLength) {
            uint32_t NALUnitLength = 0;
            // Read the NAL unit length
            memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
            
            // 从大端转系统端
            NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
            
            NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
            [encoder gotEncodedData:data isKeyFrame:isKeyframe];
            
            // 移动到写一个块,转成NALU单元
            // Move to the next NAL unit in the block buffer
            bufferOffset += AVCCHeaderLength + NALUnitLength;
        }
    }
}

- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
    // 1.拼接NALU的header
    const char bytes[] = "\x00\x00\x00\x01";
    size_t length = (sizeof bytes) - 1;
    NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
    // 2.将NALU的头&NALU的体写入文件
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:sps];
    [self.fileHandle writeData:ByteHeader];
    [self.fileHandle writeData:pps];
    
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
    NSLog(@"gotEncodedData %d", (int)[data length]);
    if (self.fileHandle != NULL)
    {
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:data];
    }
}

- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    // 1.将sampleBuffer转成imageBuffer
    CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.根据当前的帧数,创建CMTime的时间
    CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
    VTEncodeInfoFlags flags;
    
    // 3.开始编码该帧数据
    OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                          imageBuffer,
                                                          presentationTimeStamp,
                                                          kCMTimeInvalid,
                                                          NULL, (__bridge void * _Nullable)(self), &flags);
    if (statusCode == noErr) {
        NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
    }
}

- (void)endEncode {
    VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);
    VTCompressionSessionInvalidate(self.compressionSession);
    CFRelease(self.compressionSession);
    self.compressionSession = NULL;
}
@end
  • 软编码的实现:
    软编码的环境搭建要复杂些:其中电脑中要先安装ffmpeg
    顺便说下ffmpeg他是纯c的 他可以分流 也可以推流 是一些高手中的高手集成的命令行工具。感兴趣的可以继续研究一下,我对此理解的不是很深。

Mac安装/使用FFmpeg

  • 安装
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    brew install ffmpeg
  • 简单使用
    转化格式: ffmpeg -i 你的视频.webm 你的视频.mp4
    分离视频: ffmpeg -i 你的视频.mp4 -vcodec copy -an 你的视频.mp4
    分离音频: ffmpeg -i 你的视频.mp4 -acodec copy -vn 你的视频.aac

编译FFmpeg(iOS)

编译X264

  • x264官网 下载x264源码,将其文件夹名称改为x264
  • https://www.videolan.org/developers/x264.html
  • 下载gas-preprocessor(FFmpeg编译时已经下载过)
  • 下载x264 build shell
  • 修改权限/执行脚本
    • sudo chmod u+x build-x264.sh
    • sudo ./build-x264.sh
      最后得到的是这两个文件夹


      image.png

      如果生成的文件夹里面包含这些东西 基本就对了。
      下面是我的软编码实现:(实话说硬编码我还能写出一部分,软编码我真的一点写不出来,但是我找的资料还可以 我基本弄明白了,根据我说的视频压缩标准,应该知道每一部分在干什么,基本代码固定会调试参数就行了)

X264Manager.h文件:
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>

@interface X264Manager : NSObject

/*
 * 设置编码后文件的保存路径
 */
- (void)setFileSavedPath:(NSString *)path;

/*
 * 设置X264
 * 0: 成功; -1: 失败
 * width: 视频宽度
 * height: 视频高度
 * bitrate: 视频码率,码率直接影响编码后视频画面的清晰度, 越大越清晰,但是为了便于保证编码后的数据量不至于过大,以及适应网络带宽传输,就需要合适的选择该值
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate;

/*
 * 将CMSampleBufferRef格式的数据编码成h264并写入文件
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer;

/*
 * 释放资源
 */
- (void)freeX264Resource;
@end
X264Manager.m 文件:
#import "X264Manager.h"

#ifdef __cplusplus
extern "C" {
#endif
    
#include <libavutil/opt.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#ifdef __cplusplus
};
#endif


/*
 编码之后&解码之前的画面: AVFrame --> 内容写入文件
 编码之前&解码之后的画面: AVPackage --> 解码之后, 使用OpenGLES渲染
 */

@implementation X264Manager
{
    AVFormatContext                     *pFormatCtx;
    AVOutputFormat                      *fmt;
    AVStream                            *video_st;
    AVCodecContext                      *pCodecCtx;
    AVCodec                             *pCodec;
    AVPacket                             pkt;
    uint8_t                             *picture_buf;
    AVFrame                             *pFrame;
    int                                  picture_size;
    int                                  y_size;
    int                                  framecnt;
    char                                *out_file;
    
    int                                  encoder_h264_frame_width; // 编码的图像宽度
    int                                  encoder_h264_frame_height; // 编码的图像高度
}



/*
 * 设置编码后文件的文件名,保存路径
 */
- (void)setFileSavedPath:(NSString *)path;
{
    out_file = [self nsstring2char:path];
}

/*
 * 将路径转成C语言字符串(传入路径为C字符串)
 */
- (char*)nsstring2char:(NSString *)path
{

    NSUInteger len = [path length];
    char *filepath = (char*)malloc(sizeof(char) * (len + 1));
    
    [path getCString:filepath maxLength:len + 1 encoding:[NSString defaultCStringEncoding]];
    
    return filepath;
}


/*
 *  设置X264
 */
- (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate
{
    // 1.默认从第0帧开始(记录当前的帧数)
    framecnt = 0;
    
    // 2.记录传入的宽度&高度
    encoder_h264_frame_width = width;
    encoder_h264_frame_height = height;
    
    // 3.注册FFmpeg所有编解码器(无论编码还是解码都需要该步骤)
    av_register_all();
    
    // 4.初始化AVFormatContext: 用作之后写入视频帧并编码成 h264,贯穿整个工程当中(释放资源时需要销毁)
    pFormatCtx = avformat_alloc_context();
    
    // 5.设置输出文件的路径
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;
    
    // 6.打开文件的缓冲区输入输出,flags 标识为  AVIO_FLAG_READ_WRITE ,可读写
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
        printf("Failed to open output file! \n");
        return -1;
    }
    
    // 7.创建新的输出流, 用于写入文件
    video_st = avformat_new_stream(pFormatCtx, 0);
    
    // 8.设置 20 帧每秒 ,也就是 fps 为 20
    video_st->time_base.num = 1;
    video_st->time_base.den = 25;
    
    if (video_st==NULL){
        return -1;
    }
    
    // 9.pCodecCtx 用户存储编码所需的参数格式等等
    // 9.1.从媒体流中获取到编码结构体,他们是一一对应的关系,一个 AVStream 对应一个  AVCodecContext
    pCodecCtx = video_st->codec;
    
    // 9.2.设置编码器的编码格式(是一个id),每一个编码器都对应着自己的 id,例如 h264 的编码 id 就是 AV_CODEC_ID_H264
    pCodecCtx->codec_id = fmt->video_codec;
    
    // 9.3.设置编码类型为 视频编码
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    
    // 9.4.设置像素格式为 yuv 格式
    pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
    
    // 9.5.设置视频的宽高
    pCodecCtx->width = encoder_h264_frame_width;
    pCodecCtx->height = encoder_h264_frame_height;
    
    // 9.6.设置帧率
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    
    // 9.7.设置码率(比特率)
    pCodecCtx->bit_rate = bitrate;
    
    // 9.8.视频质量度量标准(常见qmin=10, qmax=51)
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    
    // 9.9.设置图像组层的大小(GOP-->两个I帧之间的间隔)
    pCodecCtx->gop_size = 30;
    
    // 9.10.设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,也就是说相同码率的情况下,
    // 越多 B 帧的视频,越清晰,现在很多打视频网站的高清视频,就是采用多编码 B 帧去提高清晰度,
    // 但同时对于编解码的复杂度比较高,比较消耗性能与时间
    pCodecCtx->max_b_frames = 5;
    
    // 10.可选设置
    AVDictionary *param = 0;
    // H.264
    if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        // 通过--preset的参数调节编码速度和质量的平衡。
        av_dict_set(&param, "preset", "slow", 0);
        
        // 通过--tune的参数值指定片子的类型,是和视觉优化的参数,或有特别的情况。
        // zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如视频直播的编码
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    
    // 11.输出打印信息,内部是通过printf函数输出(不需要输出可以注释掉该局)
    av_dump_format(pFormatCtx, 0, out_file, 1);
    
    // 12.通过 codec_id 找到对应的编码器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        printf("Can not find encoder! \n");
        return -1;
    }
    
    // 13.打开编码器,并设置参数 param
    if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
        printf("Failed to open encoder! \n");
        return -1;
    }
    
    // 13.初始化原始数据对象: AVFrame
    pFrame = av_frame_alloc();
    
    // 14.通过像素格式(这里为 YUV)获取图片的真实大小,例如将 480 * 720 转换成 int 类型
    avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
    
    // 15.h264 封装格式的文件头部,基本上每种编码都有着自己的格式的头部,想看具体实现的同学可以看看 h264 的具体实现
    avformat_write_header(pFormatCtx, NULL);
    
    // 16.创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据
    av_new_packet(&pkt, picture_size);
    
    // 17.设置 yuv 数据中 y 图的宽高
    y_size = pCodecCtx->width * pCodecCtx->height;
    
    return 0;
}

/*
 * 将CMSampleBufferRef格式的数据编码成h264并写入文件
 * 
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通过CMSampleBufferRef对象获取CVPixelBufferRef对象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.锁定imageBuffer内存地址开始进行编码
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.从CVPixelBufferRef读取YUV的值
        // NV12和NV21属于YUV格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane
        // 3.1.获取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.获取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        
        // 3.3.根据像素获取图片的真实宽度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 获取Y分量长度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/2);
        
        // 3.4.将NV12数据转成YUV420P(I420)数据
        UInt8 *pY = bufferPtr ;
        UInt8 *pUV = bufferPtr1;
        UInt8 *pU = yuv420_data + width*height;
        UInt8 *pV = pU + width*height/4;
        for(int i =0;i<height;i++)
        {
            memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
        }
        for(int j = 0;j<height/2;j++)
        {
            for(int i =0;i<width/2;i++)
            {
                *(pU++) = pUV[i<<1];
                *(pV++) = pUV[(i<<1) + 1];
            }
            pUV+=bytesrow1;
        }
        
        // 3.5.分别读取YUV的数据
        picture_buf = yuv420_data;
        pFrame->data[0] = picture_buf;              // Y
        pFrame->data[1] = picture_buf+ y_size;      // U
        pFrame->data[2] = picture_buf+ y_size*5/4;  // V
        
        // 4.设置当前帧
        pFrame->pts = framecnt;
        int got_picture = 0;
        
        // 4.设置宽度高度以及YUV各式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = PIX_FMT_YUV420P;
        
        // 5.对编码前的原始数据(AVFormat)利用编码器进行编码,将 pFrame 编码后的数据传入pkt 中
        int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);
        if(ret < 0) {
            printf("Failed to encode! \n");
            
        }
        
        // 6.编码成功后写入 AVPacket 到 输入输出数据操作着 pFormatCtx 中,当然,记得释放内存
        if (got_picture==1) {
            framecnt++;
            pkt.stream_index = video_st->index;
            ret = av_write_frame(pFormatCtx, &pkt);
            av_free_packet(&pkt);
        }
        
        // 7.释放yuv数据
        free(yuv420_data);
    }
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}


/*
 * 释放资源
 */
- (void)freeX264Resource
{
    // 1.释放AVFormatContext
    int ret = flush_encoder(pFormatCtx,0);
    if (ret < 0) {
        printf("Flushing encoder failed\n");
    }
    
    // 2.将还未输出的AVPacket输出出来
    av_write_trailer(pFormatCtx);
    
    // 3.关闭资源
    if (video_st){
        avcodec_close(video_st->codec);
        av_free(pFrame);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
}

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
          CODEC_CAP_DELAY))
        return 0;
    
    while (1) {
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                     NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame){
            ret=0;
            break;
        }
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}
@end

外面调用:

#import "ViewController.h"
#import <GPUImage/GPUImage.h>
#import <AVFoundation/AVFoundation.h>
#import "X264Manager.h"

@interface ViewController ()<GPUImageVideoCameraDelegate>
// 创建摄像头
@property (strong, nonatomic) GPUImageVideoCamera *camera;
@property (strong, nonatomic) GPUImageView *previewLayer;
// 创建几个滤镜
/**
 摩皮
 */
@property (strong, nonatomic) GPUImageBilateralFilter *bilaterFilter;
/**
 曝光
 */
@property (strong, nonatomic) GPUImageExposureFilter *exposureFilter;
/**
 美白
 */
@property (strong, nonatomic) GPUImageBrightnessFilter *brigtnessFilter;
/**
 饱和
 */
@property (strong, nonatomic) GPUImageSaturationFilter *saturationFilter;

@property (strong, nonatomic) X264Manager *encoder;

@end

@implementation ViewController
-(GPUImageVideoCamera *)camera {
    if (!_camera) {
        _camera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront];
    }
    return _camera;
}
-(GPUImageView *)previewLayer {
    if (!_previewLayer) {
        _previewLayer = [[GPUImageView alloc] initWithFrame:self.view.bounds];
    }
    return _previewLayer;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.encoder = [[X264Manager alloc] init];
    // 1.获取沙盒路径
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"abc.h264"];
    // 2.开始编码
    [self.encoder setFileSavedPath:file];
    // 特别注意: 宽度&高度
    [self.encoder setX264ResourceWithVideoWidth:720 height:1280 bitrate:1024*800];
    
    
    // 初始化一些滤镜
    self.bilaterFilter = [[GPUImageBilateralFilter alloc] init];
    self.exposureFilter = [[GPUImageExposureFilter alloc] init];
    self.brigtnessFilter = [[GPUImageBrightnessFilter alloc] init];
    self.saturationFilter = [[GPUImageSaturationFilter alloc] init];
    // 调整摄像头的方向
    //    self.camera.outputImageOrientation = UIInterfaceOrientationPortrait;
    // 设置竖屏 否则露出来的视频 和我们想要的不一样
    self.camera.videoCaptureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    // 调整摄像头的镜像 自己动的方向和镜子中的方向一致
    self.camera.videoCaptureConnection.videoMirrored = YES;
    self.camera.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
    //    self.camera.horizontallyMirrorFrontFacingCamera = YES;
    // 创建过滤层
    GPUImageFilterGroup *filterGroup = [self obtainFilterGroup];
    [self.camera addTarget:filterGroup];
    // 将imageview 添加到过滤层上
    [filterGroup addTarget:self.previewLayer];
    [self.view insertSubview:self.previewLayer atIndex:0];
    self.camera.delegate = self;
    // 开始拍摄
    [self.camera startCameraCapture];
    
}
/**
 创建过滤组
 */
- (GPUImageFilterGroup *)obtainFilterGroup{
    
    GPUImageFilterGroup *group = [[GPUImageFilterGroup alloc] init];
    // 按照顺序组成一个链
    [self.bilaterFilter addTarget:self.exposureFilter];
    [self.exposureFilter addTarget:self.brigtnessFilter];
    [self.brigtnessFilter addTarget:self.saturationFilter];
    // 将滤镜添加到滤镜组中(开始和结尾)
    group.initialFilters = @[self.bilaterFilter];
    group.terminalFilter = self.saturationFilter;
    
    return group;
}
/**
 结束直播相关的事件
 
 @param sender 按钮
 */
- (IBAction)endLiveAction:(UIButton *)sender {
    
    [self.camera stopCameraCapture];
    [self.previewLayer removeFromSuperview];
    [self.encoder freeX264Resource];

}
#pragma mark - camera 的 delegate

- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    NSLog(@"---------------");
    [self.encoder encoderToH264:sampleBuffer];
}
@end

如果ffmpeg+x264打包不成功的 可以找我要 我直接发给你完整的包,加我qq:2586784401 ,添加留言:ffmpeg+x264 ,否则不理哈。
值得注意的是软编码这块

 // 特别注意: 宽度&高度
    [self.encoder setX264ResourceWithVideoWidth:720 height:1280 bitrate:1024*800];

因为视频我们是竖屏录制一般,所以宽度和高度应该和分辨率的相反。
软编码中的yuv:

yuv是什么?

  • 视频是由一帧一帧的数据连接而成,而一帧视频数据其实就是一张图片。
    yuv是一种图片储存格式,跟RGB格式类似。
    RGB格式的图片很好理解,计算机中的大多数图片,都是以RGB格式存储的。
  • yuv中,y表示亮度,单独只有y数据就可以形成一张图片,只不过这张图片是灰色的。u和v表示色差(u和v也被称为:Cb-蓝色差,Cr-红色差)
  • 为什么要yuv?
    有一定历史原因,最早的电视信号,为了兼容黑白电视,采用的就是yuv格式。
    一张yuv的图像,去掉uv,只保留y,这张图片就是黑白的。
    而且yuv可以通过抛弃色差来进行带宽优化。
    比如yuv420格式图像相比RGB来说,要节省一半的字节大小,抛弃相邻的色差对于人眼来说,差别不大。

YUV颜色存储格式

  • 常用的I420(YUV420P),NV12(YUV420SP),YV12(YUV420P),NV21(YUV420SP)等都是属于YUV420,NV12是一种两平面存储方式,Y为一个平面,交错的UV为另一个平面
  • 通常,用来远程传输的是I420数据,而本地摄像头采集的是NV12数据。(iOS)
    所有在真正编码的过程中, 需要将NV12数据转成I420数据进行编码
  • 编码方式完成之后需要研究传输协议,其中传输协议最常见的就是rtmp 和
    hls

最常见的协议有rtmp协议和hls协议

  • rtmp协议:
    RTMP协议是 Adobe 公司开发的一个基于TCP的应用层协议,Adobe 公司也公布了关于RTMP的规范
    1.1 它的优点:
    实时性高:一般它的延迟在3秒左右,正常的可能在5秒左右,一般实时行高用的都是rtmp
    支持加密:加密方式:rtmpe 和rtmps为加密方式。
    稳定行高:http虽然也稳定,但是稳定性不至在服务器上还有在cdn分发上,rtmp都能很好的支持。
    1.2 rtmp的原理:
    rtmp也需要和服务器通过“握手”基于传输层建立基于rtmp协议的链接。
    rtmp在传输的时候会做自己的格式化,这种格式化我们称之为:rtmp Message
    rtmp为了实现多路复用,公平性等等,它一般在发送方会组装消息的头里面封装消息的id的chunk,消息体的数据长度等等 ,会发送一个或者部分完整包给接收方,接收方在收到消息时进行还原(个人觉得有点像我们自己定义的socket协议)。
  • hls:
    1.1 HTTP Live Streaming(HLS)是苹果公司实现的基于HTTP的流媒体传输协议,可实现流媒体的直播和点播。原理上是将视频流分片成一系列HTTP下载文件。所以,HLS比RTMP有较高的延迟。HLS基于HTTP协议实现,传输内容包括两部分,一是M3U8描述文件,二是TS媒体文件
    相对于常见的流媒体直播协议,例如RTMP协议、RTSP协议、MMS协议等,HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件,因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。
    由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。
    1.2 hls 实现原理:
    采集数据
    对原始数据进行h264编码和aac编码
    视频或者音频封装成mpeg-ts包
    hls分段生成策略以及m3u8索引文件
    http传输协议进行传输数据
    1.3 用一张图片进行解释:


    image.png

    这个命令可以把视频 切成一个个m3u8的索引文件
    ffmpeg -i XXX.mp4 -c:v libx264 -c:a copy -f hls XXX.m3u8
    总结:
    一般实时性较高的一般使用rtmp协议,对实时性要求不高可以使用hls协议。一般来讲我们做直播用的都是rtmp协议。
    我们知道了传输协议了 但是对于我们普通人而言或者做过一点点直播人而言实现推流还是比较难的,因为这里面需要做很多工作,(比如你需要知道rtmp协议具体每一个小步怎么做的、怎么进行推送等等),但是在it中就是大牛很多 而且他们一般都不怎么说话,对于oc 而言 现在用到的推流框架最好的就是这个LFLiveKit,它不仅可以推rtmp 也可以推送 hls协议的,星目前3000多个 ,其中它还有实现美颜相机的部分,个人对它的一点点不满就是觉得它美颜相机可调式美颜功能太少 ,你要做类似于斗鱼那样设置 曝光、磨皮、美颜、饱和度等等的 你需要fork一份到自己的仓库中 然后进行修改美颜相机那个文件,默认的好像只有美颜 和 饱和度它的这个框架,目前发现它已经两年没更新了 不用怕 一般这种都是底层的东西 变化不大。
    下面是我简单使用,说实话用了这个框架自己真的是太省心了:

#import "ViewController.h"
#import <LFLiveKit/LFLiveKit.h>


@interface ViewController ()<LFLiveSessionDelegate>

@property (strong, nonatomic) LFLiveSession *session;


@end

@implementation ViewController
- (LFLiveSession*)session {
    if (!_session) {
        LFLiveVideoConfiguration * liveConfigation = [LFLiveVideoConfiguration defaultConfigurationForQuality:LFLiveVideoQuality_High2 outputImageOrientation:UIInterfaceOrientationPortrait];
        _session = [[LFLiveSession alloc] initWithAudioConfiguration:[LFLiveAudioConfiguration defaultConfiguration] videoConfiguration:liveConfigation];
        _session.preView = self.view;
        _session.delegate = self;
    }
    return _session;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self stopLive];
    // 是否需要美颜
    self.session.beautyFace = NO;
}
- (IBAction)startLive {
    LFLiveStreamInfo *streamInfo = [LFLiveStreamInfo new];
    //类似于: rtmp://localhost:1935/rtmplive/home
    streamInfo.url = @"推流地址";
    [self.session startLive:streamInfo];
    self.session.running = YES;
}
- (IBAction)switchCamera:(UIButton *)sender {
    if (self.session.captureDevicePosition == 1) {
        self.session.captureDevicePosition = 2;
    }else{
        self.session.captureDevicePosition = 1;
    }
}

- (IBAction)stopLive {
    [self.session stopLive];
}
- (IBAction)beauty:(UISlider *)sender {
    self.session.beautyLevel = sender.value;
}
- (IBAction)bright:(UISlider *)sender {
    self.session.brightLevel = sender.value;
}
- (IBAction)zoom:(UISlider *)sender {
    self.session.zoomScale = sender.value;
}
#pragma mark - delegate
/** live status changed will callback */
- (void)liveSession:(nullable LFLiveSession *)session liveStateDidChange:(LFLiveState)state{
    NSLog(@"LFLiveState -- %zd",state);
}
/** live debug info callback */
- (void)liveSession:(nullable LFLiveSession *)session debugInfo:(nullable LFLiveDebug *)debugInfo{
    NSLog(@"LFLiveDebug -- %@",debugInfo);
}
/** callback socket errorcode */
- (void)liveSession:(nullable LFLiveSession *)session errorCode:(LFLiveSocketErrorCode)errorCode{
    NSLog(@"LFLiveSocketErrorCode -- %zd",errorCode);
}
@end

其中注意一个地方 ,如果你按照官方文档写 是录制不了视频的,必须加上一句话(作者官方文档中是没有说的,猜了好久,不知道自己二还是作者马虎)

self.session.running = YES;

还需要注意一个地方:一般来说如果我们没有做过直播项目 我们就需要自己搭建本地的nginx 和 rtmp协议的服务器,用我们自己的电脑。(自己百度一下,我也是遇到好多错误后来解决了)

  • 推流到服务器,服务器进行流分发 其他用户的手机就会对视频进行解码,一般来说解码的工作量更加的巨大,我们一般人解码也不太现实,我们也是用的框架,最常用的框架就是:ijkplayer,这个是b站进行开源的项目,也是纯c的,但是打包我们自己的ios项目
    需要进行如下的操作:
第一步:
# install homebrew, git, yasm
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew install git
brew install yasm

# add these lines to your ~/.bash_profile or ~/.profile
# export ANDROID_SDK=<your sdk path>
# export ANDROID_NDK=<your ndk path>

# on Cygwin (unmaintained)
# install git, make, yasm
第二部:
git clone https://github.com/Bilibili/ijkplayer.git ijkplayer-ios
cd ijkplayer-ios
git checkout -B latest k0.8.8

./init-ios.sh

cd ios
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all

# Demo
#     open ios/IJKMediaDemo/IJKMediaDemo.xcodeproj with Xcode
# 
# Import into Your own Application
#     Select your project in Xcode.
#     File -> Add Files to ... -> Select ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj
#     Select your Application's target.
#     Build Phases -> Target Dependencies -> Select IJKMediaFramework
#     Build Phases -> Link Binary with Libraries -> Add:
#         IJKMediaFramework.framework
#
#         AudioToolbox.framework
#         AVFoundation.framework
#         CoreGraphics.framework
#         CoreMedia.framework
#         CoreVideo.framework
#         libbz2.tbd
#         libz.tbd
#         MediaPlayer.framework
#         MobileCoreServices.framework
#         OpenGLES.framework
#         QuartzCore.framework
#         UIKit.framework
#         VideoToolbox.framework
#
#         ... (Maybe something else, if you get any link error)
# 

最后你需要编译模拟器和真机的包,打成一个静态库,(怎么打包一个framework不会的自己百度一下,这里不说了)
不得不说我shell方面不怎么样,我按照它一步一步执行的 但是确实还是不成功,最后包一个这个错误:

AR  libavfilter/libavfilter.a
AR  libavformat/libavformat.a
AS  libavcodec/arm/aacpsdsp_neon.o
CC  libavcodec/arm/blockdsp_init_neon.o
./libavutil/arm/asm.S:50:9: error: unknown directive
        .arch armv7-a
        ^

打包不成功,按照https://www.colabug.com/2876591.html这篇文章就可以了,原来是xcode9.3 对arm7支持变弱了,如果你的xcode<= 9.1 按照官网执行应该没问题。
下面是我的解码代码,其中它会有不在主线程的错误,因为它的包大概是一年前我觉得可能是ijkplayer还没有解决这个问题 我确定不是自身问题。这个是ijkplayer自身的问题 因为xcode更新了,他代码一年多没更新了,很多需要放到主线程的 他没有这么操作,当时可能没问题对于他来说,现在有问题了等待他解决吧。
解码代码:

 // 设置一些基本的参数
    IJKFFOptions *options = [IJKFFOptions optionsByDefault];
    // 让其支持硬编码,默认支持软编码
    [options setOptionValue:@"1" forKey:@"videotoolbox" ofCategory:kIJKFFOptionCategoryPlayer];
    // 初始化
    IJKFFMoviePlayerController *moviePlayer = [[IJKFFMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:liveStr] withOptions:options];
    if (self.authorModel.push == 1) { //手机拍摄
        moviePlayer.view.frame = CGRectMake(0, 120, self.view.frame.size.width, self.view.frame.size.width * 3/4);
    }else {
        moviePlayer.view.frame = self.view.bounds;
    }
    [self.view insertSubview:moviePlayer.view atIndex:0];
    // 开始播放
    [moviePlayer prepareToPlay];
    self.moviePlayer = moviePlayer;

最后发现我们做一个直播的项目 其实就是使用LFLiveKit和ijkplayer,实现一个推流和解视频组装视频流,但是我们学知识不能满足怎么实现就得了 我们需要知道它的原理,如果我们没有学习GPUImage 美颜相机加一点功能也就不会了,如果不学习硬编码和软编码也就不知道个别参数代表的意思,如果以后修改也就不会了,如果别人问你yuv 、rtmp、hls、h264这些等等你可能就不知道了,这些内容我大概花了一个多月的时间研究出来的,如果有不对的希望大家指正,大家如果觉得我哪块胡说也可以骂我,因为毕竟自己错了嘛,我会好好接受你的建议并改正。其实视频编码解码这块远远不止于此,要想做的好你需要对视频的编码和解码很了解还有对底层的c语言很懂才行,这个过程可能需要花几年的时间。

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

推荐阅读更多精彩内容