<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title>李成银的技术随笔</title>
        <link>https://welefen.com</link>
        <description>李成银的随笔</description>
        <atom:link href="https://welefen.com/rss.html" rel="self" />
        <language>zh-cn</language>
        <lastBuildDate>Sun, 15 Mar 2026 07:25:33 GMT</lastBuildDate>
        <item>
            <title>如何把动态 canvas 转成视频</title>
            <link>https://welefen.com/post/convert-canvas-to-video.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#capturestreamapi">captureStream API</a></li>
<li><a href="#toc-88f">MediaRecorder 支持的 mimeType</a></li>
<li><a href="#toc-bcc">视频转码和添加音频</a></li>
<li><a href="#toc-8eb">canvas2video 库</a></li>
</ul>
</div><p>随着短视频的越来越流行，有时候需要把动态的 canvas 转换为视频然后发布到对应的平台上。当然最简单的办法就是利用录屏软件（如：QuickTime）直接将屏幕录制下来，然后通过一些视频编辑软件（如：imovie）加工处理下。</p>
<p>作为一名很懒的程序员，当然是希望能够自动转换为视频，一气呵成，不需要手工来处理。你还别说，现在的浏览器还真提供了对应的 API 来处理这个事情，本文就来简单介绍下。</p>
<!--more-->

