FFmpeg小白学习记录(二)视频流解码流程

视频解码流程

在对多媒体文件中的视频流解码前,我们先来了解以下流媒体数据的播放流程,可以根据这个流程梳理一下视频解码流程

视频解码流程

音视频播放的原理主要分为:解协议 -> 解封装 -> 解码 -> 音视频同步 -> 播放,不过如果播放文件是本地文件就不需要解协议这一步骤

其中对应数据格式转换流程为:多媒体文件 -> 流 -> 包 -> 帧

蓝色元素块代表具体的数据

紫色元素块代表数据格式

橙色元素块代表数据所对应的协议

白色元素块代表执行的操作

在 FFmpeg 中获取多媒体文件中视频流的数据具体流程如下图:


获取视频流

具体解码流程

接下来,我们根据上述的流程通过FFmpeg对视频流进行解码,具体代码如下:

extern"C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
#include <iostream>
using namespace std;

int main() {
    int ret = 0;
    //文件地址
    const char* filePath = "target.mp4";

    //声明所需的变量名
    AVFormatContext* fmtCtx = NULL;
    AVCodecContext* codecCtx = NULL;
    AVCodecParameters* avCodecPara = NULL;
    AVCodec* codec = NULL;

    //包
    AVPacket* pkt = NULL;
    //帧
    AVFrame* frame = NULL;

    do {
        //----------------- 创建AVFormatContext结构体 -------------------
        //内部存放着描述媒体文件或媒体流的构成和基本信息
        fmtCtx = avformat_alloc_context();
        //----------------- 打开本地文件 -------------------
        ret = avformat_open_input(&fmtCtx, filePath, NULL, NULL);
        if (ret) {
            printf("cannot open file\n");
            break;
        }
        //----------------- 获取多媒体文件信息 -------------------
        ret = avformat_find_stream_info(fmtCtx, NULL);
        if (ret < 0) {
            printf("Cannot find stream information\n");
            break;
        }

        //通过循环查找多媒体文件中包含的流信息,直到找到视频类型的流,并记录该索引值
        int videoIndex = -1;
        for (int i = 0; i < fmtCtx->nb_streams; i++) {
            if (fmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoIndex = i;
                break;
            }
        }

        //如果videoIndex为-1 说明没有找到视频流
        if (videoIndex == -1) {
            printf("cannot find video stream\n");
            break;
        }

        //打印流信息
        av_dump_format(fmtCtx, 0, filePath, 0);
        //----------------- 查找解码器 -------------------
        avCodecPara = fmtCtx->streams[videoIndex]->codecpar;
        AVCodec* codec = avcodec_find_decoder(avCodecPara->codec_id);
        if (codec == NULL) {
            printf("cannot open decoder\n");
            break;
        }
        //根据解码器参数来创建解码器上下文

        codecCtx = avcodec_alloc_context3(codec);
        ret = avcodec_parameters_to_context(codecCtx, avCodecPara);
        if (ret < 0) {
            printf("parameters to context fail\n");
            break;
        }

        //----------------- 打开解码器 -------------------
        ret = avcodec_open2(codecCtx, codec, NULL);
        if (ret < 0) {
            printf("cannot open decoder\n");
            break;
        }

        //----------------- 创建AVPacket和AVFrame结构体 ------------------- 
        pkt = av_packet_alloc();
        frame = av_frame_alloc();

        //----------------- 读取视频帧 ------------------- 
        int i = 0;      //记录视频帧数
        while (av_read_frame(fmtCtx, pkt) >= 0) {//读取的是一帧视频  数据存入AVPacket结构体中
            //是否对应视频流的帧
            if (pkt->stream_index == videoIndex) {
                //发送包数据去进行解析获得帧数据
                ret = avcodec_send_packet(codecCtx, pkt);
                if (ret == 0) {
                    //接收的帧不一定只有一个,可能为0个或多个
                    //比如:h264中存在B帧,会参考前帧和后帧数据得出图像数据
                    //即读到B帧时不会产出对应数据,直到后一个有效帧读取时才会有数据,此时就有2帧
                    while (avcodec_receive_frame(codecCtx, frame) == 0) {
                        //此处就可以获取到视频帧中的图像数据 -> frame.data
                        //可以通过openCV、openGL、SDL方式进行显示
                        //也可以保存到文件中(需要添加文件头)
                        i++;
                    }
                }
            }
            av_packet_unref(pkt);//重置pkt的内容
        }
        
        //此时缓存区中还存在数据,需要发送空包刷新
        ret = avcodec_send_packet(codecCtx, NULL);
        if (ret == 0) {
            while (avcodec_receive_frame(codecCtx, frame) == 0) {
                i++;
            }
        }
        
        printf("There are %d frames int total.\n", i);
    } while (0);

    //----------------- 释放所有指针 ------------------- 
    avcodec_close(codecCtx);
    avformat_close_input(&fmtCtx);
    av_packet_free(&pkt);
    av_frame_free(&frame);

    return 0;
}

