Android硬编解码MediaCodec解析从猪肉餐馆的故
上篇回顾:
上一篇文章Android硬编解码MediaCodec解析从猪肉餐馆的故事讲起(一)已经叙述了MediaCodec工作流程和工作周期状态机,今天开始进入实战,从代码角度详细解析MediaCodec。如果没有看过上篇,建议还是看下才能和本文无缝衔接。MediaCodec代码实例
本次讲解的代码实例是Google官方MediaCodec的学习项目grafika,grafika由多个demo组成,比如视频解码播放、实时录制视频并将视频编码为H264保存本地,录屏等功能,每个demo都有会侧重于某项技术。
以下为grafika的App首页,每一项代表一个demo:
今天,我们就从最基本的第一个demo讲起解码一个本地MP4视频。
从gif可以看出,这是一个非常简单的视频,整个功能就是对mp4视频进行解码,然后将解码后的数据渲染到屏幕,对应的代码在com。android。grafika。PlayMovieActivity,基本流程结构图如下:
那么最核心的解码代码都在MoviePlayer中。解复用代码解析
首先要明白的概念是复用,也可以叫做封装,即将已经压缩编码的视频数据和音频数据按照一定的格式打包到一起,比如热爱看片的我们都很熟悉的MP4,MKV,RMVB,TS,FLV,AVI,就是复用格式。
比如FLV格式的数据,是由H。264编码的视频码流和AAC编码的音频码流打包一起。
FLV复用格式是由一个FLVHeader文件头和一个一个的Tag组成的。Tag中包含了音频数据以及视频数据。FLV的结构如下图所示(图来源于视音频数据处理入门:FLV封装格式解析
C学习资料免费获取方法:关注音视频开发T哥,点击下方链接即可免费获取2023年最新C音视频开发进阶独家学习资料!
资料包链接
那么在解码视频之前,就必须先将H264视频数据从复用格式中取出来,Android平台已经提供了MediaExtractor这个工具让我们方便地进行解复用。
以下是官网提供的MediaExtractor使用代码模板:MediaExtractorextractornewMediaExtractor();extractor。setDataSource(。。。);intnumTracksextractor。getTrackCount();遍历媒体复用文件中的每一条轨道数据流(音频或者视频流),得到我们需要处理的数据流的mime类型,并选中它for(inti0;inumTracks;i){MediaFormatformatextractor。getTrackFormat(i);Stringmimeformat。getString(MediaFormat。KEYMIME);if(weAreInterestedInThisTrack){选中我们需要处理的数据流的mime类型的数据流extractor。selectTrack(i);}}ByteBufferinputBufferByteBuffer。allocate(。。。)循环读取选中的音频或者视频流到inputBuffer中while(extractor。readSampleData(inputBuffer,。。。)0){inttrackIndexextractor。getSampleTrackIndex();longpresentationTimeUsextractor。getSampleTime();。。。extractor。advance();}extractor。release();extractornull;
注释已经写的比较详细了,基本能看懂。
首先了解下MediaFormat,它是一个专门描述媒体文件格式的类,内部通过一系列键值对来描述媒体格式,比如通用的媒体格式KEY:
视频专有的格式KEY:
音频专有的格式KEY:
在上面的模板代码中,就是取了KEYMIME对应的值来判断媒体文件类型。
而常见的视频的mime就有以下:
videoxvnd。on2。vp8VP8video(i。e。videoin。webm)videoxvnd。on2。vp9VP9video(i。e。videoin。webm)videoavcH。264AVCvideovideohevcH。265HEVCvideovideomp4vesMPEG4videovideo3gppH。263video
因为现在讲的编码主要是H264,而H264对应的mine就是videoavc。
在grafika中的MoviePlayer的构造方法中com。android。grafika。MoviePlayerMoviePlayer,就是通过MediaExtractor来获取视频的宽高:解复用MediaExtractorextractornull;try{extractornewMediaExtractor();传入视频文件的路径extractor。setDataSource(sourceFile。toString());inttrackIndexselectTrack(extractor);if(trackIndex0){thrownewRuntimeException(NovideotrackfoundinmSourceFile);}选中得到的轨道(视频轨道),即后面都是对此轨道的处理extractor。selectTrack(trackIndex);通过该轨道的MediaFormat得到对视频对应的宽高MediaFormatformatextractor。getTrackFormat(trackIndex);Log。d(TAG,extractor。getTrackFormatformatformat);视频对应的宽高mVideoWidthformat。getInteger(MediaFormat。KEYWIDTH);mVideoHeightformat。getInteger(MediaFormat。KEYHEIGHT);if(VERBOSE){Log。d(TAG,VideosizeismVideoWidthxmVideoHeight);}}finally{if(extractor!null){extractor。release();}}
在具体的播放视频方法com。android。grafika。MoviePlayerplay中,通过获取到的mime类型来创建一个MediaCodec解码器:MediaFormatformatextractor。getTrackFormat(trackIndex);Log。d(TAG,EgetTrackFormatformat:format);CreateaMediaCodecdecoder,andconfigureitwiththeMediaFormatfromtheextractor。ItsveryimportanttousetheformatfromtheextractorbecauseitcontainsacopyoftheCSD0CSD1codecspecificdatachunks。Stringmimeformat。getString(MediaFormat。KEYMIME);Log。d(TAG,createDecoderByTypemime:mime);通过视频mime类型初始化解码器MediaCodecdecoderMediaCodec。createDecoderByType(mime);
此时MediaCodec处于Stopped状态中的Uninitialized状态,接下来开始启动MediaCodec(老板收拾厨房桌椅,要开店了):配置解码器,指定MediaFormat以及视频输出的Surface,解码器进入configure状态decoder。configure(format,mOutputSurface,null,0);启动解码器,开始进入Executing状态Immediatelyafterstart()thecodecisintheFlushedsubstate,whereitholdsallthebuffersdecoder。start();具体的解码流程doExtract(extractor,trackIndex,decoder,mFrameCallback);
注意到configure方法传了mOutputSurface的Surface对象,在Android硬编解码利器MediaCodec解析从猪肉餐馆的故事讲起(一)讲过,对于原始视频数据来说:
视频编解码支持三种色彩格式,其中第二种就是nativerawvideoformat:COLORFormatSurface,可以用来处理surface模式的数据输入输出。而这个Surface对象是从Activity的TextureView获取到的:MoviePlayer通过Surface将解码后的原始视频数据渲染到TextureView上SurfaceTexturestmTextureView。getSurfaceTexture();SurfacesurfacenewSurface(st);MoviePlayerplayernull;try{playernewMoviePlayer(newFile(getFilesDir(),mMovieFiles〔mSelectedMovie〕),surface,callback);}catch(IOExceptionioe){Log。e(TAG,Unabletoplaymovie,ioe);surface。release();return;}解码代码解析
此时MediaCodec已经启动,此时已经进入input端和output端的大循环阶段(头脑中开始想象采购员一次又一次将生猪肉装进篮子中交给厨师,厨师做完又放在盘子上送给顾客的循环的场景)。关键代码看com。android。grafika。MoviePlayerdoExtract:Workloop。Weexecutehereuntilwerunoutofvideooraretoldtostop。privatevoiddoExtract(MediaExtractorextractor,inttrackIndex,MediaCodecdecoder,FrameCallbackframeCallback){Weneedtostrikeabalancebetweenprovidinginputandreadingoutputthatoperatesefficientlywithoutdelaysontheoutputside。Toavoiddelaysontheoutputside,weneedtokeepthecodecsinputbuffersfed。TherecanbesignificantlatencybetweensubmittingframeNtothedecoderandreceivingframeNontheoutput,soweneedtostayaheadofthegame。Manyvideodecodersseemtowantseveralframesofvideobeforetheystartproducingoutputoneimplementationwantedfourbeforeitappearedtoconfigureitself。Weneedtoprovideabunchofinputframesupfront,andtrytokeepthequeuefullaswego。(Noteitspossiblefortheencodeddatatobewrittentothestreamoutoforder,sowecantgenerallysubmitasingleframeandwaitforittoappear。)Wecantjustfixateontheinputsidethough。Ifwespendtoomuchtimetryingtostufftheinput,wemightmissapresentationdeadline。At60Hzwehave16。7msbetweenframes,sosleepingfor10mswouldeatupasignificantfractionofthetimeallowed。(Mostvideoisat30Hzorless,soformostcontentwellhavesignificantlylonger。)Waitingforoutputisokay,butsleepingonavailabilityofinputbuffersisunwiseifweneedtobeprovidingoutputonaregularschedule。Insomesituations,startuplatencymaybeaconcern。Tominimizestartuptime,wedwanttostufftheinputfullasquicklyaspossible。Thisturnsouttobesomewhatcomplicated,asthecodecmaystillbestartingupandwillrefusetoacceptinput。RemovingthetimeoutfromdequeueInputBuffer()resultsinspinningontheCPU。Ifyouhavetightstartuplatencyrequirements,itwouldprobablybebesttoprimethepumpwithasequenceofframesthatarentactuallyshown(e。g。grabthefirst10NALunitsandshovethemthrough,thenrewindtothestartofthefirstkeyframe)。Theactuallatencyseemstodependonstronglyonthenatureofthevideo(e。g。resolution)。Oneconceptuallyniceapproachistoloopontheinputsidetoensurethatthecodecalwayshasalltheinputitcanhandle。Aftersubmittingabuffer,weimmediatelychecktoseeifitwillacceptanother。Wecanuseashorttimeoutsowedontmissapresentationdeadline。Ontheoutputsideweonlycheckonce,withalongertimeout,thenreturntotheouterlooptoseeifthecodecishungryformoreinput。Inpractice,everycalltocheckforavailablebuffersinvolvesalotofmessagepassingbetweenthreadsandprocesses。Settingaverybrieftimeoutdoesntexactlyworkbecausetheoverheadrequiredtodeterminethatnobufferisavailableissubstantial。Ononedevice,thecleverapproachcausedsignificantlygreaterandmorehighlyvariablestartuplatency。Thecodebelowtakesaverysimplemindedapproachthatworks,butcarriesariskofoccasionallyrunningoutofoutput。Amoresophisticatedapproachmightdetectanoutputtimeoutandusethatasasignaltotrytoenqueueseveralinputbuffersonthenextiteration。Ifyouwanttoexperiment,settheVERBOSEflagtotrueandwatchthebehaviorinlogcat。Uselogcatvthreadtimetoseesubsecondtiming。获取解码输出数据的超时时间finalintTIMEOUTUSEC0;输入ByteBuffer数组(较高版本的MediaCodec已经用getInputBuffer取代了,可直接获取buffer)ByteBuffer〔〕decoderInputBuffersdecoder。getInputBuffers();记录传入了第几块数据intinputChunk0;用于log每帧解码时间longfirstInputTimeNsec1;booleanoutputDonefalse;booleaninputDonefalse;while(!outputDone){if(VERBOSE)Log。d(TAG,loop);if(mIsStopRequested){Log。d(TAG,Stoprequested);return;}Feedmoredatatothedecoder。if(!inputDone){拿到可用的ByteBuffer的indexintinputBufIndexdecoder。dequeueInputBuffer(TIMEOUTUSEC);if(inputBufIndex0){if(firstInputTimeNsec1){firstInputTimeNsecSystem。nanoTime();}根据index得到对应的输入ByteBufferByteBufferinputBufdecoderInputBuffers〔inputBufIndex〕;Log。d(TAG,decoderInputBuffersinputBuf:inputBuf,inputBufIndex:inputBufIndex);ReadthesampledataintotheByteBuffer。ThisneitherrespectsnorupdatesinputBufsposition,limit,etc。从媒体文件中读取的一个sample数据大小intchunkSizeextractor。readSampleData(inputBuf,0);if(chunkSize0){文件读到末尾,设置标志位,发送一个空帧,给后面解码知道具体结束位置EndofstreamsendemptyframewithEOSflagset。Whenyouqueueaninputbufferwiththeendofstreammarker,thecodectransitionstotheEndofStreamsubstate。Inthisstatethecodecnolongeracceptsfurtherinputbuffers,butstillgeneratesoutputbuffersuntiltheendofstreamisreachedontheoutput。decoder。queueInputBuffer(inputBufIndex,0,0,0L,MediaCodec。BUFFERFLAGENDOFSTREAM);Log。d(TAG,queueInputBuffer);inputDonetrue;if(VERBOSE)Log。d(TAG,sentinputEOS);}else{if(extractor。getSampleTrackIndex()!trackIndex){Log。w(TAG,WEIRD:gotsamplefromtrackextractor。getSampleTrackIndex(),expectedtrackIndex);}得到当前数据的播放时间点longpresentationTimeUsextractor。getSampleTime();把inputBufIndex对应的数据传入MediaCodecdecoder。queueInputBuffer(inputBufIndex,0,chunkSize,presentationTimeUs,0flags);Log。d(TAG,queueInputBufferinputBufIndex:inputBufIndex);if(VERBOSE){Log。d(TAG,submittedframeinputChunktodec,sizechunkSize);}记录传入了第几块数据inputChunk;extractor读取游标往前挪动extractor。advance();}}else{if(VERBOSE)Log。d(TAG,inputbuffernotavailable);}}if(!outputDone){如果解码成功,则得到解码出来的数据的buffer在输出buffer中的index。并将解码得到的buffer的相关信息放在mBufferInfo中。如果不成功,则得到的是解码的一些状态intoutputBufferIndexdecoder。dequeueOutputBuffer(mBufferInfo,TIMEOUTUSEC);Log。d(TAG,dequeueOutputBufferdecoderBufferIndex:outputBufferIndex,mBufferInfo:mBufferInfo);if(outputBufferIndexMediaCodec。INFOTRYAGAINLATER){nooutputavailableyetif(VERBOSE)Log。d(TAG,nooutputfromdecoderavailable);}elseif(outputBufferIndexMediaCodec。INFOOUTPUTBUFFERSCHANGED){notimportantforus,sincewereusingSurfaceif(VERBOSE)Log。d(TAG,decoderoutputbufferschanged);}elseif(outputBufferIndexMediaCodec。INFOOUTPUTFORMATCHANGED){MediaFormatnewFormatdecoder。getOutputFormat();if(VERBOSE)Log。d(TAG,decoderoutputformatchanged:newFormat);}elseif(outputBufferIndex0){thrownewRuntimeException(unexpectedresultfromdecoder。dequeueOutputBuffer:outputBufferIndex);}else{decoderStatus0if(firstInputTimeNsec!0){Logthedelayfromthefirstbufferofinputtothefirstbufferofoutput。longnowNsecSystem。nanoTime();Log。d(TAG,startuplag((nowNsecfirstInputTimeNsec)1000000。0)ms);firstInputTimeNsec0;}booleandoLoopfalse;if(VERBOSE)Log。d(TAG,surfacedecodergivenbufferoutputBufferIndex(outputmBufferInfosizemBufferInfo。size));判断是否到了文件结束,上面设置MediaCodec。BUFFERFLAGENDOFSTREAM标志位在这里判断if((mBufferInfo。flagsMediaCodec。BUFFERFLAGENDOFSTREAM)!0){if(VERBOSE)Log。d(TAG,outputEOS);if(mLoop){doLooptrue;}else{outputDonetrue;}}如果解码得到的buffer大小大于0,则需要渲染booleandoRender(mBufferInfo。size!0);AssoonaswecallreleaseOutputBuffer,thebufferwillbeforwardedtoSurfaceTexturetoconverttoatexture。Wecantcontrolwhenitappearsonscreen,butwecanmanagethepaceatwhichwereleasethebuffers。if(doRenderframeCallback!null){渲染前的回调,这里具体实现是通过一定时长的休眠来尽量确保稳定的帧率frameCallback。preRender(mBufferInfo。presentationTimeUs);}得到输出Buffer数组,较高版本已经被getOutputBuffer代替ByteBuffer〔〕decoderOutputBuffersdecoder。getOutputBuffers();Log。d(TAG,ecoderOutputBuffers。length:decoderOutputBuffers。length);将输出buffer数组的第outputBufferIndex个buffer绘制到surface。doRender为true绘制到配置的surfacedecoder。releaseOutputBuffer(outputBufferIndex,doRender);if(doRenderframeCallback!null){渲染后的回调frameCallback。postRender();}if(doLoop){Log。d(TAG,ReachedEOS,looping);需要循环的话,重置extractor的游标到初始位置。extractor。seekTo(0,MediaExtractor。SEEKTOCLOSESTSYNC);inputDonefalse;重置decoder到Flushed状态,不然就没法开始新一轮播放YoucanmovebacktotheFlushedsubstateatanytimewhileintheExecutingstateusingflush()。YoucanmovebacktotheFlushedsubstateatanytimewhileintheExecutingstateusingflush()decoder。flush();resetdecoderstateframeCallback。loopReset();}}}}}
代码有官方和我加上的详细注释,这里主要挑几个重点讲下:
1。采购员向厨师询问有无篮子可用:首先询问Mediacodec当前有没有可以input的Buffer可以使用:intinputBufIndexdecoder。dequeueInputBuffer(TIMEOUTUSEC);
方法定义是:Returnstheindexofaninputbuffertobefilledwithvaliddataor1ifnosuchbufferiscurrentlyavailable。ThismethodwillreturnimmediatelyiftimeoutUs0,waitindefinitelyfortheavailabilityofaninputbufferiftimeoutUs0orwaituptotimeoutUsmicrosecondsiftimeoutUs0。paramtimeoutUsThetimeoutinmicroseconds,anegativetimeoutindicatesinfinite。throwsIllegalStateExceptionifnotintheExecutingstate,orcodecisconfiguredinasynchronousmode。throwsMediaCodec。CodecExceptionuponcodecerror。publicfinalintdequeueInputBuffer(longtimeoutUs){intresnativedequeueInputBuffer(timeoutUs);if(res0){synchronized(mBufferLock){validateInputByteBuffer(mCachedInputBuffers,res);}}returnres;}
TIMEOUTUSEC为等待超时时间。当返回的inputBufIndex大于等于0,则说明当前有可用的Buffer,此时inputBufIndex表示可用Buffer在Mediacodec中的序号。如果等待了TIMEOUTUSEC时间还没找到可用的Buffer,则返回inputBufIndex小于0,等下次循环再来取Buffer。
2。采购员将生猪肉装进篮子中并交给厨师:每次从MediaExtractor中的readSampleData方法读出视频一段数据放在ByteBuffer中,然后通过Mediacodec的queueInputBuffer将ByteBuffer传给Mediacodec内部处理。从媒体文件中读取的一个sample数据大小到inputBuf中intchunkSizeextractor。readSampleData(inputBuf,0);
方法定义:Retrievethecurrentencodedsampleandstoreitinthebytebufferstartingatthegivenoffset。pbNote:bAsofAPI21,onsuccessthepositionandlimitof{codebyteBuf}isupdatedtopointtothedatajustread。parambyteBufthedestinationbytebufferreturnthesamplesize(or1ifnomoresamplesareavailable)。publicnativeintreadSampleData(NonNullByteBufferbyteBuf,intoffset);
Android硬编解码MediaCodec解析从猪肉餐馆的故事讲起(一)中讲过,根据官网描述,一般如果是视频文件数据,则都不要传递给Mediacodec不是完整帧的数据,除非是标记了BUFFERFLAGPARTIALFRAME的数据。所以这里可以推断readSampleData方法是读取一帧的数据,后面我会对其进行验证。
返回值为读取到数据大小,所以如果返回值大于0,则说明是有读取到数据的,则将数据传入MediaCodec中:得到当前数据的播放时间点longpresentationTimeUsextractor。getSampleTime();把inputBufIndex对应的数据传入MediaCodecdecoder。queueInputBuffer(inputBufIndex,0,chunkSize,presentationTimeUs,0flags);
关于queueInputBuffer方法,定义的注释实在太长了,简单来说,这里就是将input端第inputBufIndex个Buffer从第0位开始chunkSize个字节数据传入MediaCodec中,并指定这一帧数据的渲染时间为presentationTimeUs,在解析H264视频编码原理从孙艺珍的电影说起(一)曾经说过
这里由于B帧的引入,会导致一个现象,就是编码的帧顺序和播放的帧顺序会不一致,所以也衍生了pts和dts2个时间戳(编码时间和播放时间)
这里的presentationTimeUs就是pts,因为解码后的帧数据可能不是和播放顺序一样的,需要presentationTimeUs来指定播放顺序。最后一个参数flags是对传入的数据描述用的标志位,一般用于一些特殊情况,这里传0即可。
如果readSampleData方法返回值,即读到的数据大小为负数,则说明已经读到视频文件尾部了,则还是调用queueInputBuffer方法,但是需要特殊处理:decoder。queueInputBuffer(inputBufIndex,0,0,0L,MediaCodec。BUFFERFLAGENDOFSTREAM);
发送一个空帧,标志位传BUFFERFLAGENDOFSTREAM,告诉MediaCodec,已经到文件尾部了,这个文件没有剩下需要传的数据了,即采购员告诉厨师,已经没有生猪肉了。
发送了这个表示结束的空帧之后,就不能再传数据给input端了,一直到MediaCodec进入了flushed状态,或者进入stopped之后再start之后才可以重新传入数据给input端。
input端的代码就到这,然后马不停蹄,立刻到ouptut端去尝试获取一下output的buffer(顾客走到厨师面前,问猪肉炒好了没有):intoutputBufferIndexdecoder。dequeueOutputBuffer(mBufferInfo,TIMEOUTUSEC);
如果不成功(厨师对顾客说猪肉还没炒好),则得到的是解码的一些状态,在项目代码中,列出了以下几种常见的状态:
1。MediaCodec。INFOTRYAGAINLATER:表示等了TIMEOUTUSEC时间长,也暂时还没有解码出成功的数据。一般来说,一个是等待时间还不够,另一个就是输入端是B帧,需要后面一帧P帧来作为参考帧才可以解码(关于B帧P帧详见解析H264视频编码原理从孙艺珍的电影说起(一))
2。MediaCodec。INFOOUTPUTBUFFERSCHANGED:输出Buffer数组已经过时,需要及时更换,由于较新版本已经用getOutputBuffer获取输出Buffer了,所以该标志位也过时了。
3。MediaCodec。INFOOUTPUTFORMATCHANGED:输出数据的MediaFormat发生了变化。
如果解码成功,则得到解码出来的数据的buffer在输出buffer中的index。并将解码得到的buffer的相关信息放在mBufferInfo中。然后执行非常关键的一段代码:decoder。releaseOutputBuffer(outputBufferIndex,doRender);
将输出buffer数组的第outputBufferIndex个buffer绘制到surface(还记得configure方法传了的Surface对象么)。doRender为true,绘制到配置的surface。可以理解这行代码就类似Android中Canvas的draw方法,调用就绘制一帧,并将Buffer回收。总结
美好的时光总是如此短暂,我觉得解码的关键代码应该已经讲得比较细致了吧
为了避免篇幅过长导致读者看了容易打瞌睡,我还是先到此为止把,下一篇博文Android硬编解码工具MediaCodec解析从猪肉餐馆的故事讲起(三)将讲解本文代码运行后的一些要点和注意细节,敬请关注参考:
视音频数据处理入门:FLV封装格式解析MediaCodec官网
安卓解码器MediaCodec解析
作者:半岛铁盒里的猫链接:https:juejin。cnpost7111340889691127815来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在开发的路上你不是一个人,欢迎加入C音视频开发交流群链接大家庭讨论交流!
给告别一个准备人为什么会流泪,大概是因为眼睛代替了嘴巴说不出的悲伤。最近太多仓促的告别太多悲伤太多泪水。就算是那些寿终正寝的永别,我们任何时候都还远远没有准备好。死亡,即使是到了越来越开明的年代
原创诗词鉴赏我的生活也是头条依然活着的人眼睛在看着什么,看的那么多看不到自己。头脑在想着什么,想的那么多却全是自己。幽幽坟头,行行铭文,忆了一生往事,庆幸我依然活着。依然活着的人沉醉于纷扰的快
卿若不离,我便不弃原创前言天空下起了雨,一人行走在熙熙攘攘的红尘都市中,心头几近默然,看着那雨中匆匆而过的路人,我与你相遇,又与你擦肩而过。总是想留住那片刻的停留,可是无论我怎么样等待,依旧是一个孤零的
金句摘抄1。多难殷忧兴国运,动心忍性希前哲。2。生逢盛世,我们享受着新时代的和平阳光,书写着新时代的奋斗华章。3。雷霆万钧壮志不改,沧海横流方显英雄气概。4。真相不是一块橡皮泥,可以随意揉
1世界国家古巴篇古巴位于拉丁美洲的北部,包括古巴岛和青年岛等大小岛屿位于墨西哥湾的出口处,像一把钥匙插入墨西哥湾。古巴地形整体以平原为主,西北中部和东南有较大的地形起伏区,主要为较平缓的丘陵低山。
闹市中的宁静丨古德寺原创七月风心性好古,普度以德,是为古德。古德寺总览古德寺,位于湖北武汉市汉口黄浦路上滑坡74号,清光绪三年(1877年)由隆希创建,初名古德茅蓬。清宣统三年(1911年),古德寺僧
这个冬天,大同邀你来过年!这份游玩攻略请收好除了你知道的夏清凉还有你不知道的冬康养又到岁末迎新春古都大同冬季文旅盛宴开新篇2022年底以欢乐中国年地道大同味为主题的2023年大同市迎新春文化系列活动正式启幕精心组织策划的八大
哈尔滨冰雪大世界推出童话风文创雪糕,与游客共赴冰雪之约!为了让广大游客拥有更加沉浸的游玩体验,哈尔滨冰雪大世界携手中华老字号马迭尔,推出联名限定文创雪糕,与游客共赴冰雪之约。充满童话气质的哈尔滨冰雪大世界,令无数游客着迷,置身其中,更有
点亮生命的灯盏(札记74)过去的,过不去的,最终也都会过去。那些你想不通,看不透,理不清,忘不掉,放不下的往事,到最后,岁月都会替你轻描淡写。你熬得过山重水复,岁月自会赠你柳暗花明。1。贾平凹一席话,道尽活
不敢想象,如果我的孩子丢了,我该怎么办2023陪孩子成长的每一天不敢想象,如果我的孩子丢了,我该怎么办,我的生活会变成什么样子。在市民广场玩的时候,她一直吵着要一个新的泡泡机。临走之前我和她爸同意了,走到摊贩那里准备给
1月份起,退休老人养老金能统一上涨100元吗?这三类情况可以实现时间进入了2023年的1月份,部分地区的退休老人养老金迎来了发放。由于春节放假时间的关系,上海天津山东等地的退休人员,都会在春节前领到1月份的养老金。从1月份开始,有部分老人的养老