FFmpeg 入門(1):截取視頻幀

本文轉(zhuǎn)自:FFmpeg 入門(1):截取視頻幀 | www.samirchen.com

背景

在 Mac OS 上如果要運(yùn)行教程中的相關(guān)代碼需要先安裝 FFmpeg共郭,建議使用 brew 來(lái)安裝:

// 用 brew 安裝 FFmpeg:
brew install ffmpeg

或者你可以參考在 Mac OS 上編譯 FFmpeg使用源碼編譯和安裝 FFmpeg。

教程原文地址:http://dranger.com/ffmpeg/tutorial01.html舌稀,本文中的代碼做過(guò)部分修正礁击。

概要

媒體文件通常有一些基本的組成部分恰画。首先辙纬,文件本身被稱為「容器(container)」,容器的類型定義了文件的信息是如何存儲(chǔ)廊佩,比如囚聚,AVI、QuickTime 等容器格式标锄。接著顽铸,你需要了解的概念是「流(streams)」,例如料皇,你通常會(huì)有一路音頻流和一路視頻流谓松。流中的數(shù)據(jù)元素被稱為「幀(frames)」。每路流都會(huì)被相應(yīng)的「編/解碼器(codec)」進(jìn)行編碼或解碼(codec 這個(gè)名字就是源于 COded 和 DECoded)践剂。codec 定義了實(shí)際數(shù)據(jù)是如何被編解碼的鬼譬,比如你用到的 codecs 可能是 DivX 和 MP3。「數(shù)據(jù)包(packets)」是從流中讀取的數(shù)據(jù)片段逊脯,這些數(shù)據(jù)片段中包含的一個(gè)個(gè)比特就是解碼后能最終被我們的應(yīng)用程序處理的原始幀數(shù)據(jù)优质。為了達(dá)到我們音視頻處理的目標(biāo),每個(gè)數(shù)據(jù)包都包含著完整的幀军洼,在音頻情況下巩螃,一個(gè)數(shù)據(jù)包中可能會(huì)包含多個(gè)音頻幀。

基于以上這些基礎(chǔ)匕争,處理視頻流和音頻流的過(guò)程其實(shí)很簡(jiǎn)單:

  • 1:從 video.avi 文件中打開(kāi) video_stream避乏。
  • 2:從 video_stream 中讀取數(shù)據(jù)包到 frame。
  • 3:如果數(shù)據(jù)包中的 frame 不完整甘桑,則跳到步驟 2拍皮。
  • 4:處理 frame。
  • 5:跳到步驟 2扇住。

盡管在一些程序中上面步驟 4 處理 frame 的邏輯可能會(huì)非常復(fù)雜,但是在本文中的例程中盗胀,用 FFmpeg 來(lái)處理多媒體文件的部分會(huì)寫的比較簡(jiǎn)單一些艘蹋,這里我們將要做的就是打開(kāi)一個(gè)媒體文件,讀取其中的視頻流票灰,將視頻流中獲取到的視頻幀寫入到 PPM 文件中保存起來(lái)女阀。

下面我們一步一步來(lái)實(shí)現(xiàn)宅荤。

打開(kāi)媒體文件

首先,我們來(lái)看看如何打開(kāi)媒體文件浸策。在使用 FFmpeg 時(shí)冯键,首先需要初始化對(duì)應(yīng)的 Library。

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
//...

int main(int argc, char *argv[]) {

    // Register all formats and codecs.
    av_register_all();

    // ...
}

上面的代碼會(huì)注冊(cè) FFmpeg 庫(kù)中所有可用的「視頻格式」和 「codec」庸汗,這樣當(dāng)使用庫(kù)打開(kāi)一個(gè)媒體文件時(shí)惫确,就能找到對(duì)應(yīng)的視頻格式處理程序和 codec 來(lái)處理。需要注意的是在使用 FFmpeg 時(shí)蚯舱,你只需要調(diào)用 av_register_all() 一次即可改化,因此我們?cè)?main 中調(diào)用。當(dāng)然枉昏,你也可以根據(jù)需求只注冊(cè)給定的視頻格式和 codec陈肛,但通常你不需要這么做。