<h2><a id="capturestreamapi" class="anchor" href="#capturestreamapi"></a>captureStream API</h2>
<p>通过 canvas 的 <code>captureStream</code> API，就可以把 canvas 变成一个 stream，然后结合 MediaRecorder API 就可以转换为 video。</p>
<pre><code class="language-js">const stream = canvas.captureStream();
const recorder = new MediaRecorder(stream, { mimeType: &#39;video/webm&#39; });
const data = [];
recorder.ondataavailable = function (event) {
  if (event.data &amp;&amp; event.data.size) {
    data.push(event.data);
  }
};
recorder.onstop = () =&gt; {
  const url = URL.createObjectURL(new Blob(data, { type: &#39;video/webm&#39; }));
  console.log(&#39;video url&#39;, url)
};</code></pre>
<p>通过开始的时候调用 <code>recorder.start()</code> 和结束的时候调用 <code>recorder.stop()</code> 即可完成视频的录制工作。</p>
<p>注意：如果一个 canvas 操作完成后面不在变动的话，录制的时候会发现最终的视频并没有想要的长度。这是因为如果 canvas 不动的话后续就不再录制了。</p>
<p>解决办法也很简单，找到 canvas 中的一个元素，然后循环改变这个元素的透明度就可以（为了减少对视觉的影响，可以让透明度在 0.99 和 1 之间变化，这个目的就是让 canvas 一直在操作。当然用其他的方案也是可以）</p>
<h2><a id="toc-88f" class="anchor" href="#toc-88f"></a>MediaRecorder 支持的 mimeType</h2>
<p>通过 <code>MediaRecorder</code> 来录制视频时，需要指定录制视频的编码格式，如：<code>video/webm</code>，<code>vide/mp4</code>。 如果当前浏览器不支持的话则会报错。</p>
<p>可以先通过 <code>MediaRecorder.isTypeSupported</code> 方法检测浏览器是否支持，如：</p>
<pre><code class="language-js">MediaRecorder.isTypeSupported(&#39;video/webm&#39;);
MediaRecorder.isTypeSupported(&#39;video/mp4&#39;);</code></pre>
<p>从目前的测试来看，<code>video/webm</code> 格式 chrome v81/firefox v76 都支持。但 <code>video/mp4</code> 目前都还不支持。</p>
<p>由于 webm 格式现在还没有那么流行，有些网站还不支持直接支持上传这个格式的视频，那么就需要把 webm 转换为 mp4 格式的视频。</p>
<p>通过测试发现，虽然 chrome v81 还不能直接支持 <code>video/mp4</code> 的格式，但已经支持了 <code>video/webm;codecs=h264</code>，也就是录制的视频是用 h264 格式来编码的。这样后续转码的时候视频部分可以直接拷贝，大大提升转码的速度。</p>
<h2><a id="toc-bcc" class="anchor" href="#toc-bcc"></a>视频转码和添加音频</h2>
<p>通过这个方式录制的视频没有音频部分，而一般发布的视频都希望有音频作为 BGM。此时就可以通过 WebAssembly 版本的 ffmpeg 来完成了。</p>
<p>转换命令如下：</p>
<pre><code class="language-sh">ffmpeg -i video.webm output.mp4; //转换格式
ffmpeg -i video.webm -i audio.mp3 -c:v copy -af apad -map 0:v -map 1:a -shortest  out.mp4; // 添加音频</code></pre>
<p>具体的可以通过 <a href="https://github.com/ffmpegwasm/ffmpeg.wasm">ffmpeg.wasm</a> 库来完成，大致代码如下：</p>
<pre><code class="language-js">private async convertVideoUrl(url: string): Promise&lt;string&gt; {
    const { audio, outVideoType, mimeType, workerOptions, transcodeOptions, concatDemuxerOptions } = this.config;
    const { createFFmpeg } = window.FFmpeg;
    const ffmpeg = createFFmpeg(workerOptions || {});
    await ffmpeg.load();
    const type = mimeType.split(&#39;;&#39;)[0].split(&#39;/&#39;)[1];
    await ffmpeg.write(`video.${type}`, url);

    if (audio) {
      const audioType = audio.split(&#39;.&#39;).pop();
      await ffmpeg.write(`1.${audioType}`, audio);
      await ffmpeg.run(`-i video.${type} -i 1.${audioType} ${concatDemuxerOptions} out.${outVideoType}`);
    } else {
      if (type !== outVideoType) {
        await ffmpeg.transcode(`video.${type} `, `out.${outVideoType}`, transcodeOptions);
      }
    }
    const data  = await ffmpeg.read(`out.${outVideoType}`);
    const blob = new Blob([data.buffer], { type: `video/${outVideoType}` })
    const mp4Url = URL.createObjectURL(blob);
    return mp4Url;
  }</code></pre>
<h2><a id="toc-8eb" class="anchor" href="#toc-8eb"></a>canvas2video 库</h2>
<p>为了方面使用，已经将录制视频和转换视频等操作封装成了一个库 <a href="https://github.com/welefen/canvas2video">canvas2video</a> 。代码不多，功能也比较聚焦，可以直接使用。</p>

            ]]></description>
            <pubDate>Wed, 23 Sep 2020 06:45:04 GMT</pubDate>
            <guid>https://welefen.com/post/convert-canvas-to-video.html</guid>
        </item>
        <item>
            <title>2020跑步目标：365公里，最新：265公里</title>
            <link>https://welefen.com/post/2020-run.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-1f8">12月（20公里）</a></li>
<li><a href="#toc-898">11月（15公里）</a></li>
<li><a href="#toc-7a8">10月（25公里）</a></li>
<li><a href="#toc-397">9月（55公里）</a></li>
<li><a href="#toc-998">8月（10公里）</a></li>
<li><a href="#toc-f2f">7月（30公里）</a></li>
<li><a href="#toc-578">6月（40公里）</a></li>
<li><a href="#toc-158">5月（45公里）</a></li>
<li><a href="#toc-528">4月（5公里）</a></li>
<li><a href="#toc-da8">1月（20公里）</a></li>
</ul>
</div><p>2019年完成跑步300KM，2020年定的目标是365KM。因为疫情耽误了几个月，不过还是要尽力完成。</p>
<!--more-->

<h2><a id="toc-1f8" class="anchor" href="#toc-1f8"></a>12月（20公里）</h2>
<ul>
<li><code>12.17</code> - <code>10公里</code> - <code>家</code></li>
<li><code>12.15</code> - <code>5公里</code> - <code>家</code></li>
<li><code>12.12</code> - <code>5公里</code> - <code>家</code></li>
</ul>
<h2><a id="toc-898" class="anchor" href="#toc-898"></a>11月（15公里）</h2>
<ul>
<li><code>11.25</code> - <code>5公里</code> - <code>家</code></li>
<li><code>11.21</code> - <code>5公里</code> - <code>家</code></li>
<li><code>11.15</code> - <code>5公里</code> - <code>家</code></li>
</ul>
<h2><a id="toc-7a8" class="anchor" href="#toc-7a8"></a>10月（25公里）</h2>
<ul>
<li><code>10.30</code> - <code>5公里</code> - <code>家</code></li>
<li><code>10.25</code> - <code>5公里</code> - <code>朝阳公园</code></li>
<li><code>10.24</code> - <code>5公里</code> - <code>家</code></li>
<li><code>10.19</code> - <code>7公里</code> - <code>家</code></li>
<li><code>10.17</code> - <code>3公里</code> - <code>朝阳公园</code></li>
</ul>
<h2><a id="toc-397" class="anchor" href="#toc-397"></a>9月（55公里）</h2>
<ul>
<li><code>9.26</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>9.19</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>9.17</code> - <code>5公里</code> - <code>798</code></li>
<li><code>9.15</code> - <code>10公里</code> - <code>798</code></li>
<li><code>9.12</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>9.5</code> - <code>10公里</code> - <code>朝阳公园</code></li>
</ul>
<h2><a id="toc-998" class="anchor" href="#toc-998"></a>8月（10公里）</h2>
<ul>
<li><code>8.30</code> - <code>10公里</code> - <code>朝阳公园</code></li>
</ul>
<h2><a id="toc-f2f" class="anchor" href="#toc-f2f"></a>7月（30公里）</h2>
<ul>
<li><code>7.19</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>7.12</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>7.5</code> - <code>10公里</code> - <code>朝阳公园</code></li>
</ul>
<h2><a id="toc-578" class="anchor" href="#toc-578"></a>6月（40公里）</h2>
<ul>
<li><code>6.26</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>6.21</code> - <code>5公里</code> - <code>朝阳公园</code></li>
<li><code>6.20</code> - <code>5公里</code> - <code>朝阳公园</code></li>
<li><code>6.14</code> - <code>5公里</code> - <code>朝阳公园</code></li>
<li><code>6.10</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>6.5</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>6.3</code> - <code>5公里</code> - <code>360健身房</code></li>
</ul>
<h2><a id="toc-158" class="anchor" href="#toc-158"></a>5月（45公里）</h2>
<ul>
<li><code>5.26</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>5.23</code> - <code>10公里</code> - <code>朝阳公园</code></li>
<li><code>5.21</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>5.19</code> - <code>7公里</code> - <code>360健身房</code></li>
<li><code>5.15</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>5.13</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>5.10</code> - <code>5公里</code> - <code>朝阳公园</code></li>
<li><code>5.3</code> - <code>3公里</code> - <code>朱屋</code></li>
</ul>
<h2><a id="toc-528" class="anchor" href="#toc-528"></a>4月（5公里）</h2>
<ul>
<li><code>4.25</code> - <code>5公里</code> - <code>朝阳公园</code></li>
</ul>
<h2><a id="toc-da8" class="anchor" href="#toc-da8"></a>1月（20公里）</h2>
<ul>
<li><code>1.21</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>1.16</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>1.12</code> - <code>5公里</code> - <code>360健身房</code></li>
<li><code>1.1</code> - <code>5公里</code> - <code>360健身房</code></li>
</ul>

            ]]></description>
            <pubDate>Thu, 17 Sep 2020 07:30:53 GMT</pubDate>
            <guid>https://welefen.com/post/2020-run.html</guid>
        </item>
        <item>
            <title>欢迎使用 Firekylin</title>
            <link>https://welefen.com/post/hello-world-via-firekylin.html</link>
            <description><![CDATA[
            <div class="toc"><ul>
<li><a href="#toc-848">常用操作</a><ul>
<li><a href="#toc-1ab">登录后台</a></li>
<li><a href="#toc-7e4">网站基本设置</a></li>
<li><a href="#toc-ff3">评论设置</a></li>
<li><a href="#toc-310">菜单管理</a></li>
<li><a href="#toc-921">主题外观</a></li>
</ul>
</li>
<li><a href="#toc-a17">Markdown 简介</a></li>
</ul>
</div><p>这是程序自动发布的文章。如果您看到这篇文章，表示您的 Blog 已经安装成功！</p>
<p>如果您对 Firekylin 不是很熟悉，可以先阅读以下常用操作了解一下。</p>
<!--more-->

<h2><a id="toc-848" class="anchor" href="#toc-848"></a>常用操作</h2>
<h3><a id="toc-1ab" class="anchor" href="#toc-1ab"></a>登录后台</h3>
<p>Firekylin 的后台登录入口在 <a href="/admin">~/admin</a></p>
<h3><a id="toc-7e4" class="anchor" href="#toc-7e4"></a>网站基本设置</h3>
<p>后台的 <a href="/admin/options/">系统设置</a> 提供了与网站相关的选项，例如可在其中的 <a href="/admin/options/general">基本设置</a> 中设置网站名称、Logo地址等。<br>更多的设置，请参考 <a href="https://github.com/firekylin/firekylin/wiki/">官方 WIKI</a> 的 <a href="https://github.com/firekylin/firekylin/wiki/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E7%BD%AE">系统设置</a></p>
<h3><a id="toc-ff3" class="anchor" href="#toc-ff3"></a>评论设置</h3>
<p>Firekylin 没有内置评论模块。但是，Firekylin 可方便地使用第三方评论系统。在后台的 <a href="/admin/options/">系统设置</a> 的 <a href="/admin/options/comment">评论设置</a> 的 <code>自定义</code> 模式下粘贴第三方评论系统的代码即可。</p>
<p>Firekylin 还对 <a href="https://disqus.com/">Disqus</a> 、<a href="https://changyan.kuaizhan.com/">畅言</a> 、<a href="https://gentie.163.com/">网易云跟帖</a>  提供了特别的支持，只需要填写对应的网站id即可，不需要粘贴具体的代码。</p>
<h3><a id="toc-310" class="anchor" href="#toc-310"></a>菜单管理</h3>
<p>后台的 <a href="admin/appearance/">外观设置</a> 可进行 <a href="/admin/appearance/navigation">菜单管理</a>，包括新增菜单、删除菜单、菜单排序等。<br>新增菜单时，如填写了菜单属性（例如属性为 <code>home</code>），Firekylin 自带的主题会从图标库尝试寻找 <code>icon-home</code> 作为该菜单的图标，如未查到匹配的则不会显示图标。</p>
<h3><a id="toc-921" class="anchor" href="#toc-921"></a>主题外观</h3>
<p>Firekylin 目前只带了一套主题，所以基于 Firekylin 架构的网站长得都差不多^_^<br>主题外观的使用、修改、创建可参考官网 WIKI 的 <a href="https://github.com/firekylin/firekylin/wiki/%E4%B8%BB%E9%A2%98%E5%A4%96%E8%A7%82">主题外观</a>。  
欢迎越来越多的热心用户为 Firekylin 开发主题外观，开发手册详见 <a href="https://github.com/firekylin/firekylin/wiki/%E4%B8%BB%E9%A2%98%E5%BC%80%E5%8F%91">主题开发</a>。</p>
<h2><a id="toc-a17" class="anchor" href="#toc-a17"></a>Markdown 简介</h2>
<p>Firekylin 的编辑器为支持 Markdown 语法的编辑器。Markdown 是一种简化的标记语言，普通的纯文本内容（例如 Windows 的记事本撰写的内容）经过 Markdown 标记之后，可被渲染成赏心悦目的富格式文本。</p>
<p>Markdown 的格式说明可参考：<a href="https://guides.github.com/features/mastering-markdown/">英文版</a>、<a href="https://coding.net/help/doc/project/markdown.html">中文版</a></p>
<p>好了，介绍就这么多，快开始你的 Blog 之旅吧！</p>

            ]]></description>
            <pubDate>Fri, 11 Sep 2020 09:19:26 GMT</pubDate>
            <guid>https://welefen.com/post/hello-world-via-firekylin.html</guid>
        </item>
    </channel>
</rss>
