《Android音视频系列-6》FFmpeg视频文件推流

阅读本文需要一点JNI基础~

通过本文可以学到如下知识:

  • JNI 回调封装
  • 视频推流大概流程

如果还没搭建直播服务器,看上一篇文章
搭建直播服务器Nginx+rtmp,如此简单(mac)

上一篇直播服务器搭建好了,也测试推流拉流都是成功的,这一篇将在手机上将一个mp4文件推流到服务器,然后可以通过拉流软件看直播。

1、cmake 配置

# 需要引入我们头文件,以这个配置的目录为基准
include_directories(src/main/jniLibs/include)

# 添加共享库(so)搜索路径
LINK_DIRECTORIES(${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi)

AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp/push PUSH_SRC_LIST)

add_library(
        # 编译生成的库的名称叫 push_handle,对应System.loadLibrary("push_handle");
        push_handle
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        ${PUSH_SRC_LIST}
)

target_link_libraries(
        push_handle
        # 编解码(最重要的库)
        avcodec-57
        # 设备信息
        avdevice-57
        # 滤镜特效处理库
        avfilter-6
        # 封装格式处理库
        avformat-57
        # 工具库(大部分库都需要这个库的支持)
        avutil-55
        # 后期处理
        postproc-54
        # 音频采样数据格式转换库
        swresample-2
        # 视频像素数据格式转换
        swscale-4
        # 链接 android ndk 自带的一些库
        android
        # Links the target library to the log library
        # included in the NDK.
        # 链接 OpenSLES
        OpenSLES
        log)

最终生成的so叫 libpush_handle.so

FFmpeg的so跟include文件如下,跟之前的文章一样。


2、Java层推流管理类

public class PushHandle {

    static {
        System.loadLibrary("push_handle");
    }

    private PushCallback pushCallback;

    public void setCallback(PushCallback pushCallback) {
        this.pushCallback = pushCallback;
    }


    public native int nPushRtmpFile(String filePath, String rtmp_url);

    public native int nStopPush();


    /**native回调*/
    private void onInfo(long pts, long dts, long duration, long index) {
        if (pushCallback != null) {
            pushCallback.onInfo(pts, dts, duration, index);
        }
    }

    private void onError(int code, String msg) {
        if (pushCallback != null) {
            pushCallback.onError(code, msg);
        }
    }

    private void onPushComplete() {
        if (pushCallback != null) {
            pushCallback.onPushComplete();
        }
    }

}

public native int nPushRtmpFile(String filePath, String rtmp_url); 推流方法就传一个视频文件路径和推流地址,然后就等回调

回调比较简单

public interface PushCallback {

    //回调推流每一帧信息
    void onInfo(long pts, long dts, long duration, long index);

    //错误回调
    void onError(int code, String msg);

    //推流完成
    void onPushComplete();
}

3、Native层

在/src/main/cpp/push目录下定义一个 PushHandle.cpp的c++文件,
然后同步一下,鼠标依次放在PushHandle.java 的 两个native方法上,按 option+enter(win 是alt+enter),自动生成对应的native方法实现,然后就开始写c++代码了。

3.1 JNI回调封装

在当前文件目录新建一个c++ file,名字就叫 PushJniCall,
然后会生成对应的PushJniCall.h头文件和PushJniCall.cpp文件

PushJniCall.h
#ifndef FFMPEGDEMO_PUSHJNICALL_H
#define FFMPEGDEMO_PUSHJNICALL_H

#include <jni.h>

class PushJniCall {
public:
    JNIEnv *jniEnv;

    //定义java的对象和方法id,在cpp构造函数赋值
    jobject jPushCallbackObj;
    jmethodID jOnErrorMid;
    jmethodID jOnInfoMid;
    jmethodID jOnPushCompleteMid;

public:
    PushJniCall(JNIEnv *jniEnv,jobject jPushCallbackObj);
    ~PushJniCall();

public:

    //定义回调方法,在cpp实现
    void callOnError(int code, char *msg);

    void callOnPushComplete();

