MP4格式详解

概念

MP4(MPEG-4 Part 14)是一种标准的数字多媒体容器格式,其扩展名为.mp4,以存储数字音频及数字视频为主,也可存储字幕和静止图像。因其可容纳支持比特流的视频流(如高级视频编码),MP4可以在网络传输时使用流式传输。

术语

概念与术语是理解好MP4媒体封装格式和其操作算法的关键,为了方便了解MP4文件格式,需先了解以下几个概念与术语:

  1. Box:MP4文件是由一个个Box组成的,可以将其理解为一个数据块,它由Header+Data组成,Data 可以存储媒体元数据和实际的音视频码流数据。Box可直接存储数据块,也可包含其它Box,我们把包含其它Box的Box称为container box。
  2. Sample:可理解为采样,对于视频可理解为一帧数据,音频一帧数据就是一段固定时间的音频数据,可以由多个Sample数据组成,存储媒体数据的单位是sample。
  3. Chunk:连续几个sample组成的单元被称为chunk,每个chunk在文件中有一个偏移量,整个偏移量从文件头算起,在这个chunk内,sample是连续存储的。
  4. Track:表示一些chunk的集合,对于媒体数据而言就是一个视频序列或者音频序列,常说的音频/视频轨可对照该概念上。除了Video Track和Audio Track外,还可以有非媒体数据,比如Hint Track,这种类型的Track就不包含媒体数据,可以包含一些将其他数据打包成媒体数据的指示信息或者字幕信息。简单来说,Track是音视频中可以独立操作的媒体单位。

可理解为MP4文件中有多个Track,一个Track由多个Chunk组成,每个Chunk包含一组连续的Sample。例如视频流的一个Sample代表实际的nal数据,Chunk是数据存储的基本单位,它是一系列Sample数据的集合。

image.png

MP4容器格式

MP4是一种描述较为全面的容器格式,被认为可以在其中嵌入任何形式的数据,以及各种编码的音视频等,我们常见的大部分的MP4文件都是存放的AVC(H.264)或MPEG-4(Part 2)编码的视频和AAC编码的音频。MP4的结构就像俄罗斯的套娃,Box套着Box,也可理解为一棵Box树。下图是常见的box的结构图,可用来大致了解MP4文件的构造:

image.png

我们也可以通过在线工具MP4 Box来查看MP4文件的真实格式:

image.png

box结构

通过上面的介绍,我们了解了MP4格式就是由一个个的box组合成的box树,所有的数据都包含在box里,下面来了解一下box的基本结构。一个box是由Header+Data组成,如下图所示:

image.png

整个box以Header开头,Header中包含了box的大小(size)和类型(type)等信息。其中,size指明了整个box所占用的大小,包括Header部分,如果box很大(例如存放具体视频数据的mdat box),超过了uint32的最大数值,size就被设置为1,并用接下来的8位uint64的largesize来存放大小。box中的字节序为网络字节序,也就是大端字节序(Big-Endian)。

box根据header部分包含的信息的不同可以分为box和full box,如下图所示:

image.png

关于更多的box内容,在ISO_IEC_14496-12_2015中包含所有box的类型,详细可以参考该文档。

MP4基础box

ftyp

ftyp是MP4文件的第一个box,通过判断该box来确定文件的类型。该box有且仅有1个,并且只能被包含在文件层,而不能被其他box包含。该box放在文件的最开始,指示文件的相关信息。

文件的最开始是ftyp box的size,然后是该box的type。 ftyp的body依次包括1个32位的major brand(4个字符),1个32位的minor version(整数)和1个以32位(4个字符)为单位元素的数组compatible brands。这些都是用来指示文件应用级别的信息。

image.png

以一个MP4文件的ftyp box为例:

image.png
00000018:size,为24个字节,一般情况下为固定值
66747970:"ftyp"四个字符的ASCII值,也就是该box的type
6D703432:major brand,这里为"mp42"
00000000:minor version,值为0
6D703432 69736F6D:compatible brands,值为"mp42"和"isom"

虽然MP4、MOV、3GP等格式文件采用相同的封装标准,但由于是由不同的厂商合成,因此还是存在差别的。即使使用同一种媒体文件,比如MP4文件,由不同developers开发的,MP4内容格式也是存在差别的。ftyp作用就是为了标识它的developer是谁,兼容哪些标准等。