输出结果:

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'target.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.48.100
  Duration: 00:03:10.36, start: 0.000000, bitrate: 773 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 1280x720, 442 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 325 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
There are 4759 frames int total.

4759 ÷ 25 + 0.0 = 190.36 ⇒ 03 :10. 36

在 FFmpeg 中将 JPG、PNG图片文件视为只有一帧的视频流,也可以使用上述解码流程读取图像数据

代码解析

结构体

上述代码中涉及的结构体有:AVFormatContextAVCodecParametersAVCodecContextAVCodecAVPacketAVFrame

其中AVFormatContext已经讲解过,此处就不再阐述

AVCodecParametersAVCodecContext

新的 FFmpeg 中 AVStream.codecpar(struct AVCodecParameter) 代替 AVStream.codec(struct AVCodecContext)AVCodecParameter 是由 AVCodecContext 分离出来的,AVCodecParameter中没有函数,里面存放着解码器所需的各种参数

AVCodecContext 结构体仍然是编解码时不可或缺的结构体

// 其中截取出部分较为重要的数据
typedef struct AVCodecParameters {
    enum AVMediaType codec_type;    //编解码器的类型(视频,音频...)
    enum AVCodecID codec_id;        //标示特定的编码器
    int bit_rate;                   //平均比特率
    
    int sample_rate;                //采样率(音频)
    int channels;                   //声道数(音频)
    uint64_t channel_layout;        //声道格式
    
    int width, height;              //宽和高(视频)    
    int format;                     //像素格式(视频)/采样格式(音频)
    ...
} AVCodecParameters;

typedef struct AVCodecContext {
    //在AVCodecParameters中的属性,AVCodecContext都有
    struct AVCodec *codec;          //采用的解码器AVCodec(H.264,MPEG2...)
    
    enum AVSampleFormat sample_fmt; //采样格式(音频)
    enum AVPixelFormat pix_fmt;     //像素格式(视频)
    ...
}AVCodecContext;

其中avcodec_parameters_to_context就是将AVCodecParameter的参数传给AVCodecContext

AVCodec

AVCodec解码器结构体,对应一个具体的解码器

// 其中截取出部分较为重要的数据
typedef struct AVCodec {
    const char *name;       //编解码器短名字(形如:"h264")
    const char *long_name;  //编解码器全称(形如:"H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10")
    enum AVMediaType type;  //媒体类型:视频、音频或字母
    enum AVCodecID id;      //标示特定的编码器
    
    const AVRational *supported_framerates; //支持的帧率(仅视频)
    const enum AVPixelFormat *pix_fmts;     //支持的像素格式(仅视频)
    
    const int *supported_samplerates;       //支持的采样率(仅音频)
    const enum AVSampleFormat *sample_fmts; //支持的采样格式(仅音频)
    const uint64_t *channel_layouts;        //支持的声道数(仅音频)
    ...
}AVCodec ;

AVPacket

AVPacket:存储解码前数据的结构体,即包

// 其中截取出部分较为重要的数据
typedef struct AVPacket {
    AVBufferRef *buf;   //管理data指向的数据
    uint8_t *data;      //压缩编码的数据
    int size;           //data的大小
    int64_t pts;        //显示时间戳
    int64_t dts;        //解码时间戳
    int stream_index;   //标识该AVPacket所属的视频/音频流
    ...
}AVPacket ;
  • AVPacket的内存管理

  • AVPacket本身并不包含压缩的数据,通过data指针引用数据的缓存空间

  • 可以多个AVPacket共享同一个数据缓存(AVBufferRefAVBuffer

av_read_frame(pFormatCtx, packet);  // 读取Packet
av_packet_ref(dst_pkt,packet); // dst_pkt 和 packet 共享同一个数据缓存空间,引用计数+1
av_packet_unref(dst_pkt); // 释放 pkt_pkt 引用的数据缓存空间,引用计数-1

AVFrame

AVFrame:存储解码后数据的结构体,即帧

// 其中截取出部分较为重要的数据
typedef struct AVFrame {
    uint8_t *data[AV_NUM_DATA_POINTERS];    //解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
    int linesize[AV_NUM_DATA_POINTERS];     //data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。
    int width, height;  //视频帧宽和高(1920x1080,1280x720...)
    int format;         //解码后原始数据类型(YUV420,YUV422,RGB24...)
    int key_frame;      //是否是关键帧
    enum AVPictureType pict_type;   //帧类型(I,B,P...)
    AVRational sample_aspect_ratio; //图像宽高比(16:9,4:3...)
    int64_t pts;        //显示时间戳
    int coded_picture_number;       //编码帧序号
    int display_picture_number;     //显示帧序号
    
    int nb_samples;     //音频采样数
    ...
}AVFrame ;
函数

avcodec_find_decoder

avcodec_find_decoder根据解码器ID查找到对应的解码器

AVCodec *avcodec_find_decoder(enum AVCodecID id);           //通过id查找解码器
AVCodec *avcodec_find_decoder_by_name(const char *name);    //通过解码器名字查找
/* 与解码器对应的就是编码器,也有相应的查找函数 */
AVCodec *avcodec_find_encoder(enum AVCodecID id);           //通过id查找编码器
AVCodec *avcodec_find_encoder_by_name(const char *name);    //通过编码器名字查找

参数:

  • enum AVCodecID id:解码器ID,可以从AVCodecParameters中获取

return:

返回一个AVCodec指针,如果没有找到就返回NULL


avcodec_alloc_context3

avcodec_alloc_context3会生成一个AVCodecContext并根据解码器给属性设置默认值

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);

