使用FFmpeg實(shí)現(xiàn)H.264解碼

前面通過 H.264 編碼將 YUV 像素?cái)?shù)據(jù)壓縮生成了一個(gè) h264 文件快耿。那么想要播放 h264 文件泪喊,就需要解壓縮取出每一幀的具體像素?cái)?shù)據(jù)進(jìn)行播放。本文的內(nèi)容主要是解碼裸流洲敢,即從本地讀取 h264 文件蛀恩,解碼成 YUV 像素?cái)?shù)據(jù)的過程。

一镜硕、使用 FFmpeg 命令行進(jìn)行 H.264 解碼:
$ ffmpeg -c:v h264 -i in.h264 out.yuv

解碼時(shí) -c:v h264 是輸入?yún)?shù)运翼。查看本地的解碼器:

$ ffmpeg -decoders | grep 264
 VFS..D h264                 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
二、使用 FFmpeg 編程實(shí)現(xiàn) H.264 編碼

首先需要導(dǎo)入用到的 FFmpeg 庫 libavcodeclibavutil兴枯。和前面 H.264 編碼用到的庫是一樣的血淌,并且 H.264 解碼流程和 AAC 解碼流程也是類似的。

H.264 解碼流程

1财剖、獲取解碼器

在我本地默認(rèn)的解碼器就是 h264悠夯,通過 ID 或者名稱獲取到的 H.264 解碼器都是 h264

// 使用 ID 獲取編碼器:
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
// 或者使用名稱獲取編碼器:
codec = avcodec_find_decoder_by_name("h264");

2躺坟、初始化解析器上下文

通過 ID 創(chuàng)建 H.264 解析器上下文:

parserCtx = av_parser_init(codec->id);

查看函數(shù) av_parser_init 源碼:

// 源碼位置:ffmpeg-4.3.2/libavcodec/parser.c
AVCodecParserContext *av_parser_init(int codec_id)
{
    AVCodecParserContext *s = NULL;
    const AVCodecParser *parser;
    void *i = 0;
    int ret;

    if (codec_id == AV_CODEC_ID_NONE)
        return NULL;

    while ((parser = av_parser_iterate(&i))) {
        if (parser->codec_ids[0] == codec_id ||
            parser->codec_ids[1] == codec_id ||
            parser->codec_ids[2] == codec_id ||
            parser->codec_ids[3] == codec_id ||
            parser->codec_ids[4] == codec_id)
            goto found;
    }
    return NULL;

found:
    s = av_mallocz(sizeof(AVCodecParserContext));
    if (!s)
        goto err_out;
    s->parser = (AVCodecParser*)parser;
    s->priv_data = av_mallocz(parser->priv_data_size);
    if (!s->priv_data)
        goto err_out;
    s->fetch_timestamp=1;
    s->pict_type = AV_PICTURE_TYPE_I;
    if (parser->parser_init) {
        ret = parser->parser_init(s);
        if (ret != 0)
            goto err_out;
    }
    s->key_frame            = -1;
#if FF_API_CONVERGENCE_DURATION
FF_DISABLE_DEPRECATION_WARNINGS
    s->convergence_duration = 0;
FF_ENABLE_DEPRECATION_WARNINGS
#endif
    s->dts_sync_point       = INT_MIN;
    s->dts_ref_dts_delta    = INT_MIN;
    s->pts_dts_delta        = INT_MIN;
    s->format               = -1;

    return s;

err_out:
    if (s)
        av_freep(&s->priv_data);
    av_free(s);
    return NULL;
}
// 源碼片段 ffmpeg-4.3.2/libavcodec/parsers.c
const AVCodecParser *av_parser_iterate(void **opaque)
{
    uintptr_t i = (uintptr_t)*opaque;
    const AVCodecParser *p = parser_list[i];

    if (p)
        *opaque = (void*)(i + 1);

    return p;
}
// 源碼片段 ffmpeg-4.3.2/libavcodec/parsers.c
AVCodecParser ff_h264_parser = {
    .codec_ids      = { AV_CODEC_ID_H264 },
    .priv_data_size = sizeof(H264ParseContext),
    .parser_init    = init,
    .parser_parse   = h264_parse,
    .parser_close   = h264_close,
    .split          = h264_split,
};

源碼中的第一步就是通過 ID 查找 parser沦补,此處傳入的 codec->id 就是 AV_CODEC_ID_H264。函數(shù) av_parser_iterate 是 parser 迭代器咪橙,其內(nèi)部是在 parser_list 數(shù)組中查找 parser(parser_list 在源碼文件 ffmpeg-4.3.2/libavcodec/parser_list.c 中)夕膀。最終找到的 H.264 解析器是 ff_h264_parser

3匣摘、創(chuàng)建解析器上下文

