ijkplayer源码阅读03-系统播放器

概述

ijkplayer 是Bilibili开发并开源的轻量级视频播放器,支持本地网络视频播放以及流媒体播放,支持iOS和Android平台。ijkplayer基于 FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。 FFmpeg 采用LGPL或GPL许可证,提供了录制、转换以及流化音视频的完整解决方案,包括了领先的音、视频编码库libavcodec等。这篇文章的主要目的是介绍ijkplayer中IJKMPMoviePlayerControllerIJKAVMoviePlayerController。之所以放在一起是因为它们的底层都是调用系统的播放器接口,因此源码相对IJKFFMoviePlayerController来说比较简单。

特性

platform | version | CPU| video-output|audio-output|hw-decoder
:---:|:---:|:---:|:---:|:---:
iOS | iOS 7.0+ | armv7, arm64, i386, x86_64|OpenGL ES 2.0|AudioQueue, AudioUnit|VideoToolbox (iOS 8+)
Android | API 9+ | ARMv7a, ARM64v8a, x86 |NativeWindow, OpenGL ES 2.0|AudioTrack, OpenSL ES|MediaCodec (API 16+, Android 4.1+)

播放效果

播放画面.png

IJKMPMoviePlayerController

IJKMPMoviePlayerController 继承自MPMoviePlayerController实现了IJKMediaPlayback协议。通过实现 IJKMediaPlayback 协议,虽然每个播放器的底层实现不同,但是可以提供一套统一的播放接口。MPMoviePlayerController支持本地视频和网络视频的播放,它实现了MPMediaPlayback协议,因此具备一般的播放器控制功能,例如播放、暂停、停止等。但是 MPMediaPlayerController自身并不是一个完整的视图控制器,如果要在UI中展示视频需要将view属性添加到界面中。

  • 初始化。URL可以是本地视频的URL,也可以是网络视频的URL。
- (id)initWithContentURL:(NSURL *)aUrl;
- (id)initWithContentURLString:(NSString *)aUrl;

// URL初始化
- (id)initWithContentURL:(NSURL *)aUrl
{
    self = [super initWithContentURL:aUrl];
    if (self) {
        self.scalingMode = MPMovieScalingModeAspectFit;
        self.shouldAutoplay = YES;

        _notificationManager = [[IJKNotificationManager alloc] init];
        [self IJK_installMovieNotificationObservers];

        [[IJKAudioKit sharedInstance] setupAudioSession];
        
        _bufferingProgress = -1;
    }
    return self;
}

// 路径初始化
- (id)initWithContentURLString:(NSString *)aUrl
{
    NSURL *url;
    // 判断是否为文件
    if ([aUrl rangeOfString:@"/"].location == 0) {
        //构建本地URL
        url = [NSURL fileURLWithPath:aUrl];
    }
    else {
        url = [NSURL URLWithString:aUrl];
    }
    
    self = [self initWithContentURL:url];
    if (self) {
        
    }
    return self;
}
  • 相关方法。方法更多的是对MPMoviePlayerController的封装。
- (BOOL)isPlaying
{
    switch (self.playbackState) {
        case MPMoviePlaybackStatePlaying:
            return YES;
        default:
            return NO;
    }
}

- (void)shutdown
{
    // do nothing
}

-(int64_t)numberOfBytesTransferred
{
    NSArray *events = self.accessLog.events;
    if (events.count>0) {
        MPMovieAccessLogEvent *currentEvent = [events objectAtIndex:events.count -1];
        return currentEvent.numberOfBytesTransferred;
    }
    return 0;
}

- (UIImage *)thumbnailImageAtCurrentTime
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    return [super thumbnailImageAtTime:self.currentPlaybackTime timeOption:MPMovieTimeOptionExact];
#pragma clang diagnostic pop
}
  • 相关通知
// 做好播放准备后
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification 
// 媒体播放完成或用户手动退出,具体完成原因可以通过通知userInfo中的key为IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey的对象获取
IJKMPMoviePlayerPlaybackDidFinishNotification
// 播放状态改变
IJKMPMoviePlayerPlaybackStateDidChangeNotification 
// 媒体网络加载状态改变
IJKMPMoviePlayerLoadStateDidChangeNotification
// 当媒体开始通过AirPlay播放或者结束AirPlay播放
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification 
// 获取了媒体的实际尺寸
IJKMPMovieNaturalSizeAvailableNotification 

IJKAVMoviePlayerController