参数:

  • const AVCodec *codec:解码器指针,会根据解码器分配私有数据并初始化默认值

return:

返回一个AVCodec指针,如果创建失败则会返回NULL

话说avcodec_alloc_context3函数名中的 3 是什么含义?


avcodec_parameters_to_context

avcodec_parameters_to_contextAVCodecParameters中的属性赋值给AVCodecContext

int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par){
    //将par中的属性赋值给codec
    codec->codec_type = par->codec_type;
    codec->codec_id   = par->codec_id;
    codec->codec_tag  = par->codec_tag;
    ...
}

参数:

  • AVCodecContext *codec:需要被赋值的AVCodecContext
  • const AVCodecParameters *par:提供属性值的AVCodecParameters

return:

返回数值 ≥ 0时代表成功,失败时会返回一个负值


avcodec_open2

avcodec_open2打开音频解码器或者视频解码器

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options){
    ...
    avctx->codec = codec;
    ...
}

参数:

  • AVCodecContext *avctx:已经初始化完毕的AVCodecContext
  • const AVCodec *codec:用于打开AVCodecContext中的解码器,之后AVCodecContext会使用该解码器进行解码
  • AVDictionary **options:指定各种参数,基本填NULL即可

return:

返回0表示成功,若失败则会返回一个负数


av_read_frame

av_read_frame获取音视频(编码)数据,即从流中获取一个AVPacket数据。将文件中存储的内容分割成包,并为每个调用返回一个包

int av_read_frame(AVFormatContext *s, AVPacket *pkt);

参数:

  • AVFormatContext *s:AVFormatContext结构体
  • AVPacket *pkt:通过data指针引用数据的缓存空间,本身不存储数据

return:

返回0表示成功,失败或读到了文件结尾则会返回一个负数

函数为什么是av_read_frame而不是av_read_packet,是早期 FFmpeg 设计时候没有包的概念,而是编码前的帧和编码后的帧,不容易区分。之后才产生包的概念,但出于编程习惯或向前兼容的原因,于是方法名就这样延续了下来


avcodec_send_packet

avcodec_send_packet用于向解码器发送一个包,让解码器进行解析

int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);

参数:

  • AVCodecContext *avctx:AVCodecContext结构体,必须使用avcodec_open2打开解码器
  • const AVPacket *avpkt:用于解析的数据包

return:

返回0表示成功,失败则返回负数的错误码,异常值说明:

  • AVERROR(EAGAIN):当前不接受输出,必须重新发送
  • AVERROR_EOF:解码器已经刷新,并且没有新的包可以发送
  • AVERROR(EINVAL):解码器没有打开,或者这是一个编码器
  • AVERRO(ENOMEN):无法添加包到内部队列

avcodec_receive_frame

avcodec_receive_frame获取解码后的音视频数据(音视频原始数据,如YUV和PCM)

int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);

参数:

  • AVCodecContext *avctx:AVCodecContext结构体
  • AVFrame *frame:用于接收解码后的音视频数据的帧

return:

返回0表示成功,其余情况表示失败,异常值说明:

  • AVERROR(EAGAIN):此状态下输出不可用,需要发送新的输入才能解析
  • AVERROR_EOF:解码器已经刷新,并且没有新的包可以发送
  • AVERROR(EINVAL):解码器没有打开,或者这是一个编码器