接下來(lái)我們就要準(zhǔn)備打開(kāi)媒體文件了兄裂,那么媒體文件中有哪些信息是值得注意的呢句旱?

  • 是否包含:音頻、視頻晰奖。
  • 碼流的封裝格式谈撒,用于解封裝。
  • 視頻的編碼格式畅涂,用于初始化視頻解碼器
  • 音頻的編碼格式港华,用于初始化音頻解碼器。
  • 視頻的分辨率午衰、幀率立宜、碼率,用于視頻的渲染臊岸。
  • 音頻的采樣率橙数、位寬、通道數(shù)帅戒,用于初始化音頻播放器灯帮。
  • 碼流的總時(shí)長(zhǎng),用于展示逻住、拖動(dòng) Seek钟哥。
  • 其他 Metadata 信息,如作者瞎访、日期等腻贰,用于展示。

這些關(guān)鍵的媒體信息扒秸,被稱作 metadata播演,常常記錄在整個(gè)碼流的開(kāi)頭或者結(jié)尾處冀瓦,例如:wav 格式主要由 wav header 頭來(lái)記錄音頻的采樣率、通道數(shù)写烤、位寬等關(guān)鍵信息翼闽;mp4 格式,則存放在 moov box 結(jié)構(gòu)中洲炊;而 FLV 格式則記錄在 onMetaData 中等等感局。

avformat_open_input 這個(gè)函數(shù)主要負(fù)責(zé)服務(wù)器的連接和碼流頭部信息的拉取,我們就用它來(lái)打開(kāi)媒體文件:

AVFormatContext *pFormatCtx = NULL;

// Open video file.
if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) {
    return -1; // Couldn't open file.
}

我們從程序入口獲得要打開(kāi)文件的路徑选浑,作為 avformat_open_input 函數(shù)的第二個(gè)參數(shù)傳入蓝厌,這個(gè)函數(shù)會(huì)讀取媒體文件的文件頭并將文件格式相關(guān)的信息存儲(chǔ)在我們作為第一個(gè)參數(shù)傳入的 AVFormatContext 數(shù)據(jù)結(jié)構(gòu)中。avformat_open_input 函數(shù)的第三個(gè)參數(shù)用于指定媒體文件格式古徒,第四個(gè)參數(shù)是文件格式相關(guān)選項(xiàng)拓提。如果你后面這兩個(gè)參數(shù)傳入的是 NULL,那么 libavformat 將自動(dòng)探測(cè)文件格式隧膘。

接下來(lái)對(duì)于媒體信息的探測(cè)和分析工作就要交給 avformat_find_stream_info 函數(shù)了:

// Retrieve stream information.
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
    return -1; // Couldn't find stream information.
}

avformat_find_stream_info 函數(shù)會(huì)為 pFormatCtx->streams 填充對(duì)應(yīng)的信息代态。這里還有一個(gè)調(diào)試用的函數(shù) av_dump_format 可以為我們打印 pFormatCtx 中都有哪些信息。

// Dump information about file onto standard error.
av_dump_format(pFormatCtx, 0, argv[1], 0);

AVFormatContext 里包含了下面這些跟媒體信息有關(guān)的成員:

  • struct AVInputFormat *iformat; // 記錄了封裝格式信息
  • unsigned int nb_streams; // 記錄了該 URL 中包含有幾路流
  • AVStream **streams; // 一個(gè)結(jié)構(gòu)體數(shù)組疹吃,每個(gè)對(duì)象記錄了一路流的詳細(xì)信息
  • int64_t start_time; // 第一幀的時(shí)間戳
  • int64_t duration; // 碼流的總時(shí)長(zhǎng)
  • int64_t bit_rate; // 碼流的總碼率蹦疑,bps
  • AVDictionary *metadata; // 一些文件信息頭,key/value 字符串