ctx = avcodec_alloc_context3(codec);

4店诗、創(chuàng)建AVPacket

pkt = av_packet_alloc();

5、創(chuàng)建AVFrame

frame = av_frame_alloc();

6音榜、打開解碼器

ret = avcodec_open2(ctx, codec, nullptr);

7、打開文件

inFile.open(QFile::ReadOnly)
outFile.open(QFile::WriteOnly)
inLen = inFile.read(inDataArray, IN_DATA_SIZE);

8捧弃、讀取文件數(shù)據(jù) & 解析數(shù)據(jù)

while ((inLen = inFile.read(inDataArray, IN_INBUF_SIZE)) > 0) {
    inData = inDataArray;
    while (inLen > 0) {
        // 解析器解析數(shù)據(jù)
        ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (uint8_t *) inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0) {
            ERRBUF(ret);
            qDebug() << "av_parser_parse2 error" << errbuf;
            goto end;
        }

        // 跳過已經(jīng)解析過的數(shù)據(jù)
        inData += ret;
        // 減去已經(jīng)解析過的數(shù)據(jù)大小
        inLen -= ret;

        qDebug() << "pkt->size:" << pkt->size << "ret:" << ret;

        // 解碼
        if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
            goto end;
        }
    }
}

通過和在終端使用命令行解碼生成的 YUV 文件大小進(jìn)行比較赠叼,發(fā)現(xiàn)通過代碼解碼生成的 YUV 像素?cái)?shù)據(jù)有丟失:

$ ls -al
-rw-r--r--   1 mac  staff       110131200 Apr 12 14:22 out_640x480_yuv420p_code.yuv
-rw-r--r--   1 mac  staff       110592000 Apr 12 14:19 out_640x480_yuv420p_terminal.yuv

通過打印可以發(fā)現(xiàn)解碼結(jié)束后 parser 中還剩余 703 字節(jié)的數(shù)據(jù)沒有送入 AVPacket 中,需要讓 paeser把剩余數(shù)據(jù)“吐出來”:

pkt->size: 473 ret: 473
解碼完成第 237 幀
pkt->size: 0 ret: 703
解碼完成第 238 幀
解碼完成第 239 幀

解決辦法就是當(dāng) h264 文件中數(shù)據(jù)全部讀完后再調(diào)用一次 av_parser_parse2 函數(shù)违霞,將代碼改造如下:

// 是否讀到文件尾部
int inEnd = 0;

do {
    // 從文件中讀取h264數(shù)據(jù)
    inLen = inFile.read(inDataArray, IN_INBUF_SIZE);
    inData = inDataArray;
    inEnd = !inLen;
    while (inLen > 0 || inEnd) { // 到了文件尾部雖然沒有讀取到任何數(shù)據(jù)嘴办,也要調(diào)用,最后要刷出解析器上下文中的數(shù)據(jù) 
        ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
        if (ret < 0) {
            char errbuf[1024]; 
            av_strerror(ret, errbuf, sizeof (errbuf));
            qDebug() << "av_parser_parse2 error:" << errbuf;
            goto end;
        }

        // 跳過解析過的數(shù)據(jù)
        inData += ret;
        // 減去已解析過的數(shù)據(jù)大小
        inLen -= ret;

        qDebug() << "inEnd:" << inEnd << "pkt->size:" << pkt->size << "ret:" << ret;

        // 解碼
        if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
            goto end;
        }
        // 當(dāng)inEnd = 1時(shí)到了文件尾部
        if (inEnd) break;
    }
} while (!inEnd);

查看打印發(fā)現(xiàn) parser 中剩余數(shù)據(jù)已全部刷出买鸽,并且這次和在終端生成的 yuv 文件大小完全一樣:

inEnd: 0 pkt->size: 473 ret: 473
解碼完成第 237 幀
inEnd: 0 pkt->size: 0 ret: 703
inEnd: 1 pkt->size: 703 ret: 0
解碼完成第 238 幀
解碼完成第 239 幀
解碼完成第 240 幀

9涧郊、解碼

static int decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 發(fā)送壓縮數(shù)據(jù)到解碼器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        char errbuf[1024]; 
        av_strerror(ret, errbuf, sizeof (errbuf));
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }

    while (true) {
        // 獲取解碼后的數(shù)據(jù)
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            char errbuf[1024]; 
            av_strerror(ret, errbuf, sizeof (errbuf));
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }

        // 將解碼后的數(shù)據(jù)寫入文件
        int imgSize = av_image_get_buffer_size(ctx->pix_fmt, ctx->width, ctx->height, 1);
        outFile.write((char *) frame->data[0], imgSize);
    }
}

使用以上方式直接從 frame->data[0] 中讀取一幀大小寫入文件,你可能會(huì)發(fā)現(xiàn)播放解碼后的 YUV 像素?cái)?shù)據(jù)會(huì)有如下問題:

