AVFoundation开发秘籍笔记-11创建视频过渡效果

一、常用的类

1、AVVideoComposition

对两个或多个视频轨道组合在一起的方法给出了总体描述。由一组时间范围和描述组合行为的介绍内容组成,这些信息出现在组合资源内的任意时间点。

除了包含描述输入视频层组合的信息之外,还提供了配置视频组合的渲染尺寸、缩放和帧时长等属性。视频组合配置确定了委托对象处理时AVComposition的呈现方式。这里的委托对象比如AVPlayer或AVAssetImageGenerator。

AVVideoComposition并不是AVComposition的子类,没有直接关联。在视频播放、淡出或处理时会用AVVideoComposition来控制资源视频轨道的视频组合行为。

2、AVVideoCompositionInstruction

AVVideoComposition是由一组AVVideoCompositionInstruction对象格式定义的指令组成的。这个对象所提供的最关键的一段数据是组合对象时间轴内的时间范围信息,这一时间范围是在某一组合形式出现时的时间范文,要执行的组合特质是通过其layerInstructions集合定义的。

3、AVVideoCompositionLayerInstruction

用于定义对给定视频轨道应用的模糊、变形和裁剪效果。

它提供了一些方法用于在特定的时间点火灾一个时间范围内对这些值进修修改。在一段时间内对这些值应用渐变操作可以让开发者创建出动态的过渡效果,比如溶解和渐淡效果。

与所有AVFoundation媒体编辑类一样,视频组合API具有不可变和可变两种形式。不可变超类形式使用于客户端对象,比如AVAssetExportSession,不过当创建自己的视频组合应用程序时,应该使用不可变子类。

AVVideoComposition并不直接和AVComposition相关。相反,这些对象和雷系AVPlayerItem的客户端相关联,在播放组合或进行其他处理时使用这些对象。

不将AVComposition与输出行为强耦合,可以再播放、导出或处理视频时更灵活地确定如何使用这些行为。

二、概念理解

1、部署视频布局

要在剪辑间添加过过渡,首先需要将两个轨道间的视频片段重新部署。大多数情况下两个轨道就足够,为满足一些特殊需求加入更多轨道也是可以的,同时添加过多的轨道会对性能产生负面影响。

// 创建新的可变组合,添加两个AVMediaTypeVideo类型的组合轨道
AVMutableComposition *composition = [AVMutableComposition composition];
AVMutableCompositionTrack *trackA = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
AVMutableCompositionTrack *trackB = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
    
// 将组合轨道添加到一个NSArray中备用。
NSArray *videoTracks = @[trackA,trackB];
    

安排好所需轨道之后,需要错开部署两个轨道的视频布局(A-B交错模式)。当视频按照这种方式排列后,视频片段前面或后面的空间都会添加一个空片段。都是普通的AVCompositionTrackSegment实例,与视频一样,不过不包含任何媒体。


// 视频剪辑

NSArray *videoAssets ;
    
CMTime cursorTime = kCMTimeZero;
    
