代碼地址 :https://github.com/deepsadness/MediaProjectionDemo
想法來(lái)源
上一邊文章的最后說(shuō)使用錄制的Api進(jìn)行錄屏直播。本來(lái)這邊文章是預(yù)計(jì)在5月份完成的。結(jié)果過(guò)了這么久,終于有時(shí)間了。就來(lái)填坑了。
主要思路
直接使用硬件編碼器進(jìn)行錄制直播。 使用rtmp協(xié)議進(jìn)行直播推流
使用MediaProjection示意圖.png
整體流程就是通過(guò)創(chuàng)建VirtualDisplay,并且直接通過(guò)MediaCodec的Surface直接得到數(shù)據(jù)。通過(guò)MediaCodec得到編碼完成之后的數(shù)據(jù),進(jìn)行 flv格式的封裝,最后通過(guò)rtmp協(xié)議進(jìn)行發(fā)送。
獲取屏幕的截屏
1. 使用MediaCodec Surface
這部分基本上和上一遍文章相同,不同的就是使用MediaCodec來(lái)獲取Surface
@Overridepublic @NullableSurface createSurface(int width, int height) {mBufferInfo = new MediaCodec.BufferInfo();//創(chuàng)建視頻的mediaFormatMediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);//還需要對(duì)器進(jìn)行插值。設(shè)置自己設(shè)置的一些變量format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);if (VERBOSE) Log.d(TAG, "format: " + format);// 創(chuàng)建一個(gè)MediaCodec編碼器,并且使用format 進(jìn)行configure.然后將其 Get a Surface給VirtualDisplaytry {mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);mInputSurface = mEncoder.createInputSurface();//直接開(kāi)啟編碼器mEncoder.start();//...省去部分代碼return mInputSurface;} catch (IOException e) {e.printStackTrace();}return null;}
2. 獲取編碼后的數(shù)據(jù)
創(chuàng)建Encoder HanderThread 不斷獲取編碼后的數(shù)據(jù)需要在一個(gè)新的線程內(nèi)進(jìn)行。所以我們先創(chuàng)建一個(gè)HanderThread進(jìn)行異步操作和異步通行。
private void createEncoderThread() {HandlerThread encoder = new HandlerThread("Encoder");encoder.start();Looper looper = encoder.getLooper();workHanlder = new Handler(looper);}
開(kāi)始獲取數(shù)據(jù)的任務(wù) 在上面編碼器開(kāi)啟之后,直接推入一個(gè)任務(wù)運(yùn)行
//這里的1s延遲是因?yàn)殚_(kāi)啟encoder之后,硬件編碼器進(jìn)行初始化需要點(diǎn)時(shí)間workHanlder.postDelayed(new Runnable() {@Overridepublic void run() {doExtract(mEncoder,null);}, 1000);
注意是的是,這里推入任務(wù),需要稍微的延遲,因?yàn)槌跏蓟烷_(kāi)啟硬件編碼器需要一點(diǎn)時(shí)間。
/*** 不斷循環(huán)獲取,直到我們手動(dòng)結(jié)束.同步的方式* @param encoder 編碼器* @param frameCallback 獲取的回調(diào)*/private void doExtract(MediaCodec encoder,FrameCallback frameCallback) {final int TIMEOUT_USEC = 10000;long firstInputTimeNsec = -1;boolean outputDone = false;//沒(méi)有手動(dòng)停止,就只能不斷進(jìn)行while (!outputDone) {//如果手動(dòng)停止了。就結(jié)束吧if (mIsStopRequested) {Log.d(TAG, "Stop requested");return;}//因?yàn)榻o編碼器獲取狀態(tài)和喂數(shù)據(jù)的方法都直接通過(guò)Surface直接進(jìn)行了,這里只要直接獲取解碼后的狀態(tài)就可以了int decoderStatus = encoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {// no output available yet
// if (VERBOSE) Log.d(TAG, "no output from decoder available");} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {// not important for us, since we're using Surface
// if (VERBOSE) Log.d(TAG, "decoder output buffers changed");} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {//上面幾種狀態(tài),我們都可以直接忽略。這里是進(jìn)行MediaCodec開(kāi)始編碼后,會(huì)得到一個(gè)有cs-0 和cs-1的數(shù)據(jù),對(duì)應(yīng)sps和pps .獲取之后,我們后面需要處理,所以先設(shè)置成一個(gè)回調(diào)就好。MediaFormat newFormat = encoder.getOutputFormat();if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);if (frameCallback != null) {frameCallback.formatChange(newFormat);}} else if (decoderStatus < 0) {//這種情況下是出錯(cuò)了。暫時(shí)先直接出異常吧throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " +decoderStatus);} else { // decoderStatus >= 0//這里是正確獲取到編碼后的數(shù)據(jù)了if (firstInputTimeNsec != 0) {long nowNsec = System.nanoTime();Log.d(TAG, "startup lag " + ((nowNsec - firstInputTimeNsec) / 1000000.0) + " ms");firstInputTimeNsec = 0;}if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + decoderStatus +" (size=" + mBufferInfo.size + ")");//獲取到最后的數(shù)據(jù)了。這里就跳出循環(huán)。我們這個(gè)地方基本也不用用到if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {if (VERBOSE) Log.d(TAG, "output EOS");outputDone = true;}//當(dāng)size 大于0時(shí),需要送顯boolean doRender = (mBufferInfo.size != 0);//這個(gè)時(shí)候,來(lái)獲取編碼后的buffer,回調(diào)給外面if (doRender && frameCallback != null) {ByteBuffer outputBuffer = encoder.getOutputBuffer(decoderStatus);frameCallback.render(mBufferInfo, outputBuffer);}encoder.releaseOutputBuffer(decoderStatus, doRender);}}}
通過(guò)這樣的循環(huán)獲取,就可以通過(guò)回調(diào)獲取編碼后的數(shù)據(jù)了。 后面,我們可以將編碼后的數(shù)據(jù)進(jìn)行讓rtmp推流。
使用 RTMP 推流
認(rèn)識(shí) rtmp 協(xié)議 RMTP Connection 代碼
1. 認(rèn)識(shí) rtmp 協(xié)議
RTMP協(xié)議是Real Time Message Protocol(實(shí)時(shí)信息傳輸協(xié)議)的縮寫(xiě),它是由Adobe公司提出的一種應(yīng)用層的協(xié)議,用來(lái)解決多媒體數(shù)據(jù)傳輸流的多路復(fù)用(Multiplexing)和分包(packetizing)的問(wèn)題。
基于TCP 在基于傳輸層協(xié)議的鏈接建立完成后,RTMP協(xié)議也要客戶端和服務(wù)器通過(guò)“握手”來(lái)建立基于傳輸層鏈接之上的RTMP Connection鏈接。在Connection鏈接上會(huì)傳輸一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令會(huì)創(chuàng)建一個(gè)Stream鏈接,用于傳輸具體的音視頻數(shù)據(jù)和控制這些信息傳輸?shù)拿钚畔ⅰTMP協(xié)議傳輸時(shí)會(huì)對(duì)數(shù)據(jù)做自己的格式化,這種格式的消息我們稱之為RTMP Message,而實(shí)際傳輸?shù)臅r(shí)候?yàn)榱烁玫貙?shí)現(xiàn)多路復(fù)用、分包和信息的公平性,發(fā)送端會(huì)把Message劃分為帶有Message ID的Chunk,每個(gè)Chunk可能是一個(gè)單獨(dú)的Message,也可能是Message的一部分,在接受端會(huì)根據(jù)chunk中包含的data的長(zhǎng)度,message id和message的長(zhǎng)度把chunk還原成完整的Message,從而實(shí)現(xiàn)信息的收發(fā)。
2. RTMP Connection
握手(HandShake)
一個(gè)RTMP連接以握手開(kāi)始,雙方分別發(fā)送大小固定的三個(gè)數(shù)據(jù)塊
握手開(kāi)始于客戶端發(fā)送C0、C1塊。服務(wù)器收到C0或C1后發(fā)送S0和S1。 當(dāng)客戶端收齊S0和S1后,開(kāi)始發(fā)送C2。當(dāng)服務(wù)器收齊C0和C1后,開(kāi)始發(fā)送S2。 當(dāng)客戶端和服務(wù)器分別收到S2和C2后,握手完成。
image
理論上來(lái)講只要滿足以上條件,如何安排6個(gè)Message的順序都是可以的,但實(shí)際實(shí)現(xiàn)中為了在保證握手的身份驗(yàn)證功能的基礎(chǔ)上盡量減少通信的次數(shù),一般的發(fā)送順序是這樣的:
Client發(fā)送C0+C1到Sever Server發(fā)送S0+S1+S2到Client Client發(fā)送C2到Server,握手完成
建立網(wǎng)絡(luò)連接(NetConnection)
客戶端發(fā)送命令消息中的“連接”(connect)到服務(wù)器,請(qǐng)求與一個(gè)服務(wù)應(yīng)用實(shí)例建立連接。 服務(wù)器接收到連接命令消息后,發(fā)送確認(rèn)窗口大小(Window Acknowledgement Size)協(xié)議消息到客戶端,同時(shí)連接到連接命令中提到的應(yīng)用程序。 服務(wù)器發(fā)送設(shè)置帶寬(Set Peer Bandwitdh)協(xié)議消息到客戶端。 客戶端處理設(shè)置帶寬協(xié)議消息后,發(fā)送確認(rèn)窗口大小(Window Acknowledgement Size)協(xié)議消息到服務(wù)器端。 服務(wù)器發(fā)送用戶控制消息中的“流開(kāi)始”(Stream Begin)消息到客戶端。 服務(wù)器發(fā)送命令消息中的“結(jié)果”(_result),通知客戶端連接的狀態(tài)。 客戶端在收到服務(wù)器發(fā)來(lái)的消息后,返回確認(rèn)窗口大小,此時(shí)網(wǎng)絡(luò)連接創(chuàng)建完成。
服務(wù)器在收到客戶端發(fā)送的連接請(qǐng)求后發(fā)送如下信息:
image
主要是告訴客戶端確認(rèn)窗口大小,設(shè)置節(jié)點(diǎn)帶寬,然后服務(wù)器把“連接”連接到指定的應(yīng)用并返回結(jié)果,“網(wǎng)絡(luò)連接成功”。并且返回流開(kāi)始的的消息(Stream Begin 0)。
建立網(wǎng)絡(luò)流(NetStream)
客戶端發(fā)送命令消息中的“創(chuàng)建流”(createStream)命令到服務(wù)器端。 服務(wù)器端接收到“創(chuàng)建流”命令后,發(fā)送命令消息中的“結(jié)果”(_result),通知客戶端流的狀態(tài)。
推流流程
客戶端發(fā)送publish推流指令。 服務(wù)器發(fā)送用戶控制消息中的“流開(kāi)始”(Stream Begin)消息到客戶端。 客戶端發(fā)送元數(shù)據(jù)(分辨率、幀率、音頻采樣率、音頻碼率等等)。 客戶端發(fā)送音頻數(shù)據(jù)。 客戶端發(fā)送服務(wù)器發(fā)送設(shè)置塊大小(ChunkSize)協(xié)議消息。 服務(wù)器發(fā)送命令消息中的“結(jié)果”(_result),通知客戶端推送的狀態(tài)。 客戶端收到后,發(fā)送視頻數(shù)據(jù)直到結(jié)束。
推流流程
播流流程
客戶端發(fā)送命令消息中的“播放”(play)命令到服務(wù)器。 接收到播放命令后,服務(wù)器發(fā)送設(shè)置塊大小(ChunkSize)協(xié)議消息。 服務(wù)器發(fā)送用戶控制消息中的“streambegin”,告知客戶端流ID。 播放命令成功的話,服務(wù)器發(fā)送命令消息中的“響應(yīng)狀態(tài)” NetStream.Play.Start & NetStream.Play.reset,告知客戶端“播放”命令執(zhí)行成功。 在此之后服務(wù)器發(fā)送客戶端要播放的音頻和視頻數(shù)據(jù)。
播流流程
3. 代碼集成
1. 集成RTMP
直接使用librestreaming 中的RTMP的代碼,將其放到CMake中進(jìn)行編譯。
根據(jù)原來(lái)的Android.mk文件,配置CMakeList
cmake_minimum_required(VERSION 3.4.1)
add_definitions("-DNO_CRYPTO")
include_directories(${CMAKE_SOURCE_DIR}/libs/rtmp/librtmp)
#native-lib
file(GLOB PROJECT_SOURCES "${CMAKE_SOURCE_DIR}/libs/rtmp/librtmp/*.c")
add_library(rtmp-libSHAREDsrc/main/cpp/rtmp-hanlde.cpp${PROJECT_SOURCES})
find_library( # Sets the name of the path variable.log-liblog)
target_link_libraries( # Specifies the target library.rtmp-lib${log-lib})
創(chuàng)建java文件,并編寫(xiě)jni
public class RtmpClient {static {System.loadLibrary("rtmp-lib");}/*** @param url* @param isPublishMode* @return rtmpPointer ,pointer to native rtmp struct*/public static native long open(String url, boolean isPublishMode);public static native int write(long rtmpPointer, byte[] data, int size, int type, int ts);public static native int close(long rtmpPointer);public static native String getIpAddr(long rtmpPointer);
}
2. RMTP推流
之前的文章,有分析過(guò)FLV的數(shù)據(jù)格式。這樣還需要再將編碼后的數(shù)據(jù)。 這里就不贅述了。
RTMP連接部分整體的流程
連接RTMP URL 整體的連接的過(guò)程。上面的了解也有提到過(guò)。
const char *url = env->GetStringUTFChars(url_, 0);LOGD("RTMP_OPENING:%s", url);//分配RTMP對(duì)象RTMP *rtmp = RTMP_Alloc();if (rtmp == NULL) {LOGD("RTMP_Alloc=NULL");return NULL;}//初始化RTMPRTMP_Init(rtmp);int ret = RTMP_SetupURL(rtmp, const_cast<char *>(url));if (!ret) {RTMP_Free(rtmp);rtmp = NULL;LOGD("RTMP_SetupURL=ret");return NULL;}if (isPublishMode) {RTMP_EnableWrite(rtmp);}//2. 開(kāi)始Connect 。建立網(wǎng)絡(luò)連接的過(guò)程。其中包括握手ret = RTMP_Connect(rtmp, NULL);if (!ret) {RTMP_Free(rtmp);rtmp = NULL;LOGD("RTMP_Connect=ret");return NULL;}//3. create stream 建立網(wǎng)絡(luò)流的過(guò)程ret = RTMP_ConnectStream(rtmp, 0);if (!ret) {ret = RTMP_ConnectStream(rtmp, 0);RTMP_Close(rtmp);RTMP_Free(rtmp);rtmp = NULL;LOGD("RTMP_ConnectStream=ret");return NULL;}env->ReleaseStringUTFChars(url_, url);LOGD("RTMP_OPENED");
在得到MediaFormat回調(diào)時(shí),將其進(jìn)行推流發(fā)送,進(jìn)行publish 不斷得到編碼后的數(shù)據(jù),不斷推流 這兩者主要的不同,在編碼上就是type不同。我們知道第一個(gè)message必須為一個(gè)完整的message,必須為meta_data才可以。
jbyte *buffer = env->GetByteArrayElements(data_, NULL);LOGD("start write");RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));RTMPPacket_Alloc(packet, size);RTMPPacket_Reset(packet);if (type == RTMP_PACKET_TYPE_INFO) { // metadatapacket->m_nChannel = 0x03;} else if (type == RTMP_PACKET_TYPE_VIDEO) { // videopacket->m_nChannel = 0x04;} else if (type == RTMP_PACKET_TYPE_AUDIO) { //audiopacket->m_nChannel = 0x05;} else {packet->m_nChannel = -1;}RTMP *r = (RTMP *) rtmpPointer;packet->m_nInfoField2 = r->m_stream_id;LOGD("write data type: %d, ts %d", type, ts);memcpy(packet->m_body, buffer, size);packet->m_headerType = RTMP_PACKET_SIZE_LARGE;packet->m_hasAbsTimestamp = FALSE;packet->m_nTimeStamp = ts;packet->m_packetType = type;packet->m_nBodySize = size;int ret = RTMP_SendPacket((RTMP *) rtmpPointer, packet, 0);RTMPPacket_Free(packet);free(packet);env->ReleaseByteArrayElements(data_, buffer, 0);if (!ret) {LOGD("end write error %d", ret);return ret;} else {LOGD("end write success");return 0;}
最后關(guān)閉
RTMP_Close((RTMP *) rtmpPointer);RTMP_Free((RTMP *) rtmpPointer);
接受編碼后的數(shù)據(jù)回調(diào)
workHanlder.postDelayed(new Runnable() {@Overridepublic void run() {doExtract(mEncoder, new FrameCallback() {@Overridepublic void render(MediaCodec.BufferInfo info, ByteBuffer outputBuffer) {Sender.getInstance().rtmpSend(info, outputBuffer);}@Overridepublic void formatChange(MediaFormat mediaFormat) {Sender.getInstance().rtmpSendFormat(mediaFormat);}});}}, 1000);
通過(guò)回調(diào)MediaFormat
之前對(duì)flv的格式詳解,我們知道要實(shí)現(xiàn)flv推流。 需要將cs0 和cs1的頭部位置進(jìn)行推流才能正常顯示。并且必須作為第一條信息。 這里通過(guò)這方法讀取cs0 和cs1
public static byte[] generateAVCDecoderConfigurationRecord(MediaFormat mediaFormat) {ByteBuffer SPSByteBuff = mediaFormat.getByteBuffer("csd-0");SPSByteBuff.position(4);ByteBuffer PPSByteBuff = mediaFormat.getByteBuffer("csd-1");PPSByteBuff.position(4);int spslength = SPSByteBuff.remaining();int ppslength = PPSByteBuff.remaining();int length = 11 + spslength + ppslength;byte[] result = new byte[length];SPSByteBuff.get(result, 8, spslength);PPSByteBuff.get(result, 8 + spslength + 3, ppslength);/*** UB[8]configurationVersion* UB[8]AVCProfileIndication* UB[8]profile_compatibility* UB[8]AVCLevelIndication* UB[8]lengthSizeMinusOne*/result[0] = 0x01;result[1] = result[9];result[2] = result[10];result[3] = result[11];result[4] = (byte) 0xFF;/*** UB[8]numOfSequenceParameterSets* UB[16]sequenceParameterSetLength*/result[5] = (byte) 0xE1;ByteArrayTools.intToByteArrayTwoByte(result, 6, spslength);/*** UB[8]numOfPictureParameterSets* UB[16]pictureParameterSetLength*/int pos = 8 + spslength;result[pos] = (byte) 0x01;ByteArrayTools.intToByteArrayTwoByte(result, pos + 1, ppslength);return result;}
根據(jù)flv格式的分析。填充到flv中
public static void fillFlvVideoTag(byte[] dst, int pos, boolean isAVCSequenceHeader, boolean isIDR, int readDataLength) {//FrameType&CodecIDdst[pos] = isIDR ? (byte) 0x17 : (byte) 0x27;//AVCPacketTypedst[pos + 1] = isAVCSequenceHeader ? (byte) 0x00 : (byte) 0x01;//LAKETODO CompositionTimedst[pos + 2] = 0x00;dst[pos + 3] = 0x00;dst[pos + 4] = 0x00;if (!isAVCSequenceHeader) {//NALU HEADERByteArrayTools.intToByteArrayFull(dst, pos + 5, readDataLength);}}
然后發(fā)送。
發(fā)送實(shí)際數(shù)據(jù)
public static RESFlvData sendRealData(long tms, ByteBuffer realData) {int realDataLength = realData.remaining();int packetLen = Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +Packager.FLVPackager.NALU_HEADER_LENGTH +realDataLength;byte[] finalBuff = new byte[packetLen];realData.get(finalBuff, Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +Packager.FLVPackager.NALU_HEADER_LENGTH,realDataLength);int frameType = finalBuff[Packager.FLVPackager.FLV_VIDEO_TAG_LENGTH +Packager.FLVPackager.NALU_HEADER_LENGTH] & 0x1F;Packager.FLVPackager.fillFlvVideoTag(finalBuff,0,false,frameType == 5,realDataLength);RESFlvData resFlvData = new RESFlvData();resFlvData.droppable = true;resFlvData.byteBuffer = finalBuff;resFlvData.size = finalBuff.length;resFlvData.dts = (int) tms;resFlvData.flvTagType = RESFlvData.FLV_RTMP_PACKET_TYPE_VIDEO;resFlvData.videoFrameType = frameType;return resFlvData;
// dataCollecter.collect(resFlvData, RESRtmpSender.FROM_VIDEO);}
RMTP服務(wù)器
RMTP服務(wù)器的建立,可以簡(jiǎn)單的使用 RMTP服務(wù)器
總結(jié)
對(duì)比之前的一遍文章
Android PC投屏簡(jiǎn)單嘗試
獲取數(shù)據(jù)的方式 都是通過(guò)MediaProjection.createVirtualDisplay的方式來(lái)獲取截屏的數(shù)據(jù)。 不同的是,上一邊文章使用ImageReader來(lái)獲取一張一張的截圖。 而這邊文章直接是用了MediaCodec硬編碼,直接得到編碼后的h264數(shù)據(jù)。
傳輸協(xié)議 上一邊文章使用的webSocket,將得到的Bitmap的字節(jié)流,通過(guò)socket傳輸,接收方,只要接受到Socket,并且將其解析成Bitmap來(lái)展示就可以。 優(yōu)點(diǎn)是方便,而且可以自定義協(xié)議內(nèi)容。 但是缺點(diǎn)是,不能通用,必須編寫(xiě)對(duì)應(yīng)的客戶端才能完成。 這邊文章使用了rtmp的流媒體協(xié)議,優(yōu)點(diǎn)是只要支持該協(xié)議的播放器都可以直接播放我們的投屏流。
參考文章
Android實(shí)現(xiàn)錄屏直播(一)ScreenRecorder的簡(jiǎn)單分析 直播推流實(shí)現(xiàn)RTMP協(xié)議的一些注意事項(xiàng)
投屏嘗試系列文章
Android PC投屏簡(jiǎn)單嘗試- 自定義協(xié)議章(Socket+Bitmap) Android PC投屏簡(jiǎn)單嘗試(錄屏直播)2—硬解章(MediaCodec+RMTP)
?
作者:deep_sadness 鏈接:https://www.jianshu.com/p/6dde380d9b1e 來(lái)源:簡(jiǎn)書(shū) 簡(jiǎn)書(shū)著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。
總結(jié)
以上是生活随笔 為你收集整理的Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP) 的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。