問題截圖

是因?yàn)?frame->data[0]frame->data[1] 以及 frame->data[1]frame->data[2] 之間是有 padding 的:

// 打印 frame->data:
qDebug() << frame->data[0] << frame->data[1] << frame->data[2];
// 輸出:
0x7fd554693000 0x7fd5546df000 0x7fd5546f2000

// 計(jì)算數(shù)據(jù)緩沖區(qū)各平面實(shí)際大醒畚濉:
frame->data[1] - frame->data[0] = 0x7fd5546df000 - 0x7fd554693000 = 311296 字節(jié) = 實(shí)際 Y 平面大小
frame->data[2] - frame->data[1] = 0x7fd5546f2000 - 0x7fd5546df000 = 77824 字節(jié) = 實(shí)際 U 平面大小

// 各平面的期望大凶彼摇:
Y 平面大小 = 640 * 480 * 1 = 307200 字節(jié)
U 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)
V 平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)

可以使用下面方式將 YUV 像素?cái)?shù)據(jù)寫入文件彤灶,yuv420p 像素格式色度分量 U 和 V 是 1/2 垂直采樣,所以高度要除以 2:

outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
outFile.write((const char *)frame->data[2], frame->linesize[2] * frame->height >> 1);

10批旺、釋放資源

inFile.close();
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);

參考鏈接:https://patchwork.ffmpeg.org/project/ffmpeg/patch/tencent_609A2E9F73AB634ED670392DD89A63400008@qq.com/

完整示例代碼:

h264_decode.pro:

macx {
    INCLUDEPATH += /usr/local/ffmpeg/include
    LIBS += -L/usr/local/ffmpeg/lib \
        -lavcodec \
        -lavutil
}

ffmpegutils.h:

#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H

extern "C" {
    #include <libavformat/avformat.h>
}

// 解碼后的YUV參數(shù)
typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat pixFmt;
    int fps;
} VideoDecodeSpec;

class FFmpegUtils
{
public:
    FFmpegUtils();
    static void h264Decode(const char *inFilename, VideoDecodeSpec &out);
};

#endif // FFMPEGUTILS_H

ffmpegutils.cpp:

#include "ffmpegutils.h"

#include <QDebug>
#include <QFile>

extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavutil/avutil.h>
    #include <libavutil/imgutils.h>
}

#define ERRBUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf))

// 輸入緩沖區(qū)大小 官方示例程序建議大小
#define IN_INBUF_SIZE 4096

FFmpegUtils::FFmpegUtils()
{

}

static int decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, QFile &outFile)
{
    int ret = 0;

    // 發(fā)送數(shù)據(jù)到解碼器
    ret = avcodec_send_packet(ctx, pkt);

    if (ret < 0) {
        ERRBUF(ret);
        qDebug() << "avcodec_send_packet error:" << errbuf;
        return ret;
    }

    while (true) {
        // 獲取解碼后的數(shù)據(jù)
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERRBUF(ret);
            qDebug() << "avcodec_receive_frame error:" << errbuf;
            return ret;
        }

        // 將解碼后的數(shù)據(jù)寫入文件
        // 寫入Y平面數(shù)據(jù)
        outFile.write((const char *)frame->data[0], frame->linesize[0] * frame->height);
        // 寫入U(xiǎn)平面數(shù)據(jù)
        outFile.write((const char *)frame->data[1], frame->linesize[1] * frame->height >> 1);
        // 寫入V平面數(shù)據(jù)
        outFile.write((const char *)frame->data[2], frame->linesize[2] * frame->height >> 1);
    }
}