IJKAVMoviePlayerController 是对 AVPlayer 的封装。IJKAVMoviePlayerController 相比 IJKMPMoviePlayerController 要复杂些,MPMoviePlayerController 提供的播放器具有高度的封装性,使得自定义播放器变的很难。如果需要自定义播放器样式的时候,一般使用 AVPlayer。AVPlayer 存在于 AVFoundtion 中,更接近于底层,也更加灵活。

  • 初始化。这里需要注意的是在初始化的时候并没有初始化 AVPlayer,只是初始化相关的实例变量。
- (id)initWithContentURL:(NSURL *)aUrl;
- (id)initWithContentURLString:(NSString *)aUrl;

// 根据URL初始化
- (id)initWithContentURL:(NSURL *)aUrl
{
    self = [super init];
    if (self != nil) {
        self.scalingMode = IJKMPMovieScalingModeAspectFit;
        self.shouldAutoplay = NO;

        _playUrl = aUrl;

         // 初始化播放视图
        _avView = [[IJKAVPlayerLayerView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        self.view = _avView;

        // TODO:
        [[IJKAudioKit sharedInstance] setupAudioSession];

        _isPrerolling           = NO;

        _isSeeking              = NO;
        _isError                = NO;
        _isCompleted            = NO;
        self.bufferingProgress  = 0;

        _playbackLikelyToKeeyUp = NO;
        _playbackBufferEmpty    = YES;
        _playbackBufferFull     = NO;

        _playbackRate           = 1.0f;
        _playbackVolume         = 1.0f;
        // init extra
        [self setScreenOn:YES];

        _notificationManager = [[IJKNotificationManager alloc] init];
    }
    return self;
}

// 根据路径初始化
- (id)initWithContentURLString:(NSString *)aUrl
{
    NSURL *url;
    if (aUrl == nil) {
        aUrl = @"";
    }
    if ([aUrl rangeOfString:@"/"].location == 0) {
        //本地
        url = [NSURL fileURLWithPath:aUrl];
    }
    else {
        url = [NSURL URLWithString:aUrl];
    }
    self = [self initWithContentURL:url];
    if (self != nil) {
        
    }
    return self;
}
  • 异步加载。由于多媒体文件一般比较大,获取或计算出Asset中的属性非常耗时,Apple对Asset的属性采用了懒惰加载模式。在创建AVAsset的时候,只生成一个实例,并不初始化属性。只有当第一次访问属性时,系统才会根据多媒体中的数据初始化这个属性。由于不用同时加载所有属性,耗时问题得到了一定缓解。但是属性加载在计算量比较大的时候仍旧可能会阻塞线程。为了解决这个问题,AVFoundation提供了AVAsynchronousKeyValueLoading协议,可以异步加载属性:
@interface AVMetadataItem (AVAsynchronousKeyValueLoading)

// 异步加载属性,通过keys传入要加载的key数组,在handler中做加载完成的操作。
- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError * _Nullable * _Nullable)outError NS_AVAILABLE(10_7, 4_2);

// 获得属性的加载状态,如果是AVKeyValueStatusLoaded状态,表示已经加载完成。
- (void)loadValuesAsynchronouslyForKeys:(NSArray<NSString *> *)keys completionHandler:(nullable void (^)(void))handler NS_AVAILABLE(10_7, 4_2);

@end
  • 相关方法。IJKAVMoviePlayerController的方法比较多,在这里主要关注IJKAVMoviePlayerController播放器从初始化到播放的整体流程:
    1、根据初始化的URL构建AVURLAsset对象;
    2、异步加载获取视频相关属性;
    3、加载完成后初始化AVPlayerItem,并监听它的相关属性;
    4、初始化AVPlayer,并监听它的相关属性;
    5、当状态为AVPlayerItemStatusReadyToPlay的时候发送相关通知,如果开启了自动播放则自动播放。如果没有开启自动播放,我们可以监听IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification这个通知,收到通知后再去播放。
// 预加载,异步加载相关属性
- (void)prepareToPlay
{
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:_playUrl options:nil];
    NSLog(@"%@", asset);
    
    NSArray *requestedKeys = @[@"playable"];
    
    _playAsset = asset;
    // 异步加载属性
    [asset loadValuesAsynchronouslyForKeys:requestedKeys
                         completionHandler:^{
                             dispatch_async( dispatch_get_main_queue(), ^{
                                 [self didPrepareToPlayAsset:asset withKeys:requestedKeys];
                                 [[NSNotificationCenter defaultCenter]
                                  postNotificationName:IJKMPMovieNaturalSizeAvailableNotification
                                  object:self];
                             });
                         }];
}