比如上面的例子,"mp42"表示它的major brand是MP4 v2 [ISO 14496-14],而"mp42"和"isom"则表示它的compatible brands是MP4 v2 [ISO 14496-14]和MP4 Base Media v1 [IS0 14496-12:2003]。

更多的ftyp可参见:ftyps.com,其中列出了所有已知的ftyp及对其的描述。

moov

moov box是MP4文件中必须有但只能存在一个的box,该box一般存的是媒体文件的元数据,其本身很简单,是一种container box,里面的数据是子box,自己更像是一个分界标识。

所谓的媒体元数据主要包含类似SPS PPS的编解码参数信息,还有音视频的时间戳等信息。对于MP4还有一个重要的采样表stbl信息,这里面定义了采样Sample、Chunk、Track的映射关系等,是MP4能够进行随机拖动、播放等操作的关键。

image.png
mvhd box

该box是全文件唯一的一个box,其对整个媒体文件所包含的媒体数据(Video Track、Audio Track等)进行全面的描述。其中包含了媒体的创建和修改时间,默认音量、色域、时长等信息。具体实例下:

image.png

mvhd是一个full box,对应字段的含义参考下图:

image.png

真实数据内容如下:

image.png

具体数据的解析:

0000006C 6D766864 00000000 DB8665DB DB8665DD 0000AC44 0009EA0C 00010000 01000000 00000000 00000000 00010000 00000000 00000000 00000000 00010000 00000000 00000000 00000000 40000000 00000000 00000000 00000000 00000000 00000000 00000000 00000003

0000006C: 长度108
6D766864: mvhd的ASCII标识
00: version为0
000000: flags为0
DB8665DB: createTime,从UTC时间的1904年1月1日0点至今的秒数,不过这里的时间并不影响播放器识别并播放影片
DB8665DD: modification time
0000AC44: time scale,文件媒体在1秒时间内的刻度值,可理解为1秒长度的时间单元数.即将1s平均分为1/44100份,每份1/44100s
0009EA0C: duration,媒体可播放时长:duration / timescale = 可播放时长(s)
00010000: rate,推荐播放速率,高16位和低16位分别为小数点整数部分和小数部分,即[16.16] 格式,该值为1.0(0x00010000)表示正常前向播放
0100: volume,与rate类似,[8.8] 格式,1.0(0x0100)表示最大音量
0000 00000000 00000000: reverse,10字节保留位

00010000 00000000 00000000
00000000 00010000 00000000
00000000 00000000 40000000: matrix,36字节视频变换矩阵

00000000 00000000 00000000 
00000000 00000000 00000000 :pre-defined, 24字节,其中包括:
  00000000: preview_time
  00000000: preview_duration
  00000000: poster_time
  00000000: selection_time
  00000000: selection_duration
  00000000: current_time
  
00000003: next track id, 下一个track使用的id号
iods box

这个box为full box,非必须box,实际也是24字节的固定值,data定义的内容应该是Audio和Video ProfileLevel方面的描述。

image.png

对应字段的含义参考下图:

image.png
trak box

trak box定义了媒体中一个Track的信息,视频有Video Track,音频有Audio Track,媒体文件中可以有多个Track,每个Track具有自己独立的时间和空间的信息,可以进行独立操作。每个Track Box都需要有一个tkhd box和mdia Box,其它的box都是可选择的:

image.png

tkhd box

描述了Track的媒体整体信息包括时长、图像的宽度和高度等,比较重要。

image.png

当trak为audio时,对应的width和height为0,对应字段的含义参考下图:

image.png

真实数据内容如下:

image.png

具体数据的解析:

0000005C 746B6864 00000001 DB8665DB DB8665DD 00000001 00000000 0009EA0C 00000000 00000000 00000000 00000000 00010000 00000000 00000000 00000000 00010000 00000000 00000000 00000000 40000000 02D00000 05000000

0000005C: size 92
746B6864: type tkhd
00: version 0
000001: flag 1
DB8665DB: 创建时间 3683018203
DB8665DD:修改时间 3683018205
00000000: reverse
0009EA0C: duration 649740
00000000 00000000: reverse2
0000: layer, 视频层,默认为0,值小的在上层  
0000: group, track分组信息,默认为0表示该track未与其他track有群组关系
00 00: 音量  
0000:  reverse
00010000  00000000   00000000  
00000000  00010000   00000000 
00000000  00000000   40000000: matrix[36] 视频变换矩阵
02D00000: width 47185920
05000000: height 83886080