    void callOnInfo(int64_t pts, int64_t dts, int64_t duration, long long index);
};

#endif //FFMPEGDEMO_PUSHJNICALL_H
实现类 PushJniCall.cpp
#include "PushJniCall.h"

PushJniCall::PushJniCall(JNIEnv *jniEnv, jobject jPushCallbackObj) {

    this->jniEnv = jniEnv;
    //需要创建一个全局应用
    this->jPushCallbackObj = jniEnv->NewGlobalRef(jPushCallbackObj);
    jclass cls = jniEnv->GetObjectClass(jPushCallbackObj);

    //获取方法id
    jOnInfoMid = jniEnv->GetMethodID(cls, "onInfo", "(JJJJ)V");
    jOnErrorMid = jniEnv->GetMethodID(cls, "onError", "(ILjava/lang/String;)V");
    jOnPushCompleteMid = jniEnv->GetMethodID(cls, "onPushComplete", "()V");

}

PushJniCall::~PushJniCall() {
    jniEnv->DeleteGlobalRef(jPushCallbackObj);
}

void PushJniCall::callOnError(int code, char *msg) {
    //C中的char*转化为JNI中的jstring
    jniEnv->CallVoidMethod(jPushCallbackObj, jOnErrorMid, code, msg);

}

void PushJniCall::callOnPushComplete() {
    jniEnv->CallVoidMethod(jPushCallbackObj, jOnPushCompleteMid);

}

void PushJniCall::callOnInfo(int64_t pts, int64_t dts, int64_t duration, long long index) {
    jniEnv->CallVoidMethod(jPushCallbackObj, jOnInfoMid, (jlong) pts, (jlong) dts,
                           (jlong) duration, (jlong) index);

}

这些属于JNI基础,应该能看懂,看不懂就去找一篇JNI基础文章看下就行。
[传送门]

3.2 推流核心代码

PushHandle.cpp

推流代码参考文末链接,加了一些注释

#include <jni.h>
#include "PushJniCall.h"
#include "PushStatus.h"
#include "log.h"

//ffmpeg 是c写的,要用c的include
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//引入时间
#include "libavutil/time.h"
};

#include <iostream>

using namespace std;

//JNI回调处理
PushJniCall *pushJniCall;
//状态处理
PushStatus *pushStatus;

