解析H264的SPS信息

在做音视频开发的时候,存在不解码视频帧的前提下需要获取视频宽高、帧率等信息,而H.264中的SPS数据可为我们提供这些相关的信息。在此之前,我们需要对一些协议和算法有一定的初步了解,后文中有完整的代码展示。

H.264协议

我们在此不讲解H.264协议,但是我们需要了解NAL单元语法(NAL unit syntax)、序列参数集数据语法(Sequence parameter set data syntax)、视频可用参数语法(VUI parameters syntax)。具体可去ITU-T官网下载H.264文档,我查看的是T-REC-H.264-201704-I!!PDF-E.pdf。

Sequence parameter set data syntax

image
image.gif

image
image.gif

VUI parameters syntax

image
image.gif

image
image.gif

第一列:相关参数信息名称;

第二列:规定条带数据可以至多划分为三种条带数据类别(这个可以不用理解);

第三列中的u(n)表示:使用n位无符号整数表示,由n位bit换算得到,即从左到右读取n个bit位所表示的值;ue(v)表示:无符号指数哥伦布编码值;ue(v)表示:有符号指数哥伦布编码值。

u(n)代码实现:

static UINT u(sps_bit_stream *bs, BYTE bitCount)
{
    UINT val = 0;
    for (BYTE i=0; i<bitCount; i++) {
        val <<= 1;
        if (eof(bs)) {
            val = 0;
            break;
        } else if (bs->data[bs->index / 8] & (0x80 >> (bs->index % 8))) {     //计算index所在的位是否为1
            val |= 1;
        }
        bs->index++;
    }

    return val;
}
image.gif

指数哥伦布编码

Golomb编码是一种无损的数据压缩方法,由数学家Solomon W.Golomb在1960年代发明。Golomb编码只能对非负整数进行编码,符号表中的符号出现的概率符合几何分布(Geometric Distribution)时,使用Golomb编码可以取得最优效果,也就是说Golomb编码比较适合小的数字比大的数字出现概率比较高的编码。它使用较短的码长编码较小的数字,较长的码长编码较大的数字。在此,我们只需要了解:无符号指数哥伦布编码ue(v)、有符号指数哥伦布编码se(v)。

无符号指数哥伦布编码(UE)

哥伦布编码的码字code_word由三部分组成:code_word = [M个0] + [1] + [Info]
其中,Info是一个携带信息的M位数据,每个哥伦布码的长度为(2M+1)位,每个码字都可由code_num产生。
根据码字code_word解码出code_num值的过程如下:
1. 首先读入M位以"1"为结尾的0;
2. 根据得到的M,读入接下来的M位Info数据;

3. 根据这个公式得到计算结果code_num = Info – 1 + 2M

代码实现:

static UINT ue(sps_bit_stream *bs)
{
    UINT zeroNum = 0;
    while (u(bs, 1) == 0 && !eof(bs) && zeroNum < 32) {
        zeroNum ++;
    }

    return (UINT)((1 << zeroNum) - 1 + u(bs, zeroNum));
}
image.gif

有符号指数哥伦布编码(SE)

有符号的指数哥伦布编码值是通过无符号的指数哥伦布编码的值通过换算得到的,其换算关系为:n = (-1)^(k+1) * ceil(k/2)。

代码实现:

INT se(sps_bit_stream *bs)
{
    INT ueVal = (INT)ue(bs);
    double k = ueVal;

    INT seVal = (INT)ceil(k / 2);     //ceil:返回大于或者等于指定表达式的最小整数
    if (ueVal % 2 == 0) {       //偶数取反,即(-1)^(k+1)
        seVal = -seVal;
    }

    return seVal;
}
image.gif

SPS参数分析

视频宽高(Width、Height)获取

从H.264协议文档中可以看出视频宽高涉及到的参数有:

pic_width_in_mbs_minus1:加1指定以宏块(16*16)为单位的每个解码图像的宽度,即PicWidthInSamplesL = (pic_width_in_mbs_minus1 + 1) * 16 ;

image
image.gif

pic_height_in_map_units_minus1:加1指定解码帧或场中的一个切片组的高度,即PicSizeInMapUnits = PicWidthInMbs * (pic_height_in_map_units_minus1 + 1) ;

image
image.gif

frame_mbs_only_flag:等于0指明了视频序列的编码图象可能是编码场或编码帧,等于1指明了每个编码视频序列的编码图像都是只含帧宏块的编码帧,即FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits ;

image

image.gif