for (NSUInteger i = 0; i < videoAssets.count;  i ++) {
    //通过i%2来计算A-B模式下目标轨道的索引号
    NSUInteger trackIndex = i % 2;
    AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
    AVAsset *asset = videoAssets[i];
    AVAssetTrack *assetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
    CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
    // 将视频轨道从当前资源中提取出来,插入到currentTrack中
    [currentTrack insertTimeRange:timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
    cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
    
}

这个时候已经采用交错方式排列这些轨道,但是每个片段都还紧接着上一篇段的片尾,两个可见剪辑间没有任何空间,还无法将他们组合在一起。

2、定义重叠区域

要在两个片段中应用视频过度,需要根据期望的过度持续时长来确定片段的重叠情况。

设置添加一个重叠时间来处理。

NSArray *videoAssets ;
    
CMTime cursorTime = kCMTimeZero;
// 定义一个2秒的CMTIme 作为期望的过渡持续时长。
CMTime transitionDuration = CMTimeMake(2, 1);
for (NSUInteger i = 0; i < videoAssets.count;  i ++) {
    //通过i%2来计算A-B模式下目标轨道的索引号
    NSUInteger trackIndex = i % 2;
    AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
    AVAsset *asset = videoAssets[i];
    AVAssetTrack *assetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
    CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
    // 将视频轨道从当前资源中提取出来,插入到currentTrack中
    [currentTrack insertTimeRange:timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
    cursorTime = CMTimeAdd(cursorTime, timeRange.duration);
    // 减去过渡时长,过渡时长适度偏移下一个插入点
    cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
    
}

此时的组合可以播放,但是视频轨道怒又内在的z索引行为,第一个视频轨道和第二个之前,第二个在第三个之前,一次类推。

在这种情况下播放组合,会发现第一、第二段视频什么都没有,最后会看到最前面轨道中所包含的第三个片段。在看到第二个轨道的内容前,需要定义时间范围并向组合方法解释这两个轨道应该如何组合。

创建重叠区域时,已经将视频时间轴减少了(videoCount-1)*transitionDuration的大小。如果组合中有额外的轨道,则需要将它们的时长相应的缩短以满足新的视频时间轴。

3、计算通过和过渡的时间范围

AVVideoComposition由一组AVVideoCompositionInstruction对象组成。其中最重要的数据是时间范围,用来表示某种出现的组合方式持续的时长。在开始创建AVVideoCompositionInstruction实例前,首先需要为组合对象计算一系列时间范围。

需要计算两个类型的时间范围。第一个通常被认为是通过(pass-through)时间范围,在这个时间范围内希望一个轨道的所有帧序列都在不与其他轨道进行混合的情况下通过某一区域。

第二个时间范围类型是过渡(transition)时间范围。定义了在组合中视频片段重叠的区域,并在时间轴上标记处应用过渡效果的区域。

可以通过多种方式计算这些时间范围,一种很好的通用方法。

NSArray *videoAssets ;
    
CMTime cursorTime = kCMTimeZero;
// 定义一个2秒的CMTIme 作为期望的过渡持续时长。
CMTime transitionDuration = CMTimeMake(2, 1);
    
NSMutableArray *passThroughTiemRanges = [NSMutableArray array];
NSMutableArray *transitionTimeRanges = [NSMutableArray array];
    
NSUInteger videoCount = [videoAssets count];
    
for (NSUInteger i = 0; i < videoCount;  i ++) {
    AVAsset *asset = videoAssets[i];
    
    CMTimeRange timeRange = CMTimeRangeMake(cursorTime, asset.duration);
    
    if (i > 0) {
        timeRange.start = CMTimeAdd(timeRange.start, transitionDuration);
        timeRange.duration = CMTimeSubtract(timeRange.duration, transitionDuration);
    }
    
    if (i + 1 < videoCount) {
        timeRange.duration = CMTimeSubtract(timeRange.duration, transitionDuration);
    }
    
    [passThroughTiemRanges addObject:[NSValue valueWithCMTimeRange:timeRange]];
    
    cursorTime = CMTimeAdd(cursorTime, asset.duration);
    cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
    
    if (i + 1 < videoCount) {
        timeRange = CMTimeRangeMake(cursorTime, transitionDuration);
        NSValue *timeRangeValue = [NSValue valueWithCMTimeRange:timeRange];
        [transitionTimeRanges addObject:timeRangeValue];
    }
}

创建所需要的通过和过渡时间范围被认为是整个处理过程中最重要的一步操作,他并不是最难解决的问题,不过容易出错。进行这一步处理的时候,有两个关键点:

  • 1、计算的时间范围必须没有任何空隙或重叠。必须是紧接上一个片段之后按需排列的最新时间范围。
  • 2、计算必须考虑组合对象持续时间。如果组合中还包含额外的轨道,就需要使它们遵循目前的视频时间轴,或者根据他们的持续时长扩展最终的时间范围。

如果没有注意到这两点,组合对象仍可播放,但视频内容不会被渲染,只会显示黑屏。

Apple Developer Center中AVCompositionDebugViewer可以帮助呈现组合。

4、创建组合和层指令

创建AVVideoCompositionInstructionAVVideoCompositionLayerInstruction示例,提供视频组合方法所执行的指令。

NSMutableArray *compositionInstructions = [NSMutableArray array];
//遍历所有的通过时间范围
for (NSUInteger i = 0; i > passThroughTiemRanges.count; i ++) {
    // 循环在连个需要创建所需指令的视频轨道间前后切换。
    NSInteger trackIndex = i % 2;
    AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
    
    // 创建AVMutableVideoCompositionInstruction实例,设置当前通过时间范围作为timeRange
    AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    instruction.timeRange = [passThroughTiemRanges[i] CMTimeRangeValue];
    
    // 为活动组合创建一个新的AVMutableVideoCompositionLayerInstruction
    AVMutableVideoCompositionLayerInstruction *layerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:currentTrack];
    // 添加到数组
    instruction.layerInstructions = @[layerInstruction];
    // 设置为组合指令的layerInstructions属性。
    [compositionInstructions addObject:instruction];
    //组合的通过时间范围区域只需要一个与要呈现视频帧的轨道相关的单独层指令。
    
    if (i < transitionTimeRanges.count) {
        //要创建过渡时间的指令,需要得到前一个轨道(过渡前的轨道)的引用和后一个轨道(过渡后的轨道)的引用
        //确保轨道的引用始终顺序正确。
        AVCompositionTrack *foregroundTrack = videoTracks[trackIndex];
        AVCompositionTrack *backgroundTrack = videoTracks[1-trackIndex];
        
        //创建一个AVMutableVideoCompositionInstruction实例
        AVMutableVideoCompositionInstruction *instruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
        //设置当前过渡时间范围为它的timeRange属性。
        CMTimeRange timeRange = [transitionTimeRanges[i] CMTimeRangeValue];
        instruction.timeRange = timeRange;
        
        // 为每个轨道创建一个AVMutableVideoCompositionLayerInstruction实例
        // 在这些层指令上定义从一个场景到另一个场景的过渡效果
        AVMutableVideoCompositionLayerInstruction *fromLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:foregroundTrack];       
        AVMutableVideoCompositionLayerInstruction *toLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:backgroundTrack];
        
        // 将两个层指令添加到数组,并设置他们作为当前组合指令的layerInstructions属性
        // 这一数组中的元素进行排序非常重要,因为它定义了组合输出中视频图层的z轴顺序。
        instruction.layerInstructions = @[fromLayerInstruction,toLayerInstruction];        
        [compositionInstructions addObject:instruction];   
    }
}