你拿到這些數(shù)據(jù)后萨驶,與 av_dump_format 的輸出對(duì)比可能會(huì)發(fā)現(xiàn)一些不同歉摧,這時(shí)候可以去看看 FFmpeg 源碼中 av_dump_format 的實(shí)現(xiàn),里面對(duì)打印出來(lái)的數(shù)據(jù)是有一些處理邏輯的腔呜。比如對(duì)于 start_time 的處理代碼如下:

if (ic->start_time != AV_NOPTS_VALUE) {
    int secs, us;
    av_log(NULL, AV_LOG_INFO, ", start: ");
    secs = ic->start_time / AV_TIME_BASE;
    us = llabs(ic->start_time % AV_TIME_BASE);
    av_log(NULL, AV_LOG_INFO, "%d.%06d", secs, (int) av_rescale(us, 1000000, AV_TIME_BASE));
}

由此可見(jiàn)叁温,經(jīng)過(guò) avformat_find_stream_info 的處理,我們可以拿到媒體資源的封裝格式核畴、總時(shí)長(zhǎng)膝但、總碼率了。此外 pFormatCtx->streams 是一個(gè) AVStream 指針的數(shù)組谤草,里面包含了媒體資源的每一路流信息跟束,數(shù)組的大小為 pFormatCtx->nb_streams

AVStream 結(jié)構(gòu)體中關(guān)鍵的成員包括:

  • AVCodecContext *codec; // 記錄了該碼流的編碼信息
  • int64_t start_time; // 第一幀的時(shí)間戳
  • int64_t duration; // 該碼流的時(shí)長(zhǎng)
  • int64_t nb_frames; // 該碼流的總幀數(shù)
  • AVDictionary *metadata; // 一些文件信息頭丑孩,key/value 字符串
  • AVRational avg_frame_rate; // 平均幀率

這里可以拿到平均幀率冀宴。

AVCodecContext 則記錄了一路流的具體編碼信息,其中關(guān)鍵的成員包括:

  • const struct AVCodec *codec; // 編碼的詳細(xì)信息
  • enum AVCodecID codec_id; // 編碼類型
  • int bit_rate; // 平均碼率
  • video only:
    • int width, height; // 圖像的寬高尺寸温学,碼流中不一定存在該信息略贮,會(huì)由解碼后覆蓋
    • enum AVPixelFormat pix_fmt; // 原始圖像的格式,碼流中不一定存在該信息,會(huì)由解碼后覆蓋
  • audio only:
    • int sample_rate; // 音頻的采樣率
    • int channels; // 音頻的通道數(shù)
    • enum AVSampleFormat sample_fmt; // 音頻的格式刨肃,位寬
    • int frame_size; // 每個(gè)音頻幀的 sample 個(gè)數(shù)

可以看到編碼類型、圖像的寬度高度箩帚、音頻的參數(shù)都在這里了真友。

了解完這些數(shù)據(jù)結(jié)構(gòu),我們接著往下走紧帕,直到我們找到一個(gè)視頻流:

// Find the first video stream.
videoStream = -1;
for (i = 0; i < pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
        videoStream = i;
        break;
    }
}
if (videoStream == -1) {
    return -1; // Didn't find a video stream.
}

// Get a pointer to the codec context for the video stream.
pCodecCtxOrig = pFormatCtx->streams[videoStream]->codec;

流信息中關(guān)于 codec 的部分存儲(chǔ)在 codec context 中盔然,這里包含了這路流所使用的所有的 codec 的信息,現(xiàn)在我們有一個(gè)指向它的指針了是嗜,但是我們接著還需要找到真正的 codec 并打開(kāi)它:

// Find the decoder for the video stream.
pCodec = avcodec_find_decoder(pCodecCtxOrig->codec_id);
if (pCodec == NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found.
}
// Copy context.
pCodecCtx = avcodec_alloc_context3(pCodec);
if (avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context.
}

// Open codec.
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
    return -1; // Could not open codec.
}

需要注意愈案,我們不能直接使用視頻流中的 AVCodecContext,所以我們需要用 avcodec_copy_context() 來(lái)拷貝一份新的 AVCodecContext 出來(lái)鹅搪。

存儲(chǔ)數(shù)據(jù)

接下來(lái)站绪,我們需要一個(gè)地方來(lái)存儲(chǔ)視頻中的幀:

