MediaMuxer+MediaCodec生成MP4視頻黑屏

發(fā)表這篇文章目的是為了記錄一次解決Android開發(fā)中遇到的問題,總結(jié)解決思路及心得.這里要特別感謝指導(dǎo)我的劉老師,新項(xiàng)目的領(lǐng)導(dǎo).

現(xiàn)象:配置(CPU)稍微偏低的手機(jī)生成視頻播放時(shí)為黑屏.
初步分析:為寫入視頻時(shí)出錯(cuò)導(dǎo)致.
分析的思路如下:

下面是音視頻混合代碼:
EncoderVideoRunnable和MediaMuxerRunnable是兩個(gè)線程,前者生成編碼后的視頻數(shù)據(jù),后者將視頻數(shù)據(jù)寫入文件.

(AiMediaMuxer.java)
private class MediaMuxerRunnable implements Runnable {

        @Override
        public void run() {
            initMuxer();
            baseTimeStamp = System.nanoTime();
            while (!isExit) {
                // 混合器沒有啟動(dòng)或數(shù)據(jù)緩存為空,則阻塞混合線程等待啟動(dòng)(數(shù)據(jù)輸入)
                if (isMuxerStarted) {
                    // 從緩存讀取數(shù)據(jù)寫入混合器中
                    if (mMuxerDatas.isEmpty()) {
//                        PaDebugUtil.i(TAG, "run--->混合器沒有數(shù)據(jù)乳幸,阻塞線程等待");
                        synchronized (lock) {
                            try {
                                lock.wait();
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    } else {
                        MuxerData data = mMuxerDatas.remove(0);
                        if (data != null) {
                            int track = 0;
                            try {
                                if (data.trackIndex == TRACK_VIDEO) {
                                    track = videoTrack;
//                                    PaDebugUtil.d(TAG, "---寫入視頻數(shù)據(jù)---");
                                } else if (data.trackIndex == TRACK_AUDIO) {
//                                    PaDebugUtil.d(TAG, "---寫入音頻數(shù)據(jù)---");
                                    track = audioTrack;
                                }
//                                PaDebugUtil.d(TAG, "before SampleData presentationTimeUs: "+data.bufferInfo.presentationTimeUs);
                                mMuxer.writeSampleData(track, data.byteBuf, data.bufferInfo);
                                prevOutputPTSUs = data.bufferInfo.presentationTimeUs;
                            } catch (Exception e) {
                                PaDebugUtil.e(TAG, "寫入數(shù)據(jù)到混合器失敗滩报,track=" + track);
                                e.printStackTrace();
                            }
                        }
                    }
                } else {
                    PaDebugUtil.i(TAG, "run--->混合器沒有啟動(dòng)欺税,阻塞線程等待");
                    synchronized (lock) {
                        try {
                            lock.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            stopMuxer();
        }
    }

其中mMuxerDatas為自定義混合器數(shù)據(jù)集合,便于MediaMuxer.writeSampleData()使用.

private Vector<MuxerData> mMuxerDatas;
/**
    * 封裝要混合器數(shù)據(jù)實(shí)體
     */
public static class MuxerData {
        int trackIndex;
        ByteBuffer byteBuf;
        MediaCodec.BufferInfo bufferInfo;

        public MuxerData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo) {
            this.trackIndex = trackIndex;
            this.byteBuf = byteBuf;
            this.bufferInfo = bufferInfo;
        }
    }

組裝數(shù)據(jù)的地方:

(EncoderVideoRunnable.java)
@SuppressLint("NewApi")
    private void encoderBytes(byte[] rawFrame) {

        ByteBuffer[] inputBuffers = mVideoEncodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mVideoEncodec.getOutputBuffers();

        //返回編碼器的一個(gè)輸入緩存區(qū)句柄跷跪,-1表示當(dāng)前沒有可用的輸入緩存區(qū)
        int inputBufferIndex = mVideoEncodec.dequeueInputBuffer(TIMES_OUT);
        if (inputBufferIndex >= 0) {
            // 綁定一個(gè)被空的摘悴、可寫的輸入緩存區(qū)inputBuffer到客戶端
            ByteBuffer inputBuffer = null;
            if (!isLollipop()) {
                inputBuffer = inputBuffers[inputBufferIndex];
            } else {
                inputBuffer = mVideoEncodec.getInputBuffer(inputBufferIndex);
            }
            // 向輸入緩存區(qū)寫入有效原始數(shù)據(jù),并提交到編碼器中進(jìn)行編碼處理
            inputBuffer.clear();
            inputBuffer.put(rawFrame);
            mVideoEncodec.queueInputBuffer(inputBufferIndex, 0, rawFrame.length, getPTSUs(), 0);
        }

        // 返回一個(gè)輸出緩存區(qū)句柄饮怯,當(dāng)為-1時(shí)表示當(dāng)前沒有可用的輸出緩存區(qū)
        // mBufferInfo參數(shù)包含被編碼好的數(shù)據(jù)蝌衔,timesOut參數(shù)為超時(shí)等待的時(shí)間
        int outputBufferIndex = -1;
        MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
        do {
            outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//        PaDebugUtil.i(TAG, "獲得編碼器輸出緩存區(qū)超時(shí)");
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // 如果API小于21,APP需要重新綁定編碼器的輸入緩存區(qū)蝌蹂;
                // 如果API大于21噩斟,則無(wú)需處理INFO_OUTPUT_BUFFERS_CHANGED
                if (!isLollipop()) {
                    outputBuffers = mVideoEncodec.getOutputBuffers();
                }
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 編碼器輸出緩存區(qū)格式改變,通常在存儲(chǔ)數(shù)據(jù)之前且只會(huì)改變一次
                // 這里設(shè)置混合器視頻軌道孤个,如果音頻已經(jīng)添加則啟動(dòng)混合器(保證音視頻同步)
                MediaFormat newFormat = mVideoEncodec.getOutputFormat();
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                if (mMuxerUtils != null) {
                    mMuxerUtils.setMediaFormat(AiMediaMuxer.TRACK_VIDEO, newFormat);
                    PaDebugUtil.i(TAG, "編碼器輸出緩存區(qū)格式改變剃允,添加視頻軌道到混合器");
                }
            } else {
                // 獲取一個(gè)只讀的輸出緩存區(qū)inputBuffer ,它包含被編碼好的數(shù)據(jù)
                ByteBuffer outputBuffer = null;
                if (!isLollipop()) {
                    outputBuffer = outputBuffers[outputBufferIndex];
                } else {
                    outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
                }
                // 如果API<=19,需要根據(jù)BufferInfo的offset偏移量調(diào)整ByteBuffer的位置
                // 并且限定將要讀取緩存區(qū)數(shù)據(jù)的長(zhǎng)度斥废,否則輸出數(shù)據(jù)會(huì)混亂
                if (isKITKAT()) {
                    outputBuffer.position(mBufferInfo.offset);
                    outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                }
                // 根據(jù)NALU類型判斷幀類型
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                int type = outputBuffer.get(4) & 0x1F;
//        PaDebugUtil.d(TAG, "------還有數(shù)據(jù)---->" + type);
                if (type == 7 || type == 8) {
//          PaDebugUtil.e(TAG, "------PPS椒楣、SPS幀(非圖像數(shù)據(jù))憔四,忽略-------");
                    mBufferInfo.size = 0;
                } else if (type == 5) {
                    // 錄像時(shí)探膊,第1秒畫面會(huì)靜止,這是由于音視軌沒有完全被添加
                    // Muxer沒有啟動(dòng)
//          PaDebugUtil.e(TAG, "------I幀(關(guān)鍵幀)-------");
                    if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//            mBufferInfo.presentationTimeUs = getPTSUs();
                        mMuxerUtils.addPreviewData(
                                new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                        isAddKeyFrame = true;
//            PaDebugUtil.e(TAG, "----------->添加關(guān)鍵幀到混合器");
                    }
                } else {
                    if (isAddKeyFrame) {
//            PaDebugUtil.d(TAG, "------非I幀(type=1)蚂且,添加到混合器-------");
                        if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//              mBufferInfo.presentationTimeUs = getPTSUs();
                            mMuxerUtils.addPreviewData(
                                    new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                            prevPresentationTimes = mBufferInfo.presentationTimeUs;
//              PaDebugUtil.d(TAG, "------添加到混合器");
                        }
                    }
                }
                // 處理結(jié)束统锤,釋放輸出緩存區(qū)資源
                mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);

                outputBuffer = null;
//        outputBuffers = null;
//        System.gc();
            }
        } while (outputBufferIndex >= 0);
    }

錄制過(guò)程中,我們發(fā)現(xiàn)黑屏的視頻在MediaMuxer.writeSampleData()方法中catch到了異常:
MediaAdapter: "pushBuffer called before start"
我們找到MediaAdapter源碼(http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/MediaAdapter.cpp)拋出異常的地方:

status_t MediaAdapter::pushBuffer(MediaBuffer *buffer) {
    if (buffer == NULL) {
        ALOGE("pushBuffer get an NULL buffer");
        return -EINVAL;
    }

    Mutex::Autolock autoLock(mAdapterLock);
    if (!mStarted) {
        ALOGE("pushBuffer called before start");
        return INVALID_OPERATION;
    }
    mCurrentMediaBuffer = buffer;
    mBufferReadCond.signal();

    ALOGV("wait for the buffer returned @ pushBuffer! %p", buffer);
    mBufferReturnedCond.wait(mAdapterLock);

    return OK;
}

這里寫明是mStarted = false的時(shí)候會(huì)拋出異常,往上查找到是調(diào)用了stop()方法后才置為false,那這里可以猜想到肯定是其他地方調(diào)用了stop()方法才導(dǎo)致的,那什么情況下會(huì)調(diào)用stop呢?
我們繼續(xù)看到adb日志里有一條:
MPEG4Writer:"do not support out of order frames (timestamp: 1892312322 < 1892312350"
我們找到MPEG4Writer源碼(http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/MPEG4Writer.cpp)拋出異常的地方:

currDurationTicks =
    ((timestampUs * mTimeScale + 500000LL) / 1000000LL -
        (lastTimestampUs * mTimeScale + 500000LL) / 1000000LL);
if (currDurationTicks < 0ll) {
    ALOGE("do not support out of order frames (timestamp: %lld < last: %lld for %s track",
            (long long)timestampUs, (long long)lastTimestampUs, trackName);
    copy->release();
    mSource->stop();
    return UNKNOWN_ERROR;
}

通過(guò)閱讀源碼,我們發(fā)現(xiàn)這個(gè)時(shí)間戳應(yīng)該是底層寫入視頻數(shù)據(jù)時(shí)的時(shí)間戳,即我們?cè)趙riteSampleData()方法中傳入的BufferInfo的presentationTimeUs的值做了一些換算.
我們實(shí)現(xiàn)視頻數(shù)據(jù)寫入的邏輯中看到,EncoderVideoRunnable線程負(fù)責(zé)將編碼好的視頻數(shù)據(jù)交給MediaMuxerRunnable線程寫入文件.初步分析應(yīng)該是BufferInfo的presentationTimeUs在什么地方被修改了,然后我們?cè)趙riteSampleData和new MediaCodec.BufferInfo()這兩個(gè)地方都打印了BufferInfo的內(nèi)存地址和presentationTimeUs,然后發(fā)現(xiàn)在寫入視頻信息的時(shí)候BufferInfo的presentationTimeUs并不是上一次寫入的時(shí)間戳,
這里插入一段邏輯:

// 向MediaMuxer添加錄屏數(shù)據(jù)
    public void addPreviewData(MuxerData data) {
        if (needAddKeyPreviewData && (data.byteBuf.get(4) & 0x1F) != 5) {
            return;
        }
        needAddKeyPreviewData = false;

        if (isStopWriteDate || isReacordingScreen) {
            return;
        }
        if (mMuxerDatas == null) {
            PaDebugUtil.e(TAG, "添加數(shù)據(jù)失敗");
            return;
        }
        data.bufferInfo.presentationTimeUs = getPTSUs();
        mMuxerDatas.add(data);
        // 解鎖
        synchronized (lock) {
            lock.notify();
        }
    }

/**
     * 獲取下一個(gè)編碼的 presentationTimeUs
     * @return
     */
    public  long getPTSUs() {
        //long result = System.nanoTime() / 1000L;
        long result = System.nanoTime();
        // presentationTimeUs should be monotonic
        // otherwise muxer fail to write
        long time = (result - pauseDelayTime) / 1000;

        if (time < prevOutputPTSUs){
            return  prevOutputPTSUs;
        }

        return time;
    }

會(huì)判斷一次當(dāng)前時(shí)間戳與上一次寫入視頻信息的時(shí)間戳做一個(gè)比較取最大值,因而prevOutputPTSUs不可能比上一次小,那么問題就出在當(dāng)前presentationTimeUs在賦值正確的時(shí)間戳后去寫入視頻信息的時(shí)候,這個(gè)presentationTimeUs被更改了,這里的BufferInfo對(duì)象其實(shí)是在EncoderVideoRunnable中創(chuàng)建的,當(dāng)EncoderVideoRunnable中dequeueOutputBuffer的時(shí)候會(huì)被更改.
常規(guī)CPU運(yùn)行情況下,這種幾率幾乎可以忽略不計(jì),但是少數(shù)性能稍微差的手機(jī)就會(huì)大概率出現(xiàn)這種情況了.
這個(gè)時(shí)候只需要在dequeueOutputBuffer的時(shí)候,每次都創(chuàng)建一個(gè)新的BufferInfo對(duì)象,這樣就不會(huì)影響寫入的時(shí)候BufferInfo的presentationTimeUs被修改了.
修改后的EncoderVideoRunnable代碼:

do {
            MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
            outputBufferIndex = mVideoEncodec.dequeueOutputBuffer(mBufferInfo, TIMES_OUT);
            if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
//        PaDebugUtil.i(TAG, "獲得編碼器輸出緩存區(qū)超時(shí)");
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // 如果API小于21毛俏,APP需要重新綁定編碼器的輸入緩存區(qū);
                // 如果API大于21饲窿,則無(wú)需處理INFO_OUTPUT_BUFFERS_CHANGED
                if (!isLollipop()) {
                    outputBuffers = mVideoEncodec.getOutputBuffers();
                }
            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 編碼器輸出緩存區(qū)格式改變煌寇,通常在存儲(chǔ)數(shù)據(jù)之前且只會(huì)改變一次
                // 這里設(shè)置混合器視頻軌道,如果音頻已經(jīng)添加則啟動(dòng)混合器(保證音視頻同步)
                MediaFormat newFormat = mVideoEncodec.getOutputFormat();
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                if (mMuxerUtils != null) {
                    mMuxerUtils.setMediaFormat(AiMediaMuxer.TRACK_VIDEO, newFormat);
                    PaDebugUtil.i(TAG, "編碼器輸出緩存區(qū)格式改變逾雄,添加視頻軌道到混合器");
                }
            } else {
                // 獲取一個(gè)只讀的輸出緩存區(qū)inputBuffer 阀溶,它包含被編碼好的數(shù)據(jù)
                ByteBuffer outputBuffer = null;
                if (!isLollipop()) {
                    outputBuffer = outputBuffers[outputBufferIndex];
                } else {
                    outputBuffer = mVideoEncodec.getOutputBuffer(outputBufferIndex);
                }
                // 如果API<=19,需要根據(jù)BufferInfo的offset偏移量調(diào)整ByteBuffer的位置
                // 并且限定將要讀取緩存區(qū)數(shù)據(jù)的長(zhǎng)度鸦泳,否則輸出數(shù)據(jù)會(huì)混亂
                if (isKITKAT()) {
                    outputBuffer.position(mBufferInfo.offset);
                    outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                }
                // 根據(jù)NALU類型判斷幀類型
                AiMediaMuxer mMuxerUtils = muxerRunnableRf.get();
                int type = outputBuffer.get(4) & 0x1F;
//        PaDebugUtil.d(TAG, "------還有數(shù)據(jù)---->" + type);
                if (type == 7 || type == 8) {
//          PaDebugUtil.e(TAG, "------PPS银锻、SPS幀(非圖像數(shù)據(jù)),忽略-------");
                    mBufferInfo.size = 0;
                } else if (type == 5) {
                    // 錄像時(shí)辽故,第1秒畫面會(huì)靜止徒仓,這是由于音視軌沒有完全被添加
                    // Muxer沒有啟動(dòng)
//          PaDebugUtil.e(TAG, "------I幀(關(guān)鍵幀)-------");
                    if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//            mBufferInfo.presentationTimeUs = getPTSUs();
                        mMuxerUtils.addPreviewData(
                                new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                        prevPresentationTimes = mBufferInfo.presentationTimeUs;
                        isAddKeyFrame = true;
//            PaDebugUtil.e(TAG, "----------->添加關(guān)鍵幀到混合器");
                    }
                } else {
                    if (isAddKeyFrame) {
//            PaDebugUtil.d(TAG, "------非I幀(type=1),添加到混合器-------");
                        if (mMuxerUtils != null && mMuxerUtils.isMuxerStarted()) {
//              mBufferInfo.presentationTimeUs = getPTSUs();
                            mMuxerUtils.addPreviewData(
                                    new AiMediaMuxer.MuxerData(AiMediaMuxer.TRACK_VIDEO, outputBuffer, mBufferInfo));
                            prevPresentationTimes = mBufferInfo.presentationTimeUs;
//              PaDebugUtil.d(TAG, "------添加到混合器");
                        }
                    }
                }
                // 處理結(jié)束誊垢,釋放輸出緩存區(qū)資源
                mVideoEncodec.releaseOutputBuffer(outputBufferIndex, false);

                outputBuffer = null;
//        outputBuffers = null;
//        System.gc();
            }
        } while (outputBufferIndex >= 0);

