摘要:在前兩篇silverlight的文章中跟大家一塊學習了silverlight的基礎知識、silverlight攝像頭麥克風的相關操作以及截圖、聲音錄制等,在文章後面也簡單的說明了為什麼沒有視訊錄制,今天就和大家一塊看一下上一節中最後的一個問題:如何使用silverlight進行視訊錄制。
主要内容:
1.nesl項目簡介
2.使用nesl實作視訊錄制
3.注意
在silverlight 中如何錄制視訊?相信這個問題有不少朋友都搜尋過,但是好像目前還沒有見到很好的答案,究其原因其實就是視訊編碼問題。當然也有朋友提到直接進行截圖,隻要每秒截取足夠多的圖檔,然後依次播放就可以形成視訊。但是我看到國外一個朋友使用此方法進行了幾十秒的視訊錄制,其檔案大小就達到了百兆級别,而且還進行了優化。是以這種方式要實作視訊錄制就目前而言還不是很合适。那麼到底有沒有好的方法呢?答案是有,但有限制,那就是借助于nesl。
native extensions for silverlight(簡稱nesl)是由微軟silverlight團隊進行開發,其目的主要為了增強silverlight out-of-browser離線應用的功能。大家都知道雖然silverlight 4的oob應用支援信任人權限提升功能,允許silverlight的oob應用對com元件的通路,但對絕大多數windows api仍舊無法調用,而nesl的出現正是為了解決這個問題。在最新的nesl 2.0中包含了大量有用的功能,而這其中就包括今天要說的視訊編碼部分。在nesl中有一個類庫microsoft.silverlight.windows.localencode.dll主要負責本地視訊和音頻編碼,這裡就是用此類庫來解決上面提到的視訊錄制問題。
在microsoft.silverlight.windows.localencode.dll中一個核心類就是encodesession,它負責音頻和視訊的編碼輸出工作。使用encodesession進行視訊錄制大概分為下面兩步:
1.準備輸入輸出資訊
在這個過程中需要定義videinputformatinfo、audioinputformatinfo、videooutputformatinfo、audiooutputformatinfo和outputcontainerinfo,然後調用encodesession.prepare()方法。
2.捕獲視訊輸出
當輸入輸出資訊準備好之後接下來就是調用encodesession.start()方法進行視訊編碼輸出。當然為了接收音頻和視訊資料必須準備兩個sink類,分别繼承于audiosink和videosink,在這兩個sink中指定capturesource,并且在對應的onsample中調用encodesession的wirtevideosample()和wirteaudiosample()接收并編碼資料(關于audiosink在前面的文章中已經說過,videosink與之類似)。
知道了encodesession的使用方法後下面就将其操作進行簡單封裝,localcamera.cs是本例中的核心類:
<code>using</code> <code>system;</code>
<code>using</code> <code>system.collections.objectmodel;</code>
<code>using</code> <code>system.io;</code>
<code>using</code> <code>system.windows;</code>
<code>using</code> <code>system.windows.threading;</code>
<code>using</code> <code>system.windows.media;</code>
<code>using</code> <code>system.windows.controls;</code>
<code>using</code> <code>system.windows.shapes;</code>
<code>using</code> <code>microsoft.silverlight.windows.localencode;</code>
<code>namespace</code> <code>cmj.myweb.mysilverlight.silverlightmeida</code>
<code>{</code>
<code> </code><code>/// <summary></code>
<code> </code><code>/// 編碼狀态</code>
<code> </code><code>/// </summary></code>
<code> </code><code>public</code> <code>enum</code> <code>encodesessionstate</code>
<code> </code><code>{</code>
<code> </code><code>start,</code>
<code> </code><code>pause,</code>
<code> </code><code>stop</code>
<code> </code><code>}</code>
<code> </code><code>/// 本地視訊對象</code>
<code> </code><code>public</code> <code>class</code> <code>localcamera</code>
<code> </code><code>private</code> <code>string</code> <code>_savefullpath =</code><code>""</code><code>;</code>
<code> </code><code>private</code> <code>uint</code> <code>_videowidth = 640;</code>
<code> </code><code>private</code> <code>uint</code> <code>_videoheight = 480;</code>
<code> </code><code>private</code> <code>videosinkextensions _videosink =</code><code>null</code><code>;</code>
<code> </code><code>private</code> <code>audiosinkextensions _audiosink=</code><code>null</code><code>;</code>
<code> </code><code>private</code> <code>encodesession _encodesession =</code><code>null</code><code>;</code>
<code> </code><code>private</code> <code>usercontrol _page =</code><code>null</code><code>;</code>
<code> </code><code>private</code> <code>capturesource _csource =</code><code>null</code><code>;</code>
<code> </code><code>public</code> <code>localcamera(usercontrol page,videoformat videoformat,audioformat audioformat)</code>
<code> </code><code>{</code>
<code> </code><code>//this._savefullpath = savefullpath;</code>
<code> </code><code>this</code><code>._videowidth = (</code><code>uint</code><code>)videoformat.pixelwidth;</code>
<code> </code><code>this</code><code>._videoheight = (</code><code>uint</code><code>)videoformat.pixelheight;</code>
<code> </code><code>this</code><code>._page = page;</code>
<code> </code><code>this</code><code>.sessionstate = encodesessionstate.stop;</code>
<code> </code><code>//this._encodesession = new encodesession();</code>
<code> </code><code>_csource =</code><code>new</code> <code>capturesource();</code>
<code> </code><code>this</code><code>.videodevice = defaultvideodevice;</code>
<code> </code><code>this</code><code>.videodevice.desiredformat = videoformat;</code>
<code> </code><code>this</code><code>.audiodevice = defaultaudiodevice;</code>
<code> </code><code>this</code><code>.audiodevice.desiredformat = audioformat;</code>
<code> </code><code>_csource.videocapturedevice =</code><code>this</code><code>.videodevice;</code>
<code> </code><code>_csource.audiocapturedevice =</code><code>this</code><code>.audiodevice;</code>
<code> </code><code>audioinputformatinfo =</code><code>new</code> <code>audioinputformatinfo() { sourcecompressiontype = formatconstants.audioformat_pcm };</code>
<code> </code><code>videoinputformatinfo =</code><code>new</code> <code>videoinputformatinfo() { sourcecompressiontype = formatconstants.videoformat_argb32 };</code>
<code> </code><code>audiooutputformatinfo =</code><code>new</code> <code>audiooutputformatinfo() { targetcompressiontype = formatconstants.audioformat_aac };</code>
<code> </code><code>videooutputformatinfo =</code><code>new</code> <code>videooutputformatinfo() { targetcompressiontype = formatconstants.videoformat_h264 };</code>
<code> </code><code>outputcontainerinfo =</code><code>new</code> <code>outputcontainerinfo() { containertype = formatconstants.transcodecontainertype_mpeg4 };</code>
<code> </code><code>}</code>
<code> </code><code>public</code> <code>localcamera(usercontrol page,videocapturedevice videocapturedevice,audiocapturedevice audiocapturedevice, videoformat videoformat, audioformat audioformat)</code>
<code> </code><code>this</code><code>.videodevice = videocapturedevice;</code>
<code> </code><code>this</code><code>.audiodevice = audiocapturedevice;</code>
<code> </code><code>public</code> <code>encodesessionstate sessionstate</code>
<code> </code><code>get</code><code>;</code>
<code> </code><code>set</code><code>;</code>
<code> </code><code>public</code> <code>encodesession session</code>
<code> </code><code>get</code>
<code> </code><code>{</code>
<code> </code><code>return</code> <code>_encodesession;</code>
<code> </code><code>}</code>
<code> </code><code>set</code>
<code> </code><code>_encodesession = value;</code>
<code> </code><code>/// <summary></code>
<code> </code><code>/// 編碼對象所在使用者控件對象</code>
<code> </code><code>/// </summary></code>
<code> </code><code>public</code> <code>usercontrol ownpage</code>
<code> </code><code>return</code> <code>_page;</code>
<code> </code><code>_page = value;</code>
<code> </code><code>/// 捕獲源</code>
<code> </code><code>public</code> <code>capturesource source</code>
<code> </code><code>return</code> <code>_csource;</code>
<code> </code><code>/// 操作音頻對象</code>
<code> </code><code>public</code> <code>audiosinkextensions audiosink</code>
<code> </code><code>return</code> <code>_audiosink;</code>
<code> </code><code>public</code> <code>static</code> <code>videocapturedevice defaultvideodevice</code>
<code> </code><code>return</code> <code>capturedeviceconfiguration.getdefaultvideocapturedevice();</code>
<code> </code>
<code> </code><code>public</code> <code>static</code> <code>readonlycollection<videocapturedevice> availablevideodevice</code>
<code> </code><code>return</code> <code>capturedeviceconfiguration.getavailablevideocapturedevices();</code>
<code> </code><code>public</code> <code>videocapturedevice videodevice</code>
<code> </code><code>public</code> <code>static</code> <code>audiocapturedevice defaultaudiodevice</code>
<code> </code><code>return</code> <code>capturedeviceconfiguration.getdefaultaudiocapturedevice();</code>
<code> </code><code>public</code> <code>static</code> <code>readonlycollection<audiocapturedevice> availableaudiodevice</code>
<code> </code><code>return</code> <code>capturedeviceconfiguration.getavailableaudiocapturedevices();</code>
<code> </code><code>public</code> <code>audiocapturedevice audiodevice</code>
<code> </code><code>private</code> <code>object lockobj =</code><code>new</code> <code>object</code><code>();</code>
<code> </code><code>internal</code> <code>videoinputformatinfo videoinputformatinfo;</code>
<code> </code><code>internal</code> <code>audioinputformatinfo audioinputformatinfo;</code>
<code> </code><code>internal</code> <code>videooutputformatinfo videooutputformatinfo;</code>
<code> </code><code>internal</code> <code>audiooutputformatinfo audiooutputformatinfo;</code>
<code> </code><code>internal</code> <code>outputcontainerinfo outputcontainerinfo;</code>
<code> </code><code>/// 視訊錄制</code>
<code> </code><code>public</code> <code>void</code> <code>startrecord()</code>
<code> </code><code>lock</code> <code>(lockobj)</code>
<code> </code><code>if</code> <code>(</code><code>this</code><code>.sessionstate == encodesessionstate.stop)</code>
<code> </code><code>{</code>
<code> </code><code>_videosink =</code><code>new</code> <code>videosinkextensions(</code><code>this</code><code>);</code>
<code> </code><code>_audiosink =</code><code>new</code> <code>audiosinkextensions(</code><code>this</code><code>);</code>
<code> </code><code>//_audiosink.volumnchange += new audiosinkextensions.volumnchangehanlder(_audiosink_volumnchange);</code>
<code> </code><code>if</code> <code>(_encodesession ==</code><code>null</code><code>)</code>
<code> </code><code>{</code>
<code> </code><code>_encodesession =</code><code>new</code> <code>encodesession();</code>
<code> </code><code>}</code>
<code> </code><code>prepareformatinfo(_csource.videocapturedevice.desiredformat, _csource.audiocapturedevice.desiredformat);</code>
<code> </code><code>_encodesession.prepare(videoinputformatinfo, audioinputformatinfo, videooutputformatinfo, audiooutputformatinfo, outputcontainerinfo);</code>
<code> </code><code>_encodesession.start(</code><code>false</code><code>, 200);</code>
<code> </code><code>this</code><code>.sessionstate = encodesessionstate.start;</code>
<code> </code><code>}</code>
<code> </code><code>/// 音量大小訓示</code>
<code> </code><code>/// <param name="sender"></param></code>
<code> </code><code>/// <param name="e"></param></code>
<code> </code><code>//void _audiosink_volumnchange(object sender, volumnchangeargs e)</code>
<code> </code><code>//{</code>
<code> </code><code>// this.ownpage.dispatcher.begininvoke(new action(() =></code>
<code> </code><code>// {</code>
<code> </code><code>// (</code>
<code> </code><code>// this.ownpage.tag as progressbar).value = e.volumn;</code>
<code> </code><code>// }));</code>
<code> </code><code>//}</code>
<code> </code><code>/// 暫停錄制</code>
<code> </code><code>public</code> <code>void</code> <code>pauserecord()</code>
<code> </code><code>this</code><code>.sessionstate = encodesessionstate.pause;</code>
<code> </code><code>_encodesession.pause();</code>
<code> </code><code>/// 停止錄制</code>
<code> </code><code>public</code> <code>void</code> <code>stoprecord()</code>
<code> </code><code>this</code><code>.sessionstate = encodesessionstate.stop;</code>
<code> </code><code>_encodesession.shutdown();</code>
<code> </code><code>_videosink =</code><code>null</code><code>;</code>
<code> </code><code>_audiosink =</code><code>null</code><code>;</code>
<code> </code><code>/// 準備編碼資訊</code>
<code> </code><code>/// <param name="videoformat"></param></code>
<code> </code><code>/// <param name="audioformat"></param></code>
<code> </code><code>private</code> <code>void</code> <code>prepareformatinfo(videoformat videoformat, audioformat audioformat)</code>
<code> </code><code>uint</code> <code>frameraterationumerator = 0;</code>
<code> </code><code>uint</code> <code>frameraterationdenominator = 0;</code>
<code> </code><code>formatconstants.frameratetoratio((</code><code>float</code><code>)math.round(videoformat.framespersecond, 2),</code><code>ref</code> <code>frameraterationumerator,</code><code>ref</code> <code>frameraterationdenominator);</code>
<code> </code><code>videoinputformatinfo.frameraterationumerator = frameraterationumerator;</code>
<code> </code><code>videoinputformatinfo.framerateratiodenominator = frameraterationdenominator;</code>
<code> </code><code>videoinputformatinfo.framewidthinpixels = _videowidth;</code>
<code> </code><code>videoinputformatinfo.frameheightinpixels = _videoheight ;</code>
<code> </code><code>videoinputformatinfo.stride = (</code><code>int</code><code>)_videowidth*-4;</code>
<code> </code><code>videooutputformatinfo.frameraterationumerator = frameraterationumerator;</code>
<code> </code><code>videooutputformatinfo.framerateratiodenominator = frameraterationdenominator;</code>
<code> </code><code>videooutputformatinfo.framewidthinpixels = videooutputformatinfo.framewidthinpixels == 0 ? (</code><code>uint</code><code>)videoformat.pixelwidth : videooutputformatinfo.framewidthinpixels;</code>
<code> </code><code>videooutputformatinfo.frameheightinpixels = videooutputformatinfo.frameheightinpixels == 0 ? (</code><code>uint</code><code>)videoformat.pixelheight : videooutputformatinfo.frameheightinpixels;</code>
<code> </code><code>audioinputformatinfo.bitspersample = (</code><code>uint</code><code>)audioformat.bitspersample;</code>
<code> </code><code>audioinputformatinfo.samplespersecond = (</code><code>uint</code><code>)audioformat.samplespersecond;</code>
<code> </code><code>audioinputformatinfo.channelcount = (</code><code>uint</code><code>)audioformat.channels;</code>
<code> </code><code>if</code> <code>(outputcontainerinfo.filepath ==</code><code>null</code> <code>|| outputcontainerinfo.filepath ==</code><code>string</code><code>.empty)</code>
<code> </code><code>_savefullpath=system.io.path.combine(environment.getfolderpath(environment.specialfolder.myvideos),</code><code>"ccamerarecordvideo.tmp"</code><code>);</code>
<code> </code><code>outputcontainerinfo.filepath = _savefullpath;</code>
<code> </code><code>//outputcontainerinfo.filepath = _savefullpath;</code>
<code> </code><code>if</code> <code>(audiooutputformatinfo.averagebitrate == 0)</code>
<code> </code><code>audiooutputformatinfo.averagebitrate = 24000;</code>
<code> </code><code>if</code> <code>(videooutputformatinfo.averagebitrate == 0)</code>
<code> </code><code>videooutputformatinfo.averagebitrate = 2000000;</code>
<code> </code><code>/// 開始捕獲</code>
<code> </code><code>public</code> <code>void</code> <code>startcaptrue()</code>
<code> </code><code>if</code> <code>(capturedeviceconfiguration.alloweddeviceaccess || capturedeviceconfiguration.requestdeviceaccess())</code>
<code> </code><code>_csource.start();</code>
<code> </code><code>/// 停止捕獲</code>
<code> </code><code>public</code> <code>void</code> <code>stopcapture()</code>
<code> </code><code>_videosink =</code><code>null</code><code>;</code>
<code> </code><code>_audiosink =</code><code>null</code><code>;</code>
<code> </code><code>_csource.stop();</code>
<code> </code><code>/// 獲得視訊</code>
<code> </code><code>/// <returns></returns></code>
<code> </code><code>public</code> <code>videobrush getvideobrush()</code>
<code> </code><code>videobrush vbrush =</code><code>new</code> <code>videobrush();</code>
<code> </code><code>vbrush.setsource(_csource);</code>
<code> </code><code>return</code> <code>vbrush;</code>
<code> </code><code>public</code> <code>rectangle getvideorectangle()</code>
<code> </code><code>rectangle rctg =</code><code>new</code> <code>rectangle();</code>
<code> </code><code>rctg.width =</code><code>this</code><code>._videowidth;</code>
<code> </code><code>rctg.height =</code><code>this</code><code>._videoheight;</code>
<code> </code><code>rctg.fill = getvideobrush();</code>
<code> </code><code>return</code> <code>rctg;</code>
<code> </code><code>/// 儲存視訊</code>
<code> </code><code>public</code> <code>void</code> <code>saverecord()</code>
<code> </code><code>if</code> <code>(_savefullpath ==</code><code>string</code><code>.empty)</code>
<code> </code><code>messagebox.show(</code><code>"尚未錄制視訊,無法進行儲存!"</code><code>,</code><code>"系統提示"</code><code>, messageboxbutton.ok);</code>
<code> </code><code>return</code><code>;</code>
<code> </code><code>savefiledialog sfd =</code><code>new</code> <code>savefiledialog</code>
<code> </code><code>filter =</code><code>"mp4 files (*.mp4)|*.mp4"</code><code>,</code>
<code> </code><code>defaultext =</code><code>".mp4"</code><code>,</code>
<code> </code><code>filterindex = 1</code>
<code> </code><code>};</code>
<code> </code><code>if</code> <code>((</code><code>bool</code><code>)sfd.showdialog())</code>
<code> </code><code>using</code> <code>(stream stm=sfd.openfile())</code>
<code> </code><code>filestream fs =</code><code>new</code> <code>filestream(_savefullpath, filemode.open, fileaccess.read);</code>
<code> </code><code>try</code>
<code> </code><code>byte</code><code>[] buffur =</code><code>new</code> <code>byte</code><code>[fs.length];</code>
<code> </code><code>fs.read(buffur, 0, (</code><code>int</code><code>)fs.length);</code>
<code> </code><code>stm.write(buffur, 0, (</code><code>int</code><code>)buffur.length);</code>
<code> </code><code>fs.close();</code>
<code> </code><code>file.delete(_savefullpath);</code>
<code> </code><code>catch</code> <code>(ioexception ioe)</code>
<code> </code><code>messagebox.show(</code><code>"檔案儲存失敗!錯誤資訊如下:"</code><code>+environment.newline+ioe.message,</code><code>"系統提示"</code><code>,messageboxbutton.ok);</code>
<code> </code><code>stm.close();</code>
<code>}</code>
當然上面說過必須有兩個sink:
<code> </code><code>public</code> <code>class</code> <code>videosinkextensions:videosink</code>
<code> </code><code>//private usercontrol _page;</code>
<code> </code><code>//private encodesession _session;</code>
<code> </code><code>private</code> <code>localcamera _localcamera;</code>
<code> </code><code>public</code> <code>videosinkextensions(localcamera localcamera)</code>
<code> </code><code>//this._page = page;</code>
<code> </code><code>this</code><code>._localcamera = localcamera;</code>
<code> </code><code>//this._session = session;</code>
<code> </code><code>this</code><code>.capturesource = _localcamera.source;</code>
<code> </code><code>protected</code> <code>override</code> <code>void</code> <code>oncapturestarted()</code>
<code> </code>
<code> </code><code>protected</code> <code>override</code> <code>void</code> <code>oncapturestopped()</code>
<code> </code><code>protected</code> <code>override</code> <code>void</code> <code>onformatchange(videoformat videoformat)</code>
<code> </code><code>protected</code> <code>override</code> <code>void</code> <code>onsample(</code><code>long</code> <code>sampletimeinhundrednanoseconds,</code><code>long</code> <code>framedurationinhundrednanoseconds,</code><code>byte</code><code>[] sampledata)</code>
<code> </code><code>if</code> <code>(_localcamera.sessionstate == encodesessionstate.start)</code>
<code> </code><code>_localcamera.ownpage.dispatcher.begininvoke(</code><code>new</code> <code>action<</code><code>long</code><code>,</code><code>long</code><code>,</code><code>byte</code><code>[]>((ts, dur, data) =></code>
<code> </code><code>_localcamera.session.writevideosample(data, data.length, ts, dur);</code>
<code> </code><code>}), sampletimeinhundrednanoseconds, framedurationinhundrednanoseconds, sampledata);</code>
<code> </code><code>public</code> <code>class</code> <code>audiosinkextensions:audiosink</code>
<code> </code><code>public</code> <code>audiosinkextensions(localcamera localcamera)</code>
<code> </code><code>protected</code> <code>override</code> <code>void</code> <code>onformatchange(audioformat audioformat)</code>
<code> </code><code>protected</code> <code>override</code> <code>void</code> <code>onsamples(</code><code>long</code> <code>sampletimeinhundrednanoseconds,</code><code>long</code> <code>sampledurationinhundrednanoseconds,</code><code>byte</code><code>[] sampledata)</code>
<code> </code><code>_localcamera.session.writeaudiosample(data, data.length, ts, dur);</code>
<code> </code><code>}), sampletimeinhundrednanoseconds, sampledurationinhundrednanoseconds, sampledata);</code>
<code> </code><code>//計算音量變化</code>
<code> </code><code>//for (int index = 0; index < sampledata.length; index += 1)</code>
<code> </code><code>//{</code>
<code> </code><code>// short sample = (short)((sampledata[index] << 8) | sampledata[index]);</code>
<code> </code><code>// float sample32 = sample / 32768f;</code>
<code> </code><code>// float maxvalue = 0;</code>
<code> </code><code>// float minvalue = 0;</code>
<code> </code><code>// maxvalue = math.max(maxvalue, sample32);</code>
<code> </code><code>// minvalue = math.min(minvalue, sample32);</code>
<code> </code><code>// float lastpeak = math.max(maxvalue, math.abs(minvalue));</code>
<code> </code><code>// float miclevel = (100 - (lastpeak * 100)) * 10;</code>
<code> </code><code>// onvolumnchange(this, new volumnchangeargs() { volumn=miclevel});</code>
<code> </code><code>//}</code>
<code> </code><code>/// 定義一個事件,回報音量變化</code>
<code> </code><code>//public delegate void volumnchangehanlder(object sender, volumnchangeargs e);</code>
<code> </code><code>//public event volumnchangehanlder volumnchange;</code>
<code> </code><code>//private void onvolumnchange(object sender, volumnchangeargs e)</code>
<code> </code><code>// if (volumnchange != null)</code>
<code> </code><code>// volumnchange(sender, e);</code>
<code> </code><code>// }</code>
<code> </code><code>//public class volumnchangeargs : eventargs</code>
<code> </code><code>//{</code>
<code> </code><code>// public float volumn</code>
<code> </code><code>// {</code>
<code> </code><code>// get;</code>
<code> </code><code>// internal set;</code>
<code> </code><code>// }</code>
<code> </code><code>//}</code>
有了這三個類,下面準備一個界面,使用localcamera進行視訊錄制操作。
需要注意的是儲存操作,事實上在encodesession中視訊的儲存路徑是在視訊錄制之前就必須指定的(當然這一點并不難了解,因為長時間的視訊錄制是會形成很大的檔案的,儲存之前緩存到記憶體中也不是很現實),在localcamera中對儲存方法的封裝事實上是檔案的讀取和删除操作。另外在這個例子中用到了前面文章中自定義的oob控件,不明白的朋友可以檢視前面的文章内容。下面是調用代碼:
<a href="http://www.cnblogs.com/kenshincui/archive/2011/11/30/2269642.html#">+ view code</a>
ok,下面是視訊錄制的截圖:
正在錄制
停止錄制後儲存
播放錄制的視訊
1.video sink和audio sink都是運作在不同于ui的各自的線程中,你可以使用ui的dispathcher或者synchronizationcontext進行不同線程之間的調用。
2.在video sink和audio sink的onsample方法中必須進行狀态判斷,因為sink執行個體建立之後就會執行onsample方法,但此時encodesession還沒有啟動是以如果不進行狀态判讀就會抛出com異常。
3.視訊的寬度和高度不能夠随意指定,這個在nesl的幫助文檔中也是特意說明的,如果任意指定同樣會抛出異常。
4.最後再次提醒大家,上面的視訊錄制是基于nesl的是以必須将應用運作到浏覽器外(oob)。