AVFrame *pFrame = NULL;

// Allocate video frame.
pFrame = av_frame_alloc();

由于我們計(jì)劃將視頻幀輸出存儲(chǔ)為 PPM 文件,而 PPM 文件是會(huì)存儲(chǔ)為 24-bit RGB 格式的丽柿,所以我們需要將視頻幀從它本來(lái)的格式轉(zhuǎn)換為 RGB恢准。FFmpeg 可以幫我們做這些。對(duì)于大多數(shù)的項(xiàng)目甫题,我們可能都有將原來(lái)的視頻幀轉(zhuǎn)換為指定格式的需求∧倏穑現(xiàn)在我們就來(lái)創(chuàng)建一個(gè)AVFrame 用于格式轉(zhuǎn)換:

// Allocate an AVFrame structure.
pFrameRGB = av_frame_alloc();
if (pFrameRGB == NULL) {
    return -1;
}

盡管我們已經(jīng)分配了內(nèi)存類處理視頻幀,當(dāng)我們轉(zhuǎn)格式時(shí)坠非,我們?nèi)匀恍枰粔K地方來(lái)存儲(chǔ)視頻幀的原始數(shù)據(jù)敏沉。我們使用 av_image_get_buffer_size 來(lái)獲取需要的內(nèi)存大小,然后手動(dòng)分配這塊內(nèi)存炎码。

int numBytes;
uint8_t *buffer = NULL;

// Determine required buffer size and allocate buffer.
numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);
buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));

av_malloc 是一個(gè) FFmpeg 的 malloc盟迟,主要是對(duì) malloc 做了一些封裝來(lái)保證地址對(duì)齊之類的事情,它不會(huì)保證你的代碼不發(fā)生內(nèi)存泄漏辅肾、多次釋放或其他 malloc 問(wèn)題队萤。

現(xiàn)在我們用 av_image_fill_arrays 函數(shù)來(lái)關(guān)聯(lián) frame 和我們剛才分配的內(nèi)存。

// Assign appropriate parts of buffer to image planes in pFrameRGB Note that pFrameRGB is an AVFrame, but AVFrame is a superset of AVPicture
av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height, 1);

現(xiàn)在矫钓,我們準(zhǔn)備從視頻流讀取數(shù)據(jù)了要尔。

讀取數(shù)據(jù)

接下來(lái)我們要做的就是從整個(gè)視頻流中讀取數(shù)據(jù)包 packet,并將數(shù)據(jù)解碼到我們的 frame 中新娜,一旦獲得完整的 frame赵辕,我們就轉(zhuǎn)換其格式并存儲(chǔ)它。

AVPacket packet;
int frameFinished;
struct SwsContext *sws_ctx = NULL;

// Initialize SWS context for software scaling.
sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

// Read frames and save first five frames to disk.
i = 0;
while (av_read_frame(pFormatCtx, &packet) >= 0) {
    // Is this a packet from the video stream?
    if (packet.stream_index == videoStream) {
        // Decode video frame
        avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

        // Did we get a video frame?
        if (frameFinished) {
            // Convert the image from its native format to RGB.
            sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);

            // Save the frame to disk.
            if (++i <= 5) {
                SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
            }
        }
    }

    // Free the packet that was allocated by av_read_frame.
    av_packet_unref(&packet);
}