edts/elst

Edit List Box,不是所有的mp4文件有这个box,作用是使某个track的时间戳产生偏移:

image.png

mdia box

这个Box也是Container Box,里面包含子Box,一般必须有mdhd box、hdlr box、minf box。基本就是当前Track媒体头信息和媒体句柄以及媒体信息。它自身非常简单,就是一个标识而已,但最复杂的还是里面包含的子box.

image.png

mdhd box

该box里面主要定义了该Track的媒体头信息,其中我们最关心的两个字段是timescale和duration,分别表示了该Track的时间戳和时长信息,这个时间戳信息也是PTS和DTS的单位。

image.png

对应字段的含义参考下图:

image.png

真实mdhd box的数据如下:

image.png

具体数据的解析:

00000020 6D646864 00000000 DB8665DB DB8665DD 00000258 00002288 55C40000

00000020: size 32
6D646864:  mdhd
00000000: version = 0, flag = 0
DB8665DB: creation_time
DB8665DD: modification_time
00000258: timescale
00002288: duration
55C40000 language 21956 languageString und(0X15+0x60, 0X0E+0x60, 0X04+0x60)

hdlr Box

该box解释了媒体的播放过程信息,用来设置不同Track的处理方式,标识了该Track的类型,音频Track的handler为soun,视频Track的handler为video。

image.png

对应字段的含义参考下图:

image.png

真实的数据如下:

image.png

具体数据的解析:

00000031 68646C72 00000000 00000000 76696465 00000000 00000000 00000000 436F7265 204D6564 69612056 6964656F

00000031: size 49
68646C72: hdlr 
00000000: version flag 都为0
00000000: component type: 全0
76696465: component subtype soun 代表该track为 audio track
00000000: Component manufacturer 。 Reserved. Set to 0.
00000000: Component flags。Reserved. Set to 0.
00000000: Component flags mask。Reserved. Set to 0。
436F7265 204D6564 69612056 6964656F: Name(Core Media Video.)

minf box

minfmoov中最重要最复杂的box,内部还有子Box,我们从上而下从外到内地分析各个box。该box建立了时间到真实音视频sample的映射关系,是音视频数据操作的关键。该box是container box,含有三大必须的子Box:

  1. 媒体信息头box: vmhd box(视频)、smhd box(音频)
  2. 数据信息box:dinf box
  3. 采样表box:stbl box
image.png

其中stbl是moov中最复杂的部分,stbl包含了媒体流每一个sample在文件中的offset,pts,duration等信息。想要播放一个mp4文件,必须根据stbl正确找到每个sample并送给解码器。stbl用来描述每个sample的信息,包含以下几个主要的子box:

stsd

Sample Description Box,存放解码必须的描述信息。

avc1 & mp4a

Video Track中会包含avc1 box,包含SPS PPS 等音视频解码信息:

image.png

avcC box:

image.png

真实的数据如下:

image.png

具体数据的解析:

0000007B 61766331 00000000 00000001 00000000 00000000 00000000 00000000 02D00500 00480000 00480000 00000000 00010000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000018 FFFF0000 00256176 63430164 0029FFE1 000A2764 0029AC56 C0B40A19 01000428 EE3CB0FD F8F800

0000007B: size 123
61766331: avc1
000000000000: reserve
0001: index 1
0000: pre_defined
0000: reserved
00000000: pre_defined
00000000: pre_defined
00000000: pre_defined
02D0: width 720 
0500: height 1280
00480000: Horiz resolution 4718592 
00480000: Ver resolution   4718592 
00000000: reverse
0001: frame count 1
0000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 0000: 32字节compressr_name
0018:bit_depth
FFFF:pre_defined

0000 0025: size 37
61766343: avcC
01: configurationVersion
64: AVC profile indication(100, Main profile)
00: AVC profile compatibility
29: AVC level indication 41
FF: nulu size(4 暂时不知道怎么来的)
E1: SPS个数,低五位有效,1个sps
000A: sps长度,10
2764 0029AC56 C0B40A19: sps
01: pps个数
0004: pps长度
28 EE3CB0: pps数据
FD F8F800:ext

Audio Track中会包含mp4a(MPEG-4 Audio) box:

image.png

mp4a box包含的esds box:

image.png

真实的数据如下:

image.png

具体数据的解析:

00000057 6D703461 00000000 00000001 00000000 00000000 00020010 00000000 AC440000 00000033 65736473 00000000 03808080 22000000 04808080 14401400 18000000 FA000000 FA000580 80800212 10068080 800102