其實(shí)只是在dequeueOutputBuffer前每次都創(chuàng)建新的BufferInfo.
改完運(yùn)行,發(fā)現(xiàn)問題解決了,呼呼...

最后,總結(jié)一下從發(fā)現(xiàn)問題到解決問題的全過(guò)程:
1,遇到問題不要覺得太難還沒開始就放棄思考,如果最后沒有解決問題,但是分析思路的養(yǎng)成也是非常重要.
2,盡量多分析源碼,對(duì)解決問題事半功倍.
3,代碼大忌生搬硬套,網(wǎng)上大手也有寫bug的情況,代碼抄過(guò)來(lái)要分析每一步的邏輯,養(yǎng)成好的編碼習(xí)慣.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掉弛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子喂走,更是在濱河造成了極大的恐慌殃饿,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芋肠,死亡現(xiàn)場(chǎng)離奇詭異乎芳,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)帖池,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門奈惑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人睡汹,你說(shuō)我怎么就攤上這事肴甸。” “怎么了囚巴?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵原在,是天一觀的道長(zhǎng)友扰。 經(jīng)常有香客問我,道長(zhǎng)庶柿,這世上最難降的妖魔是什么村怪? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮浮庐,結(jié)果婚禮上甚负,老公的妹妹穿的比我還像新娘。我一直安慰自己兔辅,他們只是感情好腊敲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著维苔,像睡著了一般碰辅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上介时,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天没宾,我揣著相機(jī)與錄音,去河邊找鬼沸柔。 笑死循衰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的褐澎。 我是一名探鬼主播会钝,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼工三!你這毒婦竟也來(lái)了迁酸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤俭正,失蹤者是張志新(化名)和其女友劉穎奸鬓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掸读,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡串远,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了儿惫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澡罚。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肾请,靈堂內(nèi)的尸體忽然破棺而出始苇,到底是詐尸還是另有隱情,我是刑警寧澤筐喳,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布催式,位于F島的核電站,受9級(jí)特大地震影響避归,放射性物質(zhì)發(fā)生泄漏荣月。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一梳毙、第九天 我趴在偏房一處隱蔽的房頂上張望哺窄。 院中可真熱鬧,春花似錦账锹、人聲如沸萌业。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)生年。三九已至,卻和暖如春廓奕,著一層夾襖步出監(jiān)牢的瞬間抱婉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工桌粉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蒸绩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓铃肯,卻偏偏與公主長(zhǎng)得像患亿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子押逼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354