// 异步加载完后,监听相关通知、属性以及初始化AVPlayer
- (void)didPrepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys
{
    if (_isShutdown)
        return;
    
    /* Make sure that the value of each key has loaded successfully. */
    for (NSString *thisKey in requestedKeys)
    {
        NSError *error = nil;
        AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error];
        if (keyStatus == AVKeyValueStatusFailed)
        {
            [self assetFailedToPrepareForPlayback:error];
            return;
        } else if (keyStatus == AVKeyValueStatusCancelled) {
            // TODO [AVAsset cancelLoading]
            error = [self createErrorWithCode:kEC_PlayerItemCancelled
                                  description:@"player item cancelled"
                                       reason:nil];
            [self assetFailedToPrepareForPlayback:error];
            return;
        }
    }
    
    /* Use the AVAsset playable property to detect whether the asset can be played. */
    if (!asset.playable)
    {
        NSError *assetCannotBePlayedError = [NSError errorWithDomain:@"AVMoviePlayer"
                                                                code:0
                                                            userInfo:nil];
        
        [self assetFailedToPrepareForPlayback:assetCannotBePlayedError];
        return;
    }
    
    /* At this point we're ready to set up for playback of the asset. */
    
    /* Stop observing our prior AVPlayerItem, if we have one. */
    [_playerItemKVO safelyRemoveAllObservers];
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:nil
                                                  object:_playerItem];
    
    /* Create a new instance of AVPlayerItem from the now successfully loaded AVAsset. */
    _playerItem = [AVPlayerItem playerItemWithAsset:asset];
    _playerItemKVO = [[IJKKVOController alloc] initWithTarget:_playerItem];
    [self registerApplicationObservers];
    /* Observe the player item "status" key to determine when it is ready to play. */
    // 监听AVPlayer的状态,比较重要
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:@"status"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayerItem_state];
    
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:@"loadedTimeRanges"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayerItem_loadedTimeRanges];
    
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:@"playbackLikelyToKeepUp"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayerItem_playbackLikelyToKeepUp];
    
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:@"playbackBufferEmpty"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayerItem_playbackBufferEmpty];
    
    [_playerItemKVO safelyAddObserver:self
                           forKeyPath:@"playbackBufferFull"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayerItem_playbackBufferFull];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerItemDidReachEnd:)
                                                 name:AVPlayerItemDidPlayToEndTimeNotification
                                               object:_playerItem];
    
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerItemFailedToPlayToEndTime:)
                                                 name:AVPlayerItemFailedToPlayToEndTimeNotification
                                               object:_playerItem];
    
    _isCompleted = NO;
    
    /* Create new player, if we don't already have one. */
    if (!_player)
    {
        /* Get a new AVPlayer initialized to play the specified player item. */
        _player = [AVPlayer playerWithPlayerItem:_playerItem];
        _playerKVO = [[IJKKVOController alloc] initWithTarget:_player];
        
        NSLog(@"%@", _player);
        
        /* Observe the AVPlayer "currentItem" property to find out when any
         AVPlayer replaceCurrentItemWithPlayerItem: replacement will/did
         occur.*/
        [_playerKVO safelyAddObserver:self
                           forKeyPath:@"currentItem"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayer_currentItem];
        
        /* Observe the AVPlayer "rate" property to update the scrubber control. */
        [_playerKVO safelyAddObserver:self
                           forKeyPath:@"rate"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayer_rate];
        
        [_playerKVO safelyAddObserver:self
                           forKeyPath:@"airPlayVideoActive"
                              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                              context:KVO_AVPlayer_airplay];
    }
    
    /* Make our new AVPlayerItem the AVPlayer's current item. */
    if (_player.currentItem != _playerItem)
    {
        /* Replace the player item with a new player item. The item replacement occurs
         asynchronously; observe the currentItem property to find out when the
         replacement will/did occur
         
         If needed, configure player item here (example: adding outputs, setting text style rules,
         selecting media options) before associating it with a player
         */
        [_player replaceCurrentItemWithPlayerItem:_playerItem];
        
        // TODO: notify state change
    }
    
    // TODO: set time to 0;
}