int avError(int errNum) {
    char buf[1024];
    //获取错误信息
    av_strerror(errNum, buf, sizeof(buf));

    LOGE("发生异常:%s",buf);
    if (pushJniCall != NULL) {
        pushJniCall->callOnError(errNum, buf);
    }
    return errNum;
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_lanshifu_ffmpegdemo_push_PushHandle_nPushRtmpFile(JNIEnv *env, jobject instance,
                                                           jstring path_, jstring rtmp_url) {

    pushJniCall = new PushJniCall(env, instance);
    pushStatus = new PushStatus();

    const char *inUrl = env->GetStringUTFChars(path_, 0);
    const char *outUrl = env->GetStringUTFChars(rtmp_url, 0);

    LOGW("nPushRtmpFile,inUrl = %s", inUrl);
    LOGW("nPushRtmpFile,outUrl = %s", outUrl);

    int videoindex = -1;
    /// 1.使用FFmpeg之前要调用av_register_all和avformat_network_init
    //初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
    av_register_all();
    //初始化网络库
    avformat_network_init();


    //////////////////////////////////////////////////////////////////
    //                   输入流处理部分
    /////////////////////////////////////////////////////////////////


    //输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
    AVFormatContext *ictx = NULL;
    AVFormatContext *octx = NULL;

    AVPacket pkt;
    int ret = 0;

    try {
        ///2.打开文件,解封文件头
        ret = avformat_open_input(&ictx, inUrl, 0, NULL);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        LOGD("avformat_open_input success!");
        ///3.获取音频视频的信息 .h264 flv 没有头信息
        ret = avformat_find_stream_info(ictx, 0);
        if (ret != 0) {
            avError(ret);
            throw ret;
        }
        LOGD("avformat_find_stream_info success!");

        av_dump_format(ictx, 0, inUrl, 0);

        //////////////////////////////////////////////////////////////////
        //                   输出流处理部分
        /////////////////////////////////////////////////////////////////

        /// 4.创建输出上下文,如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
        ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        LOGD("avformat_alloc_output_context2 success!");

        ///遍历输入流列表,一般有音频流和视频流,为输出内容添加音视频流
        for (int i = 0; i < ictx->nb_streams; i++) {
            //获取输入视频流,可能是音频流,视频流?
            AVStream *in_stream = ictx->streams[i];
            ///为输出内容添加一个音视频流,格式什么的跟输入流保持一致。
            AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
            LOGD("avformat_new_stream %d, success!", i);
            if (!out_stream) {
                LOGE("未能成功添加音视频流 %d", i);
                ret = AVERROR_UNKNOWN;
                throw ret;
            }
            //这个不知道有什么用,注释掉吧,不影响结果
//            if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
//                out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
//            }
            /// 输入流参数拷贝到输出流
            ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
            if (ret < 0) {
                LOGD("copy 编解码器上下文失败\n");
            }
            out_stream->codecpar->codec_tag = 0;

            ///记录视频流的位置
            if (videoindex == -1 && ictx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoindex = i;
                LOGD("找到视频流的位置 %d,", videoindex);
            }
        }


        //打印输出的格式信息
        av_dump_format(octx, 0, outUrl, 1);
        LOGD("准备推流...");

        //////////////////////////////////////////////////////////////////
        //                   准备推流
        /////////////////////////////////////////////////////////////////

        ///打开IO
        ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_READ_WRITE);
        // todo :Linux服务器地址报错 IO error,本地服务器不会
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        LOGD("打开IO avio_open success!");
        ///写入头部信息
        ret = avformat_write_header(octx, 0);
        if (ret < 0) {
            avError(ret);
            throw ret;
        }
        LOGD("写入头部信息 avformat_write_header Success!");
        //推流每一帧数据
        //int64_t pts  [ pts*(num/den)  第几秒显示]
        //int64_t dts  解码时间 [P帧(相对于上一帧的变化) I帧(关键帧,完整的数据) B帧(上一帧和下一帧的变化)]  有了B帧压缩率更高。
        //获取当前的时间戳  微妙
        long long start_time = av_gettime();
        long long frame_index = 0;
        LOGD("开始推流 >>>>>>>>>>>>>>>");
        while (!pushStatus->isExit) {
            //输入输出视频流
            AVStream *in_stream, *out_stream;
            ///不断读取每一帧数据
            ret = av_read_frame(ictx, &pkt);
            if (ret < 0) {
                //数据读完,播放完成
                break;
            }

            /*
            PTS(Presentation Time Stamp)显示播放时间
            DTS(Decoding Time Stamp)解码时间
            */
            //没有显示时间(比如未解码的 H.264 )
            if (pkt.pts == AV_NOPTS_VALUE) {
                //AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
                AVRational time_base1 = ictx->streams[videoindex]->time_base;

                //计算两帧之间的时间
                /*
                r_frame_rate 基流帧速率  (不是太懂,先知道流程就行)
                av_q2d 转化为double类型
                */
                int64_t calc_duration = (double) AV_TIME_BASE / av_q2d(ictx->streams[videoindex]->r_frame_rate);

                //配置参数
                pkt.pts = (double) (frame_index * calc_duration) / (double) (av_q2d(time_base1) * AV_TIME_BASE);
                pkt.dts = pkt.pts;
                pkt.duration = (double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
            }

            ///通过睡眠的方式保持播放时间同步
            if (pkt.stream_index == videoindex) {
                AVRational time_base = ictx->streams[videoindex]->time_base;
                AVRational time_base_q = {1, AV_TIME_BASE};
                //计算视频播放时间,比如在11s播放
                int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
                //计算实际视频的播放时间,比如已经播放了10s
                int64_t now_time = av_gettime() - start_time;
                if (pts_time > now_time) {
                    //睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
                    av_usleep((unsigned int) (pts_time - now_time));
                }
            }

            //输入输出视频流赋值
            in_stream = ictx->streams[pkt.stream_index];
            out_stream = octx->streams[pkt.stream_index];

            //计算延时后,重新指定时间戳,调函数即可
            pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                    (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                    (AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base,out_stream->time_base);
            //字节流的位置,-1 表示不知道字节流位置
            pkt.pos = -1;

            if (pkt.stream_index == videoindex) {
                //当前视频帧数
                frame_index++;
            }
            //回调数据
            if (pushJniCall != NULL) {
                pushJniCall->callOnInfo(pkt.pts, pkt.dts, pkt.duration, frame_index);
            }

            ///把一帧数据写到输出流(推流)
            ret = av_interleaved_write_frame(octx, &pkt);

            if (ret < 0) {
                LOGE("推流失败 ret=%d",ret);
                break;
            }

        }
        ret = 0;
    } catch (int errNum) {
        if (pushJniCall != NULL) {
            pushJniCall->callOnError(errNum, const_cast<char *>("出错了"));
        }
    }

    if (pushJniCall != NULL) {
        pushJniCall->callOnPushComplete();
    }

    LOGD("推流结束》》》");
    //关闭资源
    if (octx != NULL){
        avio_close(octx->pb);
        octx = NULL;
    }
    //释放输出封装上下文
    if (octx != NULL) {
        avformat_free_context(octx);
    }
    //关闭输入上下文
    if (ictx != NULL) {
        avformat_close_input(&ictx);
        ictx = NULL;
    }
    //释放
    av_packet_unref(&pkt);

    env->ReleaseStringUTFChars(path_, outUrl);
    return ret;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_lanshifu_ffmpegdemo_push_PushHandle_nStopPush(JNIEnv *env, jobject instance) {
    LOGD("nStopPush");
    if (pushStatus != NULL){
        pushStatus->isExit = true;
    }
}

其中,日志封装了一个 log.h,如下

#ifndef _LOG_H
#define _LOG_H

#define LOGN (void) 0

#ifndef WIN32
#include <android/log.h>

#define LOG_VERBOSE     1
#define LOG_DEBUG       2
#define LOG_INFO        3
#define LOG_WARNING     4
#define LOG_ERROR       5
#define LOG_FATAL       6
#define LOG_SILENT      7

#ifndef LOG_TAG
//#define LOG_TAG __FILE__
#define LOG_TAG "JNI_TAG"
#endif

#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_VERBOSE
#endif

#define LOGP(level, fmt, ...) \
        __android_log_print(level, LOG_TAG, "%s:" fmt, \
            __PRETTY_FUNCTION__, ##__VA_ARGS__)

#if LOG_VERBOSE >= LOG_LEVEL
#define LOGV(fmt, ...) \
        LOGP(ANDROID_LOG_VERBOSE, fmt, ##__VA_ARGS__)
#else
#define LOGV(...) LOGN
#endif

#if LOG_DEBUG >= LOG_LEVEL
#define LOGD(fmt, ...) \
        LOGP(ANDROID_LOG_DEBUG, fmt, ##__VA_ARGS__)
#else
#define LOGD(...) LOGN
#endif

#if LOG_INFO >= LOG_LEVEL
#define LOGI(fmt, ...) \
        LOGP(ANDROID_LOG_INFO, fmt, ##__VA_ARGS__)
#else
#define LOGI(...) LOGN
#endif

#if LOG_WARNING >= LOG_LEVEL
#define LOGW(fmt, ...) \
        LOGP(ANDROID_LOG_WARN, fmt, ##__VA_ARGS__)
#else
#define LOGW(...) LOGN
#endif

#if LOG_ERROR >= LOG_LEVEL
#define LOGE(fmt, ...) \
        LOGP(ANDROID_LOG_ERROR, fmt, ##__VA_ARGS__)
#else
#define LOGE(...) LOGN
#endif

#if LOG_FATAL >= LOG_LEVEL
#define LOGF(fmt, ...) \
        LOGP(ANDROID_LOG_FATAL, fmt, ##__VA_ARGS__)
#else
#define LOGF(...) LOGN
#endif

#if LOG_FATAL >= LOG_LEVEL
#define LOGA(condition, fmt, ...) \
    if (!(condition)) \
    { \
        __android_log_assert(condition, LOG_TAG, "(%s:%u) %s: error:%s " fmt, \
            __FILE__, __LINE__, __PRETTY_FUNCTION__, condition, ##__VA_ARGS__); \
    }
#else
#define LOGA(...) LOGN
#endif

#else
#include <stdio.h>

#define LOGP(fmt, ...) printf("%s line:%d " fmt, __FILE__, __LINE__, ##__VA_ARGS__)

#define LOGV(fmt, ...) LOGP(fmt, ##__VA_ARGS__)

#define LOGD(fmt, ...) LOGP(fmt, ##__VA_ARGS__)

#define LOGI(fmt, ...) LOGP(fmt, ##__VA_ARGS__)

#define LOGW(fmt, ...) LOGP(fmt, ##__VA_ARGS__)

#define LOGE(fmt, ...) LOGP(fmt, ##__VA_ARGS__)

#define LOGF(fmt, ...) LOGP(fmt, ##__VA_ARGS__)

#define LOGA(...) LOGN

#endif // ANDROID_PROJECT

#endif // _LOG_H

PushStatus 主要是管理状态,比如退出的时候要取消推流,将标志位改成true

PushStatus.h

#ifndef FFMPEGDEMO_PLAYERSTATUS_H
#define FFMPEGDEMO_PLAYERSTATUS_H

class PushStatus {
public:
    /**
     * 是否退出,打算用这个变量来做退出(销毁)
     */
    bool isExit = false;

};

#endif //FFMPEGDEMO_PLAYERSTATUS_H

PushStatus.cpp

#include "PushStatus.h"

最终Activity中调用

点击推流按钮开个子线程执行

    private void filePushRunable() {
        mPushHandle = new PushHandle();
        mPushHandle.setCallback(new PushCallback() {
            @Override
            public void onInfo(final long pts, final long dts, final long duration, final long index) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        StringBuilder sb = new StringBuilder();
                        sb.append("pts: ").append(pts).append("\n");
                        sb.append("dts: ").append(dts).append("\n");
                        sb.append("duration: ").append(duration).append("\n");
                        sb.append("index: ").append(index).append("\n");
                        tvPushInfo.setText(sb.toString());
                    }
                });

            }

            @Override
            public void onError(final int code, final String msg) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tvPushInfo.setText("失败,code=" + code + ",msg=" + msg);
                    }
                });
            }

            @Override
            public void onPushComplete() {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tvPushInfo.setText("推流完成");
                    }
                });

            }
        });

        String videoPath = "/sdcard/input1.mp4";
        mPushHandle.nPushRtmpFile(videoPath, "rtmp://192.168.43.144:1935/test/live");
    }

打印日志


可以看到推流是成功的,拉流试试

嗯,是成功的,2019 AG超玩会回归了,中单老帅认识吧,哈哈。


总结一下推流的大概流程:

  • 打开输入文件,解封文件头,avformat_open_input
  • 获取输入音视频文件的信息 avformat_find_stream_info
  • 创建输出流上下文 avformat_alloc_output_context2
  • 添加音视频流 avformat_new_stream
  • 输入流参数拷贝到输出流 avcodec_parameters_copy
  • 打开推流地址 avio_open
  • 写入头部信息 avformat_write_header
  • 不断读取输入视频文件的每一帧 av_read_frame
  • 没有显示时间的帧添加时间
  • 同步视频记录的时间和实际播放时间
  • 更正时间戳
  • 推流 av_interleaved_write_frame

代码中可能有些不太好理解,没关系,只要这个流程清楚了就行,后面文章再深入一点理解FFmpeg。

下一篇介绍摄像头推流,可以结合OpenGL,添加滤镜和水印,应该会比较有意思。


参考
https://www.jianshu.com/p/dcac5da8f1da

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

推荐阅读更多精彩内容