00000057: size 87
6D703461: mp4a
00000000 0000:  6字节保留,必须为0
0001: index 1
0000: unsigned int(16) pre_defined = 0;
0000: const unsigned int(16) reserved = 0;
00000000:暂时不知道什么作用
0002:channel_count 2
0010:samplesize 166
00000000: version 和 flag 均为 0
00 AC44:samplerate 44100
0000: 暂时不知道什么作用


00000033: size 51
65736473: esds
00000000: version 和 flag 均为 0
03: ES_DescrTag
80808022: 4字节size,这里有点奇怪(一般就一字节),4字节长度,但只取最后一个0x22当长度
0000:ES_ID
00: 
  :0000 0000(bits)
     0:   steamDependenceFlag,如果为1,则有16bits的dependsOn_ES_IS
      0:    URL_Flag,如果为1,后边则有8bits URLlength, 和相应的URLstring(URLlength)
       0:   OCRstreamFlag, 如果为1,有16bits OCR_ES_id;
        0 0000: streamPriority
        
04: DecoderConfigDescriptor TAG
80 80 80 14: 4字节size(一般就一字节),这里有点奇怪,4字节长度,但只取最后一个0x14(20)当长度
40: objectTypeIndication (14496-1 Table8), 0x40是Audio (ISO/IEC 14496-3)
14:
   0001 0101
   0001 01        :streamType  5是Audio Stream, 14496-1 Table9
          0       :upStream
           0      :reserved
00 1800: bufferSizeDB: 6144
0000 FA00: Max bitrate 64000  //可以获取最大码率
0000 FA00: Avg bitrate 64000  //可以获取平均码率

05: DecSpecificInfotag
80 80 80 02: 4字节size(一般就一字节),这里有点奇怪,4字节长度,但只取最后一个0x02当长度
12 10:
  0001 0010 0001 0000:
  0001 0                     :audioObjectType 2 GASpecificConfig
        010 0                :samplingFrequencyIndex
             001 0           :channelConfiguration 1 
                  00         :cpConfig
                    0        :directMapping
                      
06: SLConfigDescrTag
80 80 80 01: 长度1
02: predefined 0x02 Reserved for use in MP4 files

stts

Time-to-Sample Box,保存每个sample时长,描述了sample时序的映射方法,我们通过它可以找到任何时间的sample。stts包含一个压缩的表来映射时间和sample序号,用其他的表来提供每个sample的长度和指针。表中每个条目提供了在同一个时间偏移量里面连续的sample序号,以及samples的偏移量。递增这些偏移量,就可以建立一个完整的time to sample表。

image.png

对应字段的含义参考下图:

image.png

真实数据内容分析如下:

00000018 73747473 00000000 00000001 000001BA

00000018: size 24
73747473: stts
00000000: version 和 flag
00000001: Entry count 1
000001BA: sample count 442
00000014: Sample delta 20

在mdhd的timescale为600,这里sample delta为20,600/20=30,即1秒30帧
stts只有一个entry,sample count442,delta 20, 442*20=56000, 与mdhd的duration相对应

ctts

Composition Time to Sample 时间合成偏移表,每个 sample 有自己解码序(DTS)和显示序(PTS)。对每个 sample 而言, DTS 和 PTS 不相同时,则存在该 BOX:

image.png

对应字段的含义参考下表:

image.png

stss

Sync Sample Box,存放关键帧列表:

image.png

对应字段的含义参考下图:

image.png

真实数据内容如下:

image.png

具体数据的解析:

0000004C 73747373 00000000 0000000F 00000001 0000001F 0000003D 0000005B 00000079 00000097 000000B5 000000D3 000000F1 0000010F 0000012D 0000014B 00000169 00000187 000001A5

0000004C: size 56
73747373: stss
00000000: version flag
0000000F: entry count 10,有15个关键帧
00 00 00 01: 第1个关键帧位于第1帧
...
...
000001A5:第16个关键帧位于第421帧

stsc

Sample-To-Chunk Box,sample-chunk映射表。上文提到MP4通常把sample封装到chunk中,一个chunk可能会包含一个或者几个sample。

image.png

对应字段的含义参考下图:

image.png