5、创建和配置AVVideoComposition

AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
videoComposition.instructions = compositionInstructions;
// 定义组合应该被渲染尺寸。对应于组合中视频原始大小720p-1280*720,1080p-1920*1080
videoComposition.renderSize = CGSizeMake(1280.f, 720.f);
// 设置有效帧率
videoComposition.frameDuration = CMTimeMake(1, 30);
// 视频组合应用的缩放
videoComposition.renderScale = 1.0f;

6、创建视频组合的捷径

// 创建一个基础的AVMutableVideoComposition
AVMutableVideoComposition *co = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:composition];

包含基础配置:

  • instructions 属性包含一组完整的基于组合视频轨道(以及其中包含的片段空间布局)的组合和层指令
  • renderSize 属性被设置为AVComposition对象的naturalSize,如果没有设置,则使用能够满足组合视频轨道中最大视频纬度的尺寸值。
  • frameDuration 设置为组合视频轨道中最大nominalFrameRate的值。如果所有轨道的nominalFrameRate值都为0,则frameDuration设置层默认值1/30秒(30FPS)。
  • renderScale 始终设置为1.0

三、过渡效果

1、溶解

[fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:timeRange];
// 对fromLayer对象设置一个模糊渐变

2、推出

 //定义个对于输入视频层的变换。