void FFmpegUtils::h264Decode(const char *inFilename, VideoDecodeSpec &out)
{
    // 返回值
    int ret = 0;

    // 輸入文件(h264文件)
    QFile inFile(inFilename);
    // 輸出文件(yuv文件)
    QFile outFile(out.filename);

    // 解碼器
    AVCodec *codec = nullptr;
    // 解碼上下文
    AVCodecContext *ctx = nullptr;
    // 解析器上下文
    AVCodecParserContext *parserCtx = nullptr;
    // 存放解碼前的h264數(shù)據(jù)
    AVPacket *pkt = nullptr;
    // 存放解碼后的yuv數(shù)據(jù)
    AVFrame *frame = nullptr;

    // 存放讀取的h264文件數(shù)據(jù)
    // 加上AV_INPUT_BUFFER_PADDING_SIZE是為了防止某些優(yōu)化過的reader一次性讀取過多導(dǎo)致越界(參考了FFmpeg示例代碼)
    char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    char *inData = nullptr;
    // 輸入數(shù)據(jù)緩沖區(qū)中剩余的待解碼的數(shù)據(jù)長度
    int inLen = 0;
    // 是否讀取到了輸入文件尾部
    int inEnd = 0;

    // 獲取H264解碼器幌陕,也可以根據(jù)解碼器名稱獲取
    codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    if (!codec) {
        qDebug() << "decoder h264 not found";
        return;
    }

    // 初始化解析器上下文
    parserCtx = av_parser_init(codec->id);
    if (!parserCtx) {
        qDebug() << "av_parser_init error";
        return;
    }

    // 創(chuàng)建解碼上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        goto end;
    }

    // 創(chuàng)建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 創(chuàng)建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // 打開解碼器
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERRBUF(ret);
        qDebug() << "open decoder error:" << errbuf;
        goto end;
    }

    // 打開h264文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "open file failure:" << inFilename;
        goto end;
    }

    // 打開yuv文件
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "open file failure:" << out.filename;
    }

    do {
        // 從文件中讀取h264數(shù)據(jù)
        inLen = inFile.read(inDataArray, AUDIO_INBUF_SIZE);
        // inData指向inDataArray首元素
        inData = inDataArray;
        // 設(shè)置是否到了文件尾部
        inEnd = !inLen;

        while (inLen > 0 || inEnd) { // 到了文件尾部,雖然沒有讀取任何數(shù)據(jù)汽煮,但也要調(diào)用av_parser_parse2(修復(fù)bug)
            ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                ERRBUF(ret);
                qDebug() << "av_parser_parse2 error:" << errbuf;
                goto end;
            }

            // 跳過解析過的數(shù)據(jù)
            inData += ret;
            // 減去已解析過的數(shù)據(jù)大小
            inLen -= ret;

            // 解碼
            if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
                goto end;
            }

            // 當(dāng)inEnd = 1時(shí)到了文件尾部
            if (inEnd) break;
        }
    } while (!inEnd);

    // 刷出緩沖區(qū)中剩余數(shù)據(jù)

    // 方式一:
    decode(ctx, nullptr, frame, outFile);

    // 方式二:
    // pkt->data = nullptr;
    // pkt->size = 0;
    // decode(ctx, pkt, frame, outFile);

    // 輸出參數(shù)
    out.width = ctx->width;
    out.height = ctx->height;
    out.pixFmt = ctx->pix_fmt;
    // 用framerate.num獲取幀率搏熄,并不是time_base.den
    out.fps = ctx->framerate.num;

end:
    inFile.close();
    outFile.close();
    av_packet_free(&pkt);
    av_frame_free(&frame);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);
}

方法調(diào)用:

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QDebug>

#include <ffmpegutils.h>

extern "C" {
    #include <libavutil/imgutils.h>
}

#define IN_FILE "/Users/mac/Downloads/pic/in_640x480_yuv420p.h264"
#define OUT_FILE "/Users/mac/Downloads/pic/out_640x480_yuv420p_code.yuv"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_decodeH264Button_clicked()
{
    VideoDecodeSpec spec;
    spec.filename = OUT_FILE;
    FFmpegUtils::h264Decode(IN_FILE, spec);

    qDebug() << "寬度:" << spec.width;
    qDebug() << “高度:" << spec.height;
    qDebug() << “像素格式:" << av_get_pix_fmt_name(spec.pixFmt);
    qDebug() << “幀率:" << spec.fps;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市暇赤,隨后出現(xiàn)的幾起案子心例,更是在濱河造成了極大的恐慌,老刑警劉巖鞋囊,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件止后,死亡現(xiàn)場離奇詭異,居然都是意外死亡失暴,警方通過查閱死者的電腦和手機(jī)坯门,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逗扒,“玉大人古戴,你說我怎么就攤上這事【丶纾” “怎么了现恼?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長黍檩。 經(jīng)常有香客問我叉袍,道長,這世上最難降的妖魔是什么刽酱? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任喳逛,我火速辦了婚禮,結(jié)果婚禮上棵里,老公的妹妹穿的比我還像新娘润文。我一直安慰自己,他們只是感情好殿怜,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布典蝌。 她就那樣靜靜地躺著,像睡著了一般头谜。 火紅的嫁衣襯著肌膚如雪骏掀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音截驮,去河邊找鬼笑陈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛侧纯,可吹牛的內(nèi)容都是我干的新锈。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼眶熬,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼妹笆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起娜氏,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤拳缠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后贸弥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窟坐,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年绵疲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哲鸳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盔憨,死狀恐怖徙菠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情郁岩,我是刑警寧澤婿奔,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站问慎,受9級(jí)特大地震影響萍摊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜如叼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一冰木、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧笼恰,春花似錦片酝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽练湿。三九已至猴仑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辽俗。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工疾渣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人崖飘。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓榴捡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親朱浴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吊圾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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