该box关键点在于里面的三个字段: first_chunk、samples_per_chunk、sample_description_index:

  1. first_chunk: 每一个 entry 开始的 chunk 位置
  2. samples_per_chunk: 每一个 chunk 里面包含多少的 sample
  3. sample_description_index: 每一个 sample 的描述,一般默认设为 1

这 3 个字段实际上决定了一个 MP4 中有多少个 chunks,每个 chunks 有多少个 samples。在 MP4 文件中,最小的基本单位是 Chunk 而不是 Sample:

  1. sample: 包含最小单元数据的 slice。里面有实际的 NAL 数据。
  2. chunk: 里面包含的是一个一个的 sample。为了是优化数据的读取,让 I/O 更有效率。

真实数据内容如下:

image.png

具体数据的解析:

00000034 73747363 00000000 00000003 00000001 0000000F 00000001 0000001B 0000001E 00000001 0000001C 00000016 00000001

00000034: size 52
73747363: stsc
00000000: version flag 0
00000003: entry count 3

00000001: first_chunk 1
0000000F: samples_per_chunk 15
00000001: sample_description_index 1
...

文件中Entrys数据如下:

first_chunk samples_per_chunk sample_description_index
1 15 1
27 30 1
28 22 1
  1. entry 1: 第 1 个 chunk 开始,有 26 个包含 15 个 sample的 chunk
  2. entry 2: 第 27 个 chunk,有 30 个 sample
  3. entry 3: 第 28 个 chunk,有 22 个 sample

所以全部的sample count = 390 + 30 + 22 = 442,与stts中的sample_counts值保持一致。

stsz

Sample Size Box,指定了每个sample的size。stsz box包含两sample总数和一张包含了每个sample size的表。

image.png

完整参数信息参见下图:

image.png

真实数据分析如下:

image.png
000006FC 7374737A 00000000 00000000 000001BA 000042C2 00000738 00001A6A 00002383 0000025B 000047CF 00000545 00003A1B 00000546 00003DD0 000010DE 00003AB7 000007F8 00003CC4 00000C89 000039D9 00002282 00002988 00000EBB 000031F8 000015B9 000026A4 0000098D 

000006FC: size 1788 
7374737A: stsz
00000000: version flag
00000000: Sample size 0
000001BA: Sample count 442 视频track可用于确定视频帧数

000042C2: 第一个sample(帧)大小 17090
...
...

stco

Chunk Offset Box,指定了每个chunk在文件中的位置,这个表是确定每个sample在文件中位置的关键。该表包含了chunk个数和一个包含每个chunk在文件中偏移位置的表。

image.png

完整参数信息参见下图:

image.png

真实数据分析如下:

image.png
00000080 7374636F 00000000 0000001C 000064C7 00026778 0004E1F5 00077C7A 0009996E 000BC0CD 000DD165 000F9A64 0010EA9C 0012D734 00147692 00166D4A 00183F7A 001ADEF2 001CC70B 001EB874 00204561 00221F51 0023C2E7 002676B4 00289C8B 002AB54B 002CCACE 002F1A66 0030F353 0032C3DF 003449E1 00379B7C

00000080: size 128
7374636F: stco
00000000: version flag
0000001C: entry count 28,有28个chunk

000064C7: 第一个Chunk offset 25799,与stsc(chunk包含sample的个数)和stsz(每个sample的大小)一起,可以找到具体序号的sample的位置和大小,用于读取数据
...
...

需要注意,这里stco只是指定的每个chunk在文件中的偏移位置,并没有给出每个sample在文件中的偏移。想要获得每个sample的偏移位置,需要结合 Sample Size box(stss)和Sample-To-Chunk (stsc)计算后取得。

mdat box

mdat box用于存储音视频数据,可从该Box解封装出真实的媒体数据。该Box一般都会存在,但非必须。

原始的NALU单元组成:

Start code + NALU header + NALU payload

但是在MP4文件中,H264 slice并不是以Start Code来分割,而是存储在mdat box的Data中。mdat box的格式:

Box header + Box Data
==>
Box size + Box type + (NALU length + NALU Header + NALU Data)..+..(NALU length + NALU Header + NALU Data)

下面通过分析真实MP4文件的mdat box的Data数据:

image.png
0011A1C0 6D646174 01402280 A37D2085 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D 2D2D2D2D

0011A1C0: size 1155520
6D646174: type mdat

参考资料

ISO/IEC 14496 Part 12:http://standards.iso.org/ittf/PubliclyAvailableStandards/index.html

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

推荐阅读更多精彩内容