frame_cropping_flag:等于0表明不存在帧剪切偏移量,等于1表明在sps中下一个使用的帧剪切偏移量参数,即需要使用到frame_crop_left_offset、frame_crop_right_offset、frame_crop_top_offset、frame_crop_bottom_offset四个参数;

frame_crop_left_offset、frame_crop_right_offset、frame_crop_top_offset、frame_crop_bottom_offset:左右上下帧裁剪偏移量,需要根据chroma format计算实际的视频宽高;

image
image.gif

chroma_format_idc:亮度取样对应的色度取样值,包括0~3,不存在时默认为1(即YUV420);

image
image.gif

根据上述参数的定义说明,计算视频分辨率存在多种情况:1、需要判断是否包含宏片的编码帧或场;2、需要判断视频帧是否被裁剪;3、需要判断采样类型。因此代码大致如下:

UINT pic_width_in_mbs_minus1 = ue(&bs);     //第36位开始
UINT pic_height_in_map_units_minus1 = ue(&bs);      //47
UINT frame_mbs_only_flag = u(&bs, 1);

info->width = (INT)(pic_width_in_mbs_minus1 + 1) * 16;
info->height = (INT)(2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16;

if (!frame_mbs_only_flag) {
    u(&bs, 1);      //mb_adaptive_frame_field_flag
}

u(&bs, 1);     //direct_8x8_inference_flag
UINT frame_cropping_flag = u(&bs, 1);
if (frame_cropping_flag) {
    UINT frame_crop_left_offset = ue(&bs);
    UINT frame_crop_right_offset = ue(&bs);
    UINT frame_crop_top_offset = ue(&bs);
    UINT frame_crop_bottom_offset= ue(&bs);

    //See 6.2 Source, decoded, and output picture formats
    INT crop_unit_x = 1;
    INT crop_unit_y = 2 - frame_mbs_only_flag;      //monochrome or 4:4:4
    if (chroma_format_idc == 1) {   //4:2:0
        crop_unit_x = 2;
        crop_unit_y = 2 * (2 - frame_mbs_only_flag);
    } else if (chroma_format_idc == 2) {    //4:2:2
        crop_unit_x = 2;
        crop_unit_y = 2 - frame_mbs_only_flag;
    }

    info->width -= crop_unit_x * (frame_crop_left_offset + frame_crop_right_offset);
    info->height -= crop_unit_y * (frame_crop_top_offset + frame_crop_bottom_offset);
}
image.gif

视频帧率(FPS)获取

视频帧率信息在SPS的VUI parameters syntax中,需要根据time_scale、fixed_frame_rate_flag计算得到:fps = time_scale / num_units_in_tick。但是需要判断参数timing_info_present_flag是否存在,若不存在表示FPS在信息流中无法获取。同时还存在另外一种情况:fixed_frame_rate_flag为1时,两个连续图像的HDR输出时间频率为单位,获取的fps是实际的2倍。

UINT timing_info_present_flag = u(bs, 1);
if (timing_info_present_flag) {
    UINT num_units_in_tick = u(bs, 32);
    UINT time_scale = u(bs, 32);
    UINT fixed_frame_rate_flag = u(bs, 1);

    info->fps = (UINT)((float)time_scale / (float)num_units_in_tick);
    if (fixed_frame_rate_flag) {
        info->fps = info->fps/2;
    }
}
image.gif

SPS解析代码

H264ParseSPS.h

//
//  H264ParseSPS.h
//
//  Created by lzj<lizhijian_21@163.com> on 2018/7/6.
//  Copyright © 2018年 LZJ. All rights reserved.
//

#ifndef H264ParseSPS_h
#define H264ParseSPS_h
#ifdef __cplusplus
extern "C"
{
#endif
#include <stdio.h>

typedef struct
{
    unsigned int profile_idc;
    unsigned int level_idc;

    unsigned int width;
    unsigned int height;
    unsigned int fps;       //SPS中可能不包含FPS信息
} sps_info_struct;

/**
 解析SPS数据信息

 @param data SPS数据内容,需要Nal类型为0x7数据的开始(比如:67 42 00 28 ab 40 22 01 e3 cb cd c0 80 80 a9 02)
 @param dataSize SPS数据的长度
 @param info SPS解析之后的信息数据结构体
 @return success:1,fail:0

 */
int h264_parse_sps(const unsigned char *data, unsigned int dataSize, sps_info_struct *info);

#ifdef __cplusplus
}
#endif
#endif /* H264ParseSPS_h */

image.gif

H264ParseSPS.c

//
//  H264ParseSPS.c
//
//  Created by lzj<lizhijian_21@163.com> on 2018/7/6.
//  Copyright © 2018年 LZJ. All rights reserved.
//
//  See https://www.itu.int/rec/T-REC-H.264-201610-S
//

#include "H264ParseSPS.h"
#include <string.h>
#include <stdlib.h>
#include <math.h>

typedef unsigned char BYTE;
typedef int INT;
typedef unsigned int UINT;

typedef struct
{
    const BYTE *data;   //sps数据
    UINT size;          //sps数据大小
    UINT index;         //当前计算位所在的位置标记
} sps_bit_stream;

/**
 移除H264的NAL防竞争码(0x03)

 @param data sps数据
 @param dataSize sps数据大小
 */
static void del_emulation_prevention(BYTE *data, UINT *dataSize)
{
    UINT dataSizeTemp = *dataSize;
    for (UINT i=0, j=0; i<(dataSizeTemp-2); i++) {
        int val = (data[i]^0x0) + (data[i+1]^0x0) + (data[i+2]^0x3);    //检测是否是竞争码
        if (val == 0) {
            for (j=i+2; j<dataSizeTemp-1; j++) {    //移除竞争码
                data[j] = data[j+1];
            }

            (*dataSize)--;      //data size 减1
        }
    }
}

static void sps_bs_init(sps_bit_stream *bs, const BYTE *data, UINT size)
{
    if (bs) {
        bs->data = data;
        bs->size = size;
        bs->index = 0;
    }
}

/**
 是否已经到数据流最后

 @param bs sps_bit_stream数据
 @return 1:yes,0:no
 */
static INT eof(sps_bit_stream *bs)
{
    return (bs->index >= bs->size * 8);    //位偏移已经超出数据
}

/**
 读取从起始位开始的BitCount个位所表示的值

 @param bs sps_bit_stream数据
 @param bitCount bit位个数(从低到高)
 @return value
 */
static UINT u(sps_bit_stream *bs, BYTE bitCount)
{
    UINT val = 0;
    for (BYTE i=0; i<bitCount; i++) {
        val <<= 1;
        if (eof(bs)) {
            val = 0;
            break;
        } else if (bs->data[bs->index / 8] & (0x80 >> (bs->index % 8))) {     //计算index所在的位是否为1
            val |= 1;
        }
        bs->index++;  //递增当前起始位(表示该位已经被计算,在后面的计算过程中不需要再次去计算所在的起始位索引,缺点:后面每个bit位都需要去位移)
    }

    return val;
}

/**
 读取无符号哥伦布编码值(UE)
 #2^LeadingZeroBits - 1 + (xxx)

 @param bs sps_bit_stream数据
 @return value
 */
static UINT ue(sps_bit_stream *bs)
{
    UINT zeroNum = 0;
    while (u(bs, 1) == 0 && !eof(bs) && zeroNum < 32) {
        zeroNum ++;
    }

    return (UINT)((1 << zeroNum) - 1 + u(bs, zeroNum));
}

/**
 读取有符号哥伦布编码值(SE)
 #(-1)^(k+1) * Ceil(k/2)

 @param bs sps_bit_stream数据
 @return value
 */
INT se(sps_bit_stream *bs)
{
    INT ueVal = (INT)ue(bs);
    double k = ueVal;

    INT seVal = (INT)ceil(k / 2);     //ceil:返回大于或者等于指定表达式的最小整数
    if (ueVal % 2 == 0) {       //偶数取反,即(-1)^(k+1)
        seVal = -seVal;
    }

    return seVal;
}

/**
 视频可用性信息(Video usability information)解析

 @param bs sps_bit_stream数据
 @param info sps解析之后的信息数据及结构体
 @see E.1.1 VUI parameters syntax
 */
void vui_para_parse(sps_bit_stream *bs, sps_info_struct *info)
{
    UINT aspect_ratio_info_present_flag = u(bs, 1);
    if (aspect_ratio_info_present_flag) {
        UINT aspect_ratio_idc = u(bs, 8);
        if (aspect_ratio_idc == 255) {      //Extended_SAR
            u(bs, 16);      //sar_width
            u(bs, 16);      //sar_height
        }
    }

    UINT overscan_info_present_flag = u(bs, 1);
    if (overscan_info_present_flag) {
        u(bs, 1);       //overscan_appropriate_flag
    }

    UINT video_signal_type_present_flag = u(bs, 1);
    if (video_signal_type_present_flag) {
        u(bs, 3);       //video_format
        u(bs, 1);       //video_full_range_flag
        UINT colour_description_present_flag = u(bs, 1);
        if (colour_description_present_flag) {
            u(bs, 8);       //colour_primaries
            u(bs, 8);       //transfer_characteristics
            u(bs, 8);       //matrix_coefficients
        }
    }

    UINT chroma_loc_info_present_flag = u(bs, 1);
    if (chroma_loc_info_present_flag) {
        ue(bs);     //chroma_sample_loc_type_top_field
        ue(bs);     //chroma_sample_loc_type_bottom_field
    }

    UINT timing_info_present_flag = u(bs, 1);
    if (timing_info_present_flag) {
        UINT num_units_in_tick = u(bs, 32);
        UINT time_scale = u(bs, 32);
        UINT fixed_frame_rate_flag = u(bs, 1);

        info->fps = (UINT)((float)time_scale / (float)num_units_in_tick);
        if (fixed_frame_rate_flag) {
            info->fps = info->fps/2;
        }
    }

    UINT nal_hrd_parameters_present_flag = u(bs, 1);
    if (nal_hrd_parameters_present_flag) {
        //hrd_parameters()  //see E.1.2 HRD parameters syntax
    }

    //后面代码需要hrd_parameters()函数接口实现才有用
    UINT vcl_hrd_parameters_present_flag = u(bs, 1);
    if (vcl_hrd_parameters_present_flag) {
        //hrd_parameters()
    }
    if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) {
        u(bs, 1);   //low_delay_hrd_flag
    }

    u(bs, 1);       //pic_struct_present_flag
    UINT bitstream_restriction_flag = u(bs, 1);
    if (bitstream_restriction_flag) {
        u(bs, 1);   //motion_vectors_over_pic_boundaries_flag
        ue(bs);     //max_bytes_per_pic_denom
        ue(bs);     //max_bits_per_mb_denom
        ue(bs);     //log2_max_mv_length_horizontal
        ue(bs);     //log2_max_mv_length_vertical
        ue(bs);     //max_num_reorder_frames
        ue(bs);     //max_dec_frame_buffering
    }
}