// 监听到相关状态改变的时候做进一步处理,并且发送相关通知
- (void)observeValueForKeyPath:(NSString*)path
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context
{
    if (_isShutdown)
        return;
    
    if (context == KVO_AVPlayerItem_state)
    {
        /* AVPlayerItem "status" property value observer. */
        AVPlayerItemStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
        switch (status)
        {
            case AVPlayerItemStatusUnknown:
            {
                /* Indicates that the status of the player is not yet known because
                 it has not tried to load new media resources for playback */
            }
                break;

             // 准备播放
            case AVPlayerItemStatusReadyToPlay:
            {
                /* Once the AVPlayerItem becomes ready to play, i.e.
                 [playerItem status] == AVPlayerItemStatusReadyToPlay,
                 its duration can be fetched from the item. */
                dispatch_once(&_readyToPlayToken, ^{
                    [_avView setPlayer:_player];
                    
                    self.isPreparedToPlay = YES;
                    AVPlayerItem *playerItem = (AVPlayerItem *)object;
                    NSTimeInterval duration = CMTimeGetSeconds(playerItem.duration);
                    if (duration <= 0)
                        self.duration = 0.0f;
                    else
                        self.duration = duration;
                    
                    [[NSNotificationCenter defaultCenter]
                     postNotificationName:IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification
                     object:self];
                    
                    // 如果自动播放,且应用程序为激活状态则自动播放
                    if (_shouldAutoplay && (!_pauseInBackground || [UIApplication sharedApplication].applicationState == UIApplicationStateActive))
                        [_player play];
                });
            }
                break;
            // 播放准备失败
            case AVPlayerItemStatusFailed:
            {
                AVPlayerItem *playerItem = (AVPlayerItem *)object;
                [self assetFailedToPrepareForPlayback:playerItem.error];
            }
                break;
        }
        
        [self didPlaybackStateChange];
        [self didLoadStateChange];
    }
    else if (context == KVO_AVPlayerItem_loadedTimeRanges)
    {
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        if (_player != nil && playerItem.status == AVPlayerItemStatusReadyToPlay) {
            NSArray *timeRangeArray = playerItem.loadedTimeRanges;
            CMTime currentTime = [_player currentTime];
            
            BOOL foundRange = NO;
            CMTimeRange aTimeRange = {0};
            
            if (timeRangeArray.count) {
                aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];
                if(CMTimeRangeContainsTime(aTimeRange, currentTime)) {
                    foundRange = YES;
                }
            }
            
            if (foundRange) {
                CMTime maxTime = CMTimeRangeGetEnd(aTimeRange);
                NSTimeInterval playableDuration = CMTimeGetSeconds(maxTime);
                if (playableDuration > 0) {
                    self.playableDuration = playableDuration;
                    [self didPlayableDurationUpdate];
                }
            }
        }
        else
        {
            self.playableDuration = 0;
        }
    }
    else if (context == KVO_AVPlayerItem_playbackLikelyToKeepUp) {
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        NSLog(@"KVO_AVPlayerItem_playbackLikelyToKeepUp: %@\n", playerItem.isPlaybackLikelyToKeepUp ? @"YES" : @"NO");
        [self fetchLoadStateFromItem:playerItem];
        [self didLoadStateChange];
    }
    else if (context == KVO_AVPlayerItem_playbackBufferEmpty) {
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        BOOL isPlaybackBufferEmpty = playerItem.isPlaybackBufferEmpty;
        NSLog(@"KVO_AVPlayerItem_playbackBufferEmpty: %@\n", isPlaybackBufferEmpty ? @"YES" : @"NO");
        if (isPlaybackBufferEmpty)
            _isPrerolling = YES;
        [self fetchLoadStateFromItem:playerItem];
        [self didLoadStateChange];
    }
    else if (context == KVO_AVPlayerItem_playbackBufferFull) {
        AVPlayerItem *playerItem = (AVPlayerItem *)object;
        NSLog(@"KVO_AVPlayerItem_playbackBufferFull: %@\n", playerItem.isPlaybackBufferFull ? @"YES" : @"NO");
        [self fetchLoadStateFromItem:playerItem];
        [self didLoadStateChange];
    }
    else if (context == KVO_AVPlayer_rate)
    {
        if (_player != nil && !isFloatZero(_player.rate))
            _isPrerolling = NO;
        /* AVPlayer "rate" property value observer. */
        [self didPlaybackStateChange];
        [self didLoadStateChange];
    }
    else if (context == KVO_AVPlayer_currentItem)
    {
        _isPrerolling = NO;
        /* AVPlayer "currentItem" property observer.
         Called when the AVPlayer replaceCurrentItemWithPlayerItem:
         replacement will/did occur. */
        AVPlayerItem *newPlayerItem = [change objectForKey:NSKeyValueChangeNewKey];
        
        /* Is the new player item null? */
        if (newPlayerItem == (id)[NSNull null])
        {
            NSError *error = [self createErrorWithCode:kEC_CurrentPlayerItemIsNil
                                           description:@"current player item is nil"
                                                reason:nil];
            [self assetFailedToPrepareForPlayback:error];
        }
        else /* Replacement of player currentItem has occurred */
        {
            [_avView setPlayer:_player];
            
            [self didPlaybackStateChange];
            [self didLoadStateChange];
        }
    }
    else if (context == KVO_AVPlayer_airplay)
    {
        [[NSNotificationCenter defaultCenter] postNotificationName:IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification object:nil userInfo:nil];
    }
    else
    {
        [super observeValueForKeyPath:path ofObject:object change:change context:context];
    }
}