接下來(lái)的程序是比較好理解的:av_read_frame() 函數(shù)從視頻流中讀取一個(gè)數(shù)據(jù)包 packet概龄,把它存儲(chǔ)在 AVPacket 數(shù)據(jù)結(jié)構(gòu)中还惠。需要注意,我們只創(chuàng)建了 packet 結(jié)構(gòu)私杜,F(xiàn)Fmpeg 則為我們填充了其中的數(shù)據(jù)蚕键,其中 packet.data 這個(gè)指針會(huì)指向這些數(shù)據(jù)救欧,而這些數(shù)據(jù)占用的內(nèi)存需要通過(guò) av_packet_unref() 函數(shù)來(lái)釋放。avcodec_decode_video2() 函數(shù)將數(shù)據(jù)包 packet 轉(zhuǎn)換為視頻幀 frame锣光。但是笆怠,我們可能無(wú)法通過(guò)只解碼一個(gè) packet 就獲得一個(gè)完整的視頻幀 frame,可能需要讀取多個(gè) packet 才行誊爹,avcodec_decode_video2() 會(huì)在解碼到完整的一幀時(shí)設(shè)置 frameFinished 為真蹬刷。最后當(dāng)解碼到完整的一幀時(shí),我們用 sws_scale() 函數(shù)來(lái)將視頻幀本來(lái)的格式 pCodecCtx->pix_fmt 轉(zhuǎn)換為 RGB频丘。記住你可以將一個(gè) AVFrame 指針轉(zhuǎn)換為一個(gè) AVPicture 指針办成。最后,我們使用我們的 SaveFrame 函數(shù)來(lái)保存這一個(gè)視頻幀到文件搂漠。

SaveFrame 函數(shù)中迂卢,我們將 RGB 信息寫入到一個(gè) PPM 文件中。

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
    FILE *pFile;
    char szFilename[32];
    int y;
  
    // Open file.
    sprintf(szFilename, "frame%d.ppm", iFrame);
    pFile = fopen(szFilename, "wb");
    if (pFile == NULL) {
        return;
    }
  
    // Write header.
    fprintf(pFile, "P6\n%d %d\n255\n", width, height);
  
    // Write pixel data.
    for (y = 0; y < height; y++) {
        fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
    }
  
    // Close file.
    fclose(pFile);
}

下面我們回到 main 函數(shù)桐汤,當(dāng)我們完成了視頻流的讀取冷守,我們需要做一些掃尾工作:

// Free the RGB image.
av_free(buffer);
av_frame_free(&pFrameRGB);

// Free the YUV frame.
av_frame_free(&pFrame);

// Close the codecs.
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);

// Close the video file.
avformat_close_input(&pFormatCtx);

return 0;

你可以看到,這里我們用 av_free() 函數(shù)來(lái)釋放我們用 av_malloc() 分配的內(nèi)存惊科。

以上便是我們這節(jié)教程的全部?jī)?nèi)容拍摇,其中的完整代碼你可以從這里獲得:https://github.com/samirchen/TestFFmpeg

編譯執(zhí)行

你可以使用下面的命令編譯它:

$ gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lswscale -lz -lm

找一個(gè)媒體文件,你可以這樣執(zhí)行一下試試:

$ tutorial01 myvideofile.mp4
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末馆截,一起剝皮案震驚了整個(gè)濱河市充活,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜡娶,老刑警劉巖混卵,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異窖张,居然都是意外死亡幕随,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門宿接,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赘淮,“玉大人,你說(shuō)我怎么就攤上這事睦霎∩倚叮” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵副女,是天一觀的道長(zhǎng)蛤高。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么戴陡? 我笑而不...
    開(kāi)封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任塞绿,我火速辦了婚禮,結(jié)果婚禮上恤批,老公的妹妹穿的比我還像新娘位隶。我一直安慰自己,他們只是感情好开皿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著篮昧,像睡著了一般赋荆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懊昨,一...
    開(kāi)封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天窄潭,我揣著相機(jī)與錄音,去河邊找鬼酵颁。 笑死嫉你,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的躏惋。 我是一名探鬼主播幽污,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼簿姨!你這毒婦竟也來(lái)了距误?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤扁位,失蹤者是張志新(化名)和其女友劉穎准潭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體域仇,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刑然,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暇务。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泼掠。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖垦细,靈堂內(nèi)的尸體忽然破棺而出武鲁,到底是詐尸還是另有隱情,我是刑警寧澤蝠检,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布沐鼠,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏饲梭。R本人自食惡果不足惜乘盖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望憔涉。 院中可真熱鬧订框,春花似錦、人聲如沸兜叨。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)国旷。三九已至矛物,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間跪但,已是汗流浹背履羞。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屡久,地道東北人忆首。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像被环,于是被迫代替她去往敵國(guó)和親糙及。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容