调用avcodec_receive_frame方法时不需要通过av_packet_unref解引用,因为在该方法内部已经调用过av_packet_unref方法解引用

严格来说,除AVERROR(EAGAIN)AVERROR_EOF两种错误情况之外的报错,应该直接退出程序


释放资源函数

avcodec_close(codecCtx);
avformat_close_input(&fmtCtx);
av_packet_free(&pkt);
av_frame_free(&frame);

只有我们手动申请的资源才需要我们手动进行释放,其余的资源FFmpeg会自动释放,重复调用会报错

如:AVCodecParametersAVFormatContext内部的资源,就不需要我们手动释放,在avformat_close_input函数中会对其进行释放,不需要我们通过avcodec_parameters_free释放

使用OpenCV显示视频图像

因为最近还在学OpenCV处理图像,而且相比于SDL、OpenGL或ANativeWindow流程更加简洁,所以这里就通过OpenCV显示视频图像

extern"C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
#include <iostream>
using namespace std;

#include <opencv2/opencv.hpp>
using namespace cv;

int main() {
    int ret = 0;
    //文件地址
    const char* filePath = "target.mp4";

    //声明所需的变量名
    AVFormatContext* fmtCtx = NULL;
    AVPacket* pkt = NULL;
    AVCodecContext* codecCtx = NULL;
    AVCodecParameters* avCodecPara = NULL;
    AVCodec* codec = NULL;

    //帧,并进行初始化
    AVFrame* rgbFrame = av_frame_alloc();
    AVFrame* yuvFrame = av_frame_alloc();

    do {
        ...
        //----------------- 设置数据转换参数 -------------------        
        struct SwsContext* img_ctx = sws_getContext(
            codecCtx->width, codecCtx->height, codecCtx->pix_fmt, //源地址长宽以及数据格式
            codecCtx->width, codecCtx->height, AV_PIX_FMT_BGR24,  //目的地址长宽以及数据格式
            SWS_BICUBIC, NULL, NULL, NULL);
        
        //一帧图像数据大小,会根据图像格式、图像宽高计算所需内存字节大小
        int numBytes = av_image_get_buffer_size(AV_PIX_FMT_BGR24, codecCtx->width, codecCtx->height, 1);
        unsigned char* out_buffer = (unsigned char*)av_malloc(numBytes * sizeof(unsigned char));

        //将rgbFrame中的数据以BGR格式存放在out_buffer中
        av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, out_buffer, AV_PIX_FMT_BGR24, codecCtx->width, codecCtx->height, 1);

        //创建OpenCV中的Mat
        Mat img = Mat(Size(codecCtx->width, codecCtx->height), CV_8UC3);

        while (av_read_frame(fmtCtx, pkt) >= 0) {
            if (pkt->stream_index == videoIndex) {
                ret = avcodec_send_packet(codecCtx, pkt);
                if (ret == 0) {
                    while (avcodec_receive_frame(codecCtx, yuvFrame) == 0) {
                        //mp4文件中视频流使用的是h.264,帧图像为yuv420格式
                        //通过sws_scale将数据转换为BGR格式
                        sws_scale(img_ctx,
                            (const uint8_t* const*)yuvFrame->data,
                            yuvFrame->linesize,
                            0,
                            codecCtx->height,
                            rgbFrame->data,
                            rgbFrame->linesize);

                        img.data = rgbFrame->data[0];

                        imshow("img", img);
                        waitKey(40);
                    }
                }
            }
            sws_freeContext(img_ctx);//释放SwsContext结构体
            av_packet_unref(pkt);//重置pkt的内容
        }
        ...
    } while (0);

    //----------------- 释放所有指针 ------------------- 
    av_packet_free(&pkt);
    avcodec_close(codecCtx);
    avformat_close_input(&fmtCtx);
    av_free(codec);

    av_frame_free(&rgbFrame);
    av_frame_free(&yuvFrame);

    return 0;
}
代码解析
结构体

libswscale库用于视频场景比例缩放、色彩映射转换;图像颜色空间或格式转换,而SwsContext结构体贯穿整个变换流程,其中存放变换所需的参数

// 其中截取出部分较为重要的数据 
typedef struct SwsContext {
    int srcW;                   //源图像中亮度/alpha的宽度
    int srcH;                   //源图像中亮度/alpha的高度
    int dstH;                   //目标图像中的亮度/alpha的宽度
    int dstW;                   //目标图像中的亮度/alpha的高度
    int chrSrcW;                //源图像中色度的宽度
    int chrSrcH;                //源图像中色度的高度
    int chrDstW;                //目标图像中色度的宽度
    int chrDstH;                //目标图像中色度的高度
    enum AVPixelFormat dstFormat;   //目标图像的格式,如:YUV420P、YUV444、RGB、RGBA、GRAY等
    enum AVPixelFormat srcFormat;   //源图像的格式
    int needAlpha;              //是否存在透明度
    int flags;                  //选择、优化、子采样算法的flag标识
    ...
 } SwsContext;