// 播放音视频
- (void)play
{
    if (_isCompleted)
    {
        _isCompleted = NO;
        [_player seekToTime:kCMTimeZero];
    }
    
    [_player play];
}

// 生成截图
- (UIImage *)thumbnailImageAtCurrentTime
{
    AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:_playAsset];
    NSError *error = nil;
    CMTime time = CMTimeMakeWithSeconds(self.currentPlaybackTime, 1);
    CMTime actualTime;
    CGImageRef cgImage = [imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error];
    UIImage *image = [UIImage imageWithCGImage:cgImage];
    return image;
}

// 定位到新的播放时间
- (void)setCurrentPlaybackTime:(NSTimeInterval)aCurrentPlaybackTime
{
    if (!_player)
        return;

    _seekingTime = aCurrentPlaybackTime;
    _isSeeking = YES;
    _bufferingProgress = 0;
    [self didPlaybackStateChange];
    [self didLoadStateChange];
    if (_isPrerolling) {
        [_player pause];
    }

    [_player seekToTime:CMTimeMakeWithSeconds(aCurrentPlaybackTime, NSEC_PER_SEC)
      completionHandler:^(BOOL finished) {
          dispatch_async(dispatch_get_main_queue(), ^{
              _isSeeking = NO;
              if (_isPrerolling) {
                  [_player play];
              }
              [self didPlaybackStateChange];
              [self didLoadStateChange];
          });
      }];
}
  • 相关通知
// 做好播放准备后
IJKMPMediaPlaybackIsPreparedToPlayDidChangeNotification 
// 媒体播放完成或用户手动退出,具体完成原因可以通过通知userInfo中的key为IJKMPMoviePlayerPlaybackDidFinishReasonUserInfoKey的对象获取
IJKMPMoviePlayerPlaybackDidFinishNotification
// 播放状态改变
IJKMPMoviePlayerPlaybackStateDidChangeNotification 
// 媒体网络加载状态改变
IJKMPMoviePlayerLoadStateDidChangeNotification
// 当媒体开始通过AirPlay播放或者结束AirPlay播放
IJKMPMoviePlayerIsAirPlayVideoActiveDidChangeNotification 
// 获取了媒体的实际尺寸
IJKMPMovieNaturalSizeAvailableNotification 
  • 播放器使用
- (void)setupMPPlayer
{
    _mpPlayer = [[IJKMPMoviePlayerController alloc] initWithContentURLString:[[NSBundle mainBundle] pathForResource:@"1" ofType:@"mp4"]];
    _mpPlayer.scalingMode = IJKMPMovieScalingModeAspectFit;
    _mpPlayer.view.frame = self.view.bounds;
    [self.view addSubview:_mpPlayer.view];
    
    [_mpPlayer prepareToPlay];
}

- (void)setupAVPlayer
{
    _avPlayer = [[IJKAVMoviePlayerController alloc] initWithContentURLString:[[NSBundle mainBundle] pathForResource:@"1" ofType:@"mp4"]];
    [self.view addSubview:_avPlayer.view];
    
    [_avPlayer setShouldAutoplay:YES];
    [_avPlayer prepareToPlay];
}

总结

ijkplayer 中的 IJKMPMoviePlayerController 底层由 MPMoviePlayerController 实现,由于它具有高度的封装性。因此,二次封装的时候比较简单,可定制化程度低。IJKAVMoviePlayerController 底层通过 AVPlayer 实现,更加灵活,可定程度高,二次封装相对比较困难。如果希望了解如何定制 AVPlayer,读一读IJKAVMoviePlayerController 的源码是个不错的选择。

Demo地址 : https://github.com/QinminiOS/ijkplayer

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

推荐阅读更多精彩内容