//CGAffineTransForm可以修改层的转化、旋转、缩放。对层应用一个渐变的变化可以衍生出很多效果。
CGAffineTransform identityTransform = CGAffineTransformIdentity;
CGFloat videoWidth = videoCompostion.renderSize.width;
    
// 这里通过转化(translation)转换,修改图层的x,y坐标。
CGAffineTransform fromDestTransform = CGAffineTransformMakeTranslation(-videoWidth, 0);
CGAffineTransform toStartTransform = CGAffineTransformMakeTranslation(videoWidth, 0);
// 设置fromLayer的渐变效果,初始变换设置为identityTransform,终点变换为fromDestTransform
[fromLayer setTransformRampFromStartTransform:identityTransform toEndTransform:fromDestTransform timeRange:timeRange];
[toLayer setTransformRampFromStartTransform:toStartTransform toEndTransform:identityTransform timeRange:timeRange];

3、擦除


CGFloat videoWidth = videoCompostion.renderSize.width;
CGFloat videoHeight = videoCompostion.renderSize.height;
    
CGRect startRect = CGRectMake(0, 0, videoWidth, videoHeight);
CGRect endRect = CGRectMake(0, videoHeight, videoWidth, 0);
    
[fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];

完整代码.m

#import "THTransitionCompositionBuilder.h"
#import "THVideoItem.h"
#import "THAudioItem.h"
#import "THVolumeAutomation.h"
#import "THTransitionComposition.h"
#import "THTransitionInstructions.h"
#import "THFunctions.h"

@interface THTransitionCompositionBuilder ()
@property (strong, nonatomic) THTimeline *timeline;
@property (strong, nonatomic) AVMutableComposition *composition;
@property (weak, nonatomic) AVMutableCompositionTrack *musicTrack;
@end

@implementation THTransitionCompositionBuilder

- (id)initWithTimeline:(THTimeline *)timeline {
    self = [super init];
    if (self) {
        _timeline = timeline;
    }
    return self;
}

- (id <THComposition>)buildComposition {

    self.composition = [AVMutableComposition composition];

    [self buildCompositionTracks];

    AVVideoComposition *videoComposition = [self buildVideoComposition];

    AVAudioMix *audioMix = [self buildAudioMix];

    return [[THTransitionComposition alloc] initWithComposition:self.composition
                                               videoComposition:videoComposition
                                                       audioMix:audioMix];
}