//See 7.3.1 NAL unit syntax
//See 7.3.2.1.1 Sequence parameter set data syntax
INT h264_parse_sps(const BYTE *data, UINT dataSize, sps_info_struct *info)
{
    if (!data || dataSize <= 0 || !info) return 0;
    INT ret = 0;

    BYTE *dataBuf = malloc(dataSize);
    memcpy(dataBuf, data, dataSize);        //重新拷贝一份数据,防止移除竞争码时对原数据造成影响
    del_emulation_prevention(dataBuf, &dataSize);

    sps_bit_stream bs = {0};
    sps_bs_init(&bs, dataBuf, dataSize);   //初始化SPS数据流结构体

    u(&bs, 1);      //forbidden_zero_bit
    u(&bs, 2);      //nal_ref_idc
    UINT nal_unit_type = u(&bs, 5);

    if (nal_unit_type == 0x7) {     //Nal SPS Flag
        info->profile_idc = u(&bs, 8);
        u(&bs, 1);      //constraint_set0_flag
        u(&bs, 1);      //constraint_set1_flag
        u(&bs, 1);      //constraint_set2_flag
        u(&bs, 1);      //constraint_set3_flag
        u(&bs, 1);      //constraint_set4_flag
        u(&bs, 1);      //constraint_set4_flag
        u(&bs, 2);      //reserved_zero_2bits
        info->level_idc = u(&bs, 8);

        ue(&bs);    //seq_parameter_set_id

        UINT chroma_format_idc = 1;     //摄像机出图大部分格式是4:2:0
        if (info->profile_idc == 100 || info->profile_idc == 110 || info->profile_idc == 122 ||
            info->profile_idc == 244 || info->profile_idc == 44 || info->profile_idc == 83 ||
            info->profile_idc == 86 || info->profile_idc == 118 || info->profile_idc == 128 ||
            info->profile_idc == 138 || info->profile_idc == 139 || info->profile_idc == 134 || info->profile_idc == 135) {
            chroma_format_idc = ue(&bs);
            if (chroma_format_idc == 3) {
                u(&bs, 1);      //separate_colour_plane_flag
            }

            ue(&bs);        //bit_depth_luma_minus8
            ue(&bs);        //bit_depth_chroma_minus8
            u(&bs, 1);      //qpprime_y_zero_transform_bypass_flag
            UINT seq_scaling_matrix_present_flag = u(&bs, 1);
            if (seq_scaling_matrix_present_flag) {
                UINT seq_scaling_list_present_flag[8] = {0};
                for (INT i=0; i<((chroma_format_idc != 3)?8:12); i++) {
                    seq_scaling_list_present_flag[i] = u(&bs, 1);
                    if (seq_scaling_list_present_flag[i]) {
                        if (i < 6) {    //scaling_list(ScalingList4x4[i], 16, UseDefaultScalingMatrix4x4Flag[I])
                        } else {    //scaling_list(ScalingList8x8[i − 6], 64, UseDefaultScalingMatrix8x8Flag[i − 6] )
                        }
                    }
                }
            }
        }

        ue(&bs);        //log2_max_frame_num_minus4
        UINT pic_order_cnt_type = ue(&bs);
        if (pic_order_cnt_type == 0) {
            ue(&bs);        //log2_max_pic_order_cnt_lsb_minus4
        } else if (pic_order_cnt_type == 1) {
            u(&bs, 1);      //delta_pic_order_always_zero_flag
            se(&bs);        //offset_for_non_ref_pic
            se(&bs);        //offset_for_top_to_bottom_field

            UINT num_ref_frames_in_pic_order_cnt_cycle = ue(&bs);
            INT *offset_for_ref_frame = (INT *)malloc((UINT)num_ref_frames_in_pic_order_cnt_cycle * sizeof(INT));
            for (INT i = 0; i<num_ref_frames_in_pic_order_cnt_cycle; i++) {
                offset_for_ref_frame[i] = se(&bs);
            }
            free(offset_for_ref_frame);
        }

        ue(&bs);      //max_num_ref_frames
        u(&bs, 1);      //gaps_in_frame_num_value_allowed_flag

        UINT pic_width_in_mbs_minus1 = ue(&bs);     //第36位开始
        UINT pic_height_in_map_units_minus1 = ue(&bs);      //47
        UINT frame_mbs_only_flag = u(&bs, 1);

        info->width = (INT)(pic_width_in_mbs_minus1 + 1) * 16;
        info->height = (INT)(2 - frame_mbs_only_flag) * (pic_height_in_map_units_minus1 + 1) * 16;

        if (!frame_mbs_only_flag) {
            u(&bs, 1);      //mb_adaptive_frame_field_flag
        }

        u(&bs, 1);     //direct_8x8_inference_flag
        UINT frame_cropping_flag = u(&bs, 1);
        if (frame_cropping_flag) {
            UINT frame_crop_left_offset = ue(&bs);
            UINT frame_crop_right_offset = ue(&bs);
            UINT frame_crop_top_offset = ue(&bs);
            UINT frame_crop_bottom_offset= ue(&bs);

            //See 6.2 Source, decoded, and output picture formats
            INT crop_unit_x = 1;
            INT crop_unit_y = 2 - frame_mbs_only_flag;      //monochrome or 4:4:4
            if (chroma_format_idc == 1) {   //4:2:0
                crop_unit_x = 2;
                crop_unit_y = 2 * (2 - frame_mbs_only_flag);
            } else if (chroma_format_idc == 2) {    //4:2:2
                crop_unit_x = 2;
                crop_unit_y = 2 - frame_mbs_only_flag;
            }

            info->width -= crop_unit_x * (frame_crop_left_offset + frame_crop_right_offset);
            info->height -= crop_unit_y * (frame_crop_top_offset + frame_crop_bottom_offset);
        }

        UINT vui_parameters_present_flag = u(&bs, 1);
        if (vui_parameters_present_flag) {
            vui_para_parse(&bs, info);
        }

        ret = 1;
    }
    free(dataBuf);

    return ret;
}
image.gif

代码中部分未用到的参数并没有获取并赋值,比如:u(&bs, 1); //forbidden_zero_bit。如需获取相应的参数,只需将注释后面对应的变量值进行赋值即可,如:UINT forbidden_zero_bit = u(&bs, 1);

h264_parse_sps接口中sps数据需要第一个字节以Nal类型为0x7作为开始,比如:67 42 00 28 ab 40 22 01 e3 cb cd c0 80 80 a9 02,解析得到的宽高为1080*1920,fps为0。

参考文档:T-REC-H.264-201704-I!!PDF-E.pdfH264 SPS分析编码算法之指数哥伦布编码

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

推荐阅读更多精彩内容