函数

sws_getContext

sws_getContext用来创建并返回SwsContext结构体

struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,
                                  int dstW, int dstH, enum AVPixelFormat dstFormat,
                                  int flags, SwsFilter *srcFilter,
                                  SwsFilter *dstFilter, const double *param);

参数:

  • int srcW:源图像的宽
  • int srcH:源图像的高
  • enum AVPixelFormat srcFormat:源图像的格式,如:YUV420P、YUV444、RGB、RGBA、GRAY等
  • int dstW:目标图像的宽
  • int dstH:目标图像的高
  • enum AVPixelFormat dstFormat:目标图像的格式
  • int flags:指定算法进行缩放插值
  • SwsFilter *srcFilter、SwsFilter *dstFilter:与Chroma/luminsence滤波相关,一般填NULL即可
  • const double *param:用于scalar的额外的数据,一般填NULL即可

return:

返回一个指向SwsContext的指针,或者出现错误的时候返回NULL

int flags:指定算法类型

#define SWS_FAST_BILINEAR     1        //选择快速双线性缩放算法
#define SWS_BILINEAR          2        //选择双线性缩放算法
#define SWS_BICUBIC           4        //选择双三次缩放算法
#define SWS_X                 8        
#define SWS_POINT          0x10
#define SWS_AREA           0x20
#define SWS_BICUBLIN       0x40
#define SWS_GAUSS          0x80
#define SWS_SINC          0x100
#define SWS_LANCZOS       0x200
#define SWS_SPLINE        0x400

其中具体根据需求选择合适的算法,可以看一下如何选择swscale中的缩放算法

其实还有一个获取SwsContext结构体的函数——sws_getCachedContext,这个函数会根据参数去检验参数中的SwsContext是否符合之后输入的参数,符合就直接返回该结构体指针进行复用,若不符合则会进行释放,然后根据参数创建一个新的SwsContext结构体并返回其指针

struct SwsContext *sws_getCachedContext(struct SwsContext *context,
                                        int srcW, int srcH, enum AVPixelFormat srcFormat,
                                        int dstW, int dstH, enum AVPixelFormat dstFormat,
                                        int flags, SwsFilter *srcFilter,
                                        SwsFilter *dstFilter, const double *param);

av_image_fill_arrays

该函数会根据图像类型参数、数组参数和宽高设置数据指针和线宽值

int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4],
                         const uint8_t *src,
                         enum AVPixelFormat pix_fmt, int width, int height, int align);

参数:

  • uint8_t *dst_data[4]:要进行填充的数据指针
  • int dst_linesize[4]:填充的图像的线宽值
  • const uint8_t *src:包含或之后会包含实际的图像数据
  • enum AVPixelFormat pix_fmt:图像格式
  • int width:图像的宽
  • int height:图像的高
  • int align:是否根据线宽对src进行对齐调整

return:

成功返回src所需的字节大小,失败会返回一个负数的错误码


sws_scale

sws_scale函数会根据SwsContext中设置的参数,将源图像转换为对应属性的目标图像,其中srcSlice必须是以图像中连续的行序列为顺序的二维数组

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
              const int srcStride[], int srcSliceY, int srcSliceH,
              uint8_t *const dst[], const int dstStride[]);

参数:

  • struct SwsContext *c:之前创建的SwsContext结构体
  • const uint8_t *const srcSlice[]:包含指向源数据平面的指针的数组,如:yuv420p中 y、u、v 数据分别存放,可以视为3个uint8_t数组
  • const int srcStride[]:对应每个源数据平面的长度
  • int srcSliceY:开始处理的y坐标位置
  • int srcSliceH:源数据平面的高度,即对应数组的长度
  • uint8_t *const dst[]:目标图像数据指针
  • const int dstStride[]:目标图像每个源数据平面的长度

return:

输出目标图像源数据平面的高度

RGB类型的图像R、G、B值混合存放所以无法将RGB抽离成对应的三个连续的数组,其数据全部存放在 data[0] 中,所以可以直接使用img.data = rgbFrame->data[0]给Mat赋值

而对于YUV格式的图像数据,按照img.data = rgbFrame->data[0]的方式给Mat赋值就会报错

资料参考

微信公众号:八小时程序员

FFmpeg4入门05:解码视频流过程

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

推荐阅读更多精彩内容