- (void)buildCompositionTracks {
    
    CMPersistentTrackID trackID = kCMPersistentTrackID_Invalid;
    
    // 创建两个AVMediaTypeVideo类型的轨道,提供所需的A-B轨道排列
    AVMutableCompositionTrack *compositionTrackA = [self.composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:trackID];
    AVMutableCompositionTrack *compositionTrackB = [self.composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:trackID];
    
    NSArray *videoTracks = @[compositionTrackA,compositionTrackB];
    
    CMTime cursorTime = kCMTimeZero;
    CMTime transitionDuration = kCMTimeZero;
    
    if (!THIsEmpty(self.timeline.transitions)) {
        //判断transitions是否被填充,即视频过渡是否被设置为激活。
        //如果激活过渡效果,设置常量值
        transitionDuration = THDefaultTransitionDuration;
    }
    NSArray *videos = self.timeline.videos;
    
    for (NSUInteger i = 0; i < videos.count; i ++) {
        NSUInteger trackIndex = i%2;
        THVideoItem *item = videos[i];
        // 确定要插入的目标轨道
        AVMutableCompositionTrack *currentTrack = videoTracks[trackIndex];
        AVAssetTrack *assetTrack = [[item.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
        
        [currentTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];
        cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
        cursorTime = CMTimeSubtract(cursorTime, transitionDuration);
        
    }
    
    //添加画外音和音乐轨道
    [self addCompositionTrackOfType:AVMediaTypeAudio withMediaItems:self.timeline.voiceOvers];
    
    NSArray *musicItems = self.timeline.musicItems;
    self.musicTrack = [self addCompositionTrackOfType:AVMediaTypeVideo withMediaItems:musicItems];
}

- (AVVideoComposition *)buildVideoComposition {

    // 创建一个新的AVVideoComposition实例,自动创建所需的组合对象和层指令
    // 设置renderSize,renderScale和frameDuration属性为相应的值。
    AVVideoComposition *videoCompostion = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:self.composition];
    
    // 从AVVideoComposition中提取相关的层指令,可以应用视频过渡效果。
    // 返回一个THTransitionInstructions对象数组
    NSArray *transitionInstructions = [self transitionInstructionsInVideoComposition:videoCompostion];
    
    for (THTransitionInstructions *instructions in transitionInstructions) {
        CMTimeRange timeRange = instructions.compositionInstruction.timeRange;
        
        AVMutableVideoCompositionLayerInstruction *fromLayer = instructions.fromLayerInstruction;
        
        AVMutableVideoCompositionLayerInstruction *toLayer = instructions.toLayerInstruction;
        
        THVideoTransitionType type = instructions.transition.type;
        
        if (type == THVideoTransitionTypeDissolve) { // 溶解过渡效果
            [fromLayer setOpacityRampFromStartOpacity:1.0 toEndOpacity:0.0 timeRange:timeRange];
            // 对fromLayer对象设置一个模糊渐变
        } else if (type == THVideoTransitionTypePush) { //推入过渡效果
            //定义个对于输入视频层的变换。
            //CGAffineTransForm可以修改层的转化、旋转、缩放。对层应用一个渐变的变化可以衍生出很多效果。
            CGAffineTransform identityTransform = CGAffineTransformIdentity;
            CGFloat videoWidth = videoCompostion.renderSize.width;
            
            // 这里通过转化(translation)转换,修改图层的x,y坐标。
            CGAffineTransform fromDestTransform = CGAffineTransformMakeTranslation(-videoWidth, 0);
            CGAffineTransform toStartTransform = CGAffineTransformMakeTranslation(videoWidth, 0);
            // 设置fromLayer的渐变效果,初始变换设置为identityTransform,终点变换为fromDestTransform
            [fromLayer setTransformRampFromStartTransform:identityTransform toEndTransform:fromDestTransform timeRange:timeRange];
            [toLayer setTransformRampFromStartTransform:toStartTransform toEndTransform:identityTransform timeRange:timeRange];
        } else if (type == THVideoTransitionTypeWipe) { //擦除过渡效果
            CGFloat videoWidth = videoCompostion.renderSize.width;
            CGFloat videoHeight = videoCompostion.renderSize.height;
            
            CGRect startRect = CGRectMake(0, 0, videoWidth, videoHeight);
            CGRect endRect = CGRectMake(0, videoHeight, videoWidth, 0);
            
            [fromLayer setCropRectangleRampFromStartCropRectangle:startRect toEndCropRectangle:endRect timeRange:timeRange];
        }
        // 配合组合指令的layerInstructions,按显示的顺序传递指令,确保正确应用视频过渡效果
        instructions.compositionInstruction.layerInstructions = @[fromLayer,toLayer];
    }
    
    return videoCompostion;
}

// Extract the composition and layer instructions out of the
// prebuilt AVVideoComposition. Make the association between the instructions
// and the THVideoTransition the user configured in the timeline.
- (NSArray *)transitionInstructionsInVideoComposition:(AVVideoComposition *)vc {

    NSMutableArray *transitionInstructions = [NSMutableArray array];
    
    int layerInstructionIndex = 1;
    
    NSArray *compositionInstructions = vc.instructions;
    
    // 遍历从AVVideoComposition中得到的AVVideoCompositionInstruction对象
    for (AVMutableVideoCompositionInstruction *vci in compositionInstructions) {
        
        //只关心包含两个层指令的组合指令,表示这个指令定义了组合中的过渡区域。
        if (vci.layerInstructions.count == 2) {
            THTransitionInstructions *instructions = [[THTransitionInstructions alloc] init];
            
            instructions.compositionInstruction = vci;
            // 自动创建的层指令通常保存于layerInstructions数组
            //第一个轨道的层指令作为数组的第一个元素,接下来是第二个轨道的层指令
            //
            instructions.fromLayerInstruction = [vci.layerInstructions[1-layerInstructionIndex] mutableCopy];
            instructions.toLayerInstruction = [vci.layerInstructions[layerInstructionIndex] mutableCopy];
            
            [transitionInstructions addObject:instructions];
            layerInstructionIndex = layerInstructionIndex == 1 ? 0:1;
        }
    }
    
    NSArray *transitions = self.timeline.transitions;
    
    if (THIsEmpty(transitions)) {
        //禁用了过渡
        return transitionInstructions;
    }
    
    // 如果过渡已启用,遍历transitionInstructions,并将用户选定的THVideoTransition对象和它进行关联。
    for (NSUInteger i = 0; i < transitionInstructions.count; i ++) {
        THTransitionInstructions *tis = transitionInstructions[i];
        tis.transition = self.timeline.transitions[i];
    }
    
    
    return transitionInstructions;
}

- (AVMutableCompositionTrack *)addCompositionTrackOfType:(NSString *)mediaType
                                          withMediaItems:(NSArray *)mediaItems {

    AVMutableCompositionTrack *compositionTrack = nil;

    if (!THIsEmpty(mediaItems)) {
        compositionTrack =
            [self.composition addMutableTrackWithMediaType:mediaType
                                          preferredTrackID:kCMPersistentTrackID_Invalid];

        CMTime cursorTime = kCMTimeZero;

        for (THMediaItem *item in mediaItems) {

            if (CMTIME_COMPARE_INLINE(item.startTimeInTimeline, !=, kCMTimeInvalid)) {
                cursorTime = item.startTimeInTimeline;
            }

            AVAssetTrack *assetTrack = [[item.asset tracksWithMediaType:mediaType] firstObject];
            [compositionTrack insertTimeRange:item.timeRange ofTrack:assetTrack atTime:cursorTime error:nil];

            // Move cursor to next item time
            cursorTime = CMTimeAdd(cursorTime, item.timeRange.duration);
        }
    }

    return compositionTrack;
}

- (AVAudioMix *)buildAudioMix {
    NSArray *items = self.timeline.musicItems;
    // Only one allowed
    if (items.count == 1) {
        THAudioItem *item = self.timeline.musicItems[0];

        AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix];

        AVMutableAudioMixInputParameters *parameters =
            [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:self.musicTrack];

        for (THVolumeAutomation *automation in item.volumeAutomation) {
            [parameters setVolumeRampFromStartVolume:automation.startVolume
                                         toEndVolume:automation.endVolume
                                           timeRange:automation.timeRange];
        }
        audioMix.inputParameters = @[parameters];
        return audioMix;
    }
    return nil;
}

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 初入职场的我们,刚从安逸的校园环境出来,就不得不适应这个快节奏的社会。这个社会似乎在告诉我们,我们今后的日子或许就...
    爱思考的安妮阅读 2,376评论 3 2
  • 1、面朝大海春暖花开 带你玩转世界 2、三月桃花劫 只想和你在一起 3、远离城市的喧闹 慢生活时光 广告~书 1、...
    芬芳姐377阅读 141评论 0 0
  • 我发现了一个规律, 朋友圈经常晒美颜自拍和美食的菇凉,大多是单身的,为啥,因为谈恋爱的状态下一般很少晒幸福, 而单...
    朱剑潭阅读 562评论 0 4
  • Objective 你对今天学的记得什么? 人的大脑分三种: 思考脑:准确,速度慢!专一,像蓝牙 反射脑:自发无意...
    学霸教练李斌阅读 220评论 1 2