分类
服务器端

nginx 笔记

基础配置

# daemon on;
# worker_processes 1;
error_log logs/travis.error.log error;
pid logs/travis.nginx.pid;

events {
    accept_mutex off;
}

http {

    server {
        listen 9000;

        include mime.types;

        location / {
            rewrite ^ /static/edge/index.html last;
        }

        location /admin-api/ {
            proxy_pass https://admin-dev.openresty.com.cn;
            proxy_set_header Host admin-dev.openresty.com.cn;
            proxy_ssl_name "admin-dev.openresty.com.cn";
            proxy_ssl_server_name on;
        }

        location /static/ {
            alias fe/dist/static/;
        }
    }
}

启动 nginx

nginx -p $PWD -c conf/travis.conf

其中,-p $PWD 指定当前目录为工作目录。-c 指定配置文件。

reload

找到配置中的 pid 文件,从里面找到 pid

kill -s HUP ${pid}
分类
技术

使用 Node.js 驱动 FFmpeg 在 Linux + vncserver 下完成视频录制

自动化录制屏幕有很多用途,比如生成教学视频、生成产品文档,等等。对比人工,自动化有很多好处:

  1. 避免创作者的设备和环境问题(比如邻居装修、麦克风不好等)
  2. 避免创作者的语言、发音问题(比如普通话不标准、不会说某种语言)
  3. 录制环境出现变化,可以方便的重录(比如换个背景图,界面有升级)
  4. 就像写博客一样,任何时候,拿出电脑或者手机都能编辑一段

所以目前研究这方面应用的很多,我厂也是。我近期就投入大量时间在这项工作上面,现在终于有所成果,写篇博客分享一下。

0. 准备环境

首先推荐大家使用 Linux。Linux 开源,有很多开源免费的工具可以完成各种操作,不仅可以录屏,还可以很容易地模拟各种用户操作,给我们留下大量开发空间。

建议选择有图形界面的 Linux 发行版,我尝试过 fedora 33 和 Debian 10 树莓派版,都很容易配置。如果使用纯命令行版本,然后自己完成安装图形界面,比如 gnome,再完成剩下来的配置,会很麻烦。

然后记得把系统更新到最新版,以规避可能遇到的问题。

1. 配置 vncserver

如果只能在主屏录制,这个产品的实用性就会大打折扣。所以我们选择用 vncserver 创建虚拟屏幕,然后在虚拟屏幕上完成录制。如果需要的话,也可以随时用 vnc viewer 之类的软件连上 VNC 实时查看效果,非常方便。

有些系统自带 vncserver,比如 Debian 10 树莓派,那就不用安装。我们选用 fedora 33,需要手动安装,这里推荐 TigerVNC,安装使用都很方便:

sudo dnf install tigervnc

安装完成后,使用:

vncserver :5 -geometry 1280x720

就可以创建虚拟显示器了。其中,:5 是显示器 id,可以顺延,比如 :6:7、甚至 :99,至于上限在哪里我暂时不知道。-geometry 1280x720 是设定显示器分辨率为 1280×720。

另外,还可以使用下面的命令查看和关闭显示器:

 vncserver -list
 vncserver -kill :5 

1.1 测试

配置完成之后,可以用 Firefox 测试一下效果。

# 安装 firefox
sudo dnf install firefox

# 在指定虚拟显示器打开 firefox
DISPLAY=:5 firefox https://cn.bing.com

# 截图,可能需要安装 xwd
xwd -root -display :5 > screen.xwd

# 转换成 png,可能需要安装 ImageMagick
convert screen.xwd screen.png

然后把图片下载到本地,或者启动一个 http 服务器就能看到了。

1.2 关闭桌面

默认的 VNC server 会启动桌面,此时可能会要求我们登录什么的。我们在这套系统当中并不需要桌面,只要有显示器即可,所以可以修改 ~/.vnc/xstartup 禁用:

#!/bin/sh
 

unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
# 把下面这行注释掉 
# exec /etc/X11/xinit/xinitr

2. 使用 FFmpeg 捕获屏幕内容

使用 FFmpeg 录屏比较简单,将输入源设置为指定显示器即可,formatx11grab,命令大体如下:

ffmpeg -y -f x11grab -video_size 1280x720 -framerate 30 -i :5.0+0,0 -c:v h264 a.mp4

其中,

  • -y 意思是自动覆盖前面生成的视频
  • :5.0+0,0 是使用刚才创建的 :5 显示器,用它的 0 号桌面,启动位置是 0,0 即左上角
  • -f x11grab 是使用 X server 抓取格式,Linux 下的图形界面一版是基于这个系统

注意,上面这条命令里参数的顺序很重要,否则可能遇到 Protocol not found 等错误。

3. 使用 node.js 驱动

最后只要用 node.js 的 child_process.spawn() 功能调用上面的命令即可。这段代码属于公司,我就不贴了,主要分享几点经验教训:

  1. 要用 spawn,因为录制过程中我们需要用输出来判断录制状态,exec 这种只能在结束时提供输出的没法用
  2. FFmpeg 会把输出输出到 stderr,理由不明,不过记得要用 stderr 来检查
  3. 录像完成,如果在命令行,按 q 或者 ctrl+C 都可以停止录像,并开始封装视频文件。在 node.js 里,我们可以调用 cp.kill('SIGINT') 。注意,调用之后,FFmpeg 子进程并没有立刻结束,它要把前面的录像进行封装,这个过程也是需要时间的,所以如果你接下来还要对视频文件进行操作,应该等待子进程彻底结束
  4. 判断录像开始的依据,我目前用的是:输出里包含 Output #0, mp4, to 'a.mp4'
  5. 判断录像结束,视频已经生成的依据,我用的是 cp.on('exit', onExit),然后在 onExit 里处理。注意,其它情况导致 ffpmeg 子进程退出时也会触发这个函数,所以我们必须检查 code。此时,在我的机器上,code 是 255,表示它是用户手动中止的,可以当作判断依据。

总结

剩下来的内容基本就是怎么驱动图形界面程序运行了。一般来说用 puppeteer 比较好,可以很容易的跟 node.js 联动,我厂的 showman 也是基于这个方案来实现的,最后贴一段视频,大家看下效果:

一段用上述技术生成的视频

参考阅读

分类
css

重发老视频:使用 CSS 制作工序流程图

整理之前录的视频,发现一个漏掉没有上传的:

这个视频里,我演示了如何使用纯 HTML + CSS 制作工序流程图。涉及到的技术包括:

  1. display:flex Flex 布局
  2. 使用 order: N 调整显示顺序,以实现响应式
  3. 使用 position:XX 调整定位

虽然项目不大,不过大部分布局相关的技术都有所涉及,很适合刚入门和初级同学学习。


今年想继续在直播、视频方面发力,希望大家支持。如果有什么想听的想看的想学的,也欢迎点菜。

分类
技术

欢迎欢迎

欢迎来到我的博客,我是 Meathill,想了解我可以点 关于我

我工作日晚上 9:00~10:00 会在 B 站直播,https://live.bilibili.com/5126601,大部分都是全栈开发相关,感兴趣的同学可以关注下。

06-07 ~ 06-11 直播计划:

6-7Showman 的最新进展:视频录制
6-8bb酱挂件开发
6-9bb酱挂件开发
6-10设计模式:适配器模式
6-11设计模式:适配器模式
直播计划

欢迎留言点播各种内容。


近期作品

分类
chrome

Chrome 扩展里实现 SSO

周五打算给客户发版,结果在这里卡了大半天,写篇博客记录下。

0. SSO 的实现

SSO,Single sign-on,单点登录,即统一处理用户登录、提供用户身份凭据的功能。使用 SSO,可以只维护一套用户体系,容易开发维护;对用户来说,只需要登录一次就能使用该开发商的全部产品,也很轻松方便。

一般来说,SSO 的流程是:

  1. 用户使用 A产品,域名是 pa.mydomain.com,登录服务(S)位于 login.mydomain.com
  2. 用户使用提供服务的 A产品,A产品需要登录,用户选择登录
  3. 来到登录服务,完成登录
  4. S服务将用户指回 A产品,返回的 URL 里包含一个 token
  5. A产品拿到 token,请求 S服务,验证 token,获取部分用户信息(比如邮箱,一般只用来展示
  6. A产品生成自己所需的身份凭据,并以此验证用户身份

我厂的产品也是这么实现的。

1. Chrome 扩展遇到的问题

本地调试一切正常,但是加载成扩展之后,从登录服务跳回扩展会遇到 ERR_BLOCKED_BY_CLIENT 错误,URL 也被重定向到 chrome://invalid/。我在这里卡了很久,主要是不知道该怎么定位问题和搜索答案。

后来经过反复尝试,我终于发现,只有从登录页面跳转回去插件页面的时候,即 location.href='chrome-extension://{id}/ui/index.html 的时候,才会报错,所以立刻换用 chrome extension href ERR_BLOCKED_BY_CLIENT 作为关键词,立刻找到了答案:redirect to chrome-extension:// results in ERR_BLOCKED_BY_CLIENT

然后阅读文档:Manifest – Web Accessible Resources(可由 Web 访问的资源),得知需要在 manifest.json 里添加对应的配置:

{
  ...  "web_accessible_resources": [
    "ui/index.html"
  ],
  ...
}

添加后 SSO 就正常了。

2. 后记

不过我没想明白的是,这个配置意义何在?配置写在扩展里,防止 web 访问扩展里的文件,似乎并没有什么帮助,也没什么安全性的顾虑。也许是我还没遇到吧。

分类
chrome

让 Chrome API 支持 Promise

Chrome API 都是回调型,连续使用非常不方便,希望能改成 Promise 型。Chrome 本身不提供 promisify,不过可以自己写一个:

export default function promisify(original) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      original(...args, (...results) => {
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError.message);
        } else {
          resolve(...results);
        }
      });
    });
  }
}

这里有几个注意事项:

  1. 参数使用 ...args 进行拆解和合并,方便调用
  2. 需要检查 runtime.lastError,不然出错的时候浏览器会报错,影响体验

使用的时候,我比较喜欢只修改需要使用的函数,不打算 promisify 全部函数,大概是这样:

import promisify from './index';

/* global chrome */

export const update = promisify(chrome.tabs.update);
export const remove = promisify(chrome.tabs.remove);
export const get = promisify(chrome.tabs.get);
// 我觉得 `close tab` 看起来更合理
export const close = remove;
// 封装一个 `goto` 的快捷方式
export const goto = function (tabId, url) {
  return update(tabId, {url});
}
// 全部导入,好处是简单,坏处是不方便 tree-shaking
import * as ChromeTabs from '../chrome-promisify/chrome.tabs';

ChromeTabs.goto(tabId, 'https://blog.meathill.com');

// 需要哪个导入哪个
import {goto} from '../chrome-promisify/chrome.tabs';

goto(tabId, 'https://github.com/meathill');
分类
服务器端

在树莓派上启用 PostgreSQL 对外服务

以前写过一篇笔记《树莓派4 安装 OpenResty + PostgreSQL》,记录如何在树莓派上装 PostgreSQL,不过那时候只是为了在上面做开发,没有考虑过对外服务。如今为了能够在别的机器上做开发,所以要想办法配置一下对外服务。

0. 系统

  • Raspberry Pi 4B
  • Debian 10 buster 更新到最新
  • 如上篇文章所述安装和配置 PostgrSQL

1. 判断本地运行状态。

# 查看服务状态
sudo service --status-all
# [ + ]  postgresql

# 查看端口
sudo netstat -plunt | grep postgres
# tcp        0      0 127.0.0.1:5432            0.0.0.0:*               LISTEN      6629/postgres

服务在运行,端口也在侦听,直接连接,失败,被服务器拒绝。

2. 安装防火墙工具调整规则

猜测可能跟防火墙有关,iptables 我不熟,所以安装 ufw 帮忙:

# 安装
sudo apt install ufw

# 启动端口
sudo ufw allow 5432
sudo ufw allow from 10.0.0.10 # 我的 iMac

3. 修改侦听端口

修改防火墙后还是连不上。使用 Telnet 工具可以本地连接,但不能远程连接,推断应该是侦听端口的问题。回去仔细看了一下端口状态,觉得应该是端口没配好,所以修改配置,侦听 0.0.0.0,然后重启 PostgreSQL 服务,再连接就成功了。

listen_addresses = '0.0.0.0' 
port = 5432
host    all             all              0.0.0.0/0                       md5
host    all             all              ::/0                            md5

参考链接:

分类
服务器端

LeanCloud 笔记

慢慢记。

慎用 await Promise.all(items.map(item => ....))

很容易造成 409 too many requests 问题。

最好用

const newItems = [];
for (const item of items) {
  item = await doSomeAsyncJob();
  newItems.push(item);
}

Pointer 时尽量用 query

取单一对象的时候,方法有很多,比如 createWithoutData + fetch。不过如果如果对象内部属性有 Pointer,且我们希望一次性把 Pointer 取回来的话,最好用 query,因为只有它支持 .include(),可以一次性拉取全部需要的数据,减少请求次数,减少发生 too many requests 的可能。

分类
php

PHP 8.0 发布——JIT 到来,性能大幅提升,一堆语法糖

早上起来,得知 PHP 8 正式发布了,作为曾经的半个 PHP 程序员,当然要去看一看。官方的 Release note 在这里,建议做 PHP 开发的各位同学都看一看:

PHP 8 Released!

接下来聊聊我的想法。

JIT

作为一个大版本,PHP 8 一定要有一些非常大的变化,JIT 就是这个非常大的变化。

JIT 是 just-in-time 的简写,意思是在运行时将部分代码编译成机器码,以便反复使用。执行机器码的速度会比执行一般的解释型代码快很多,所以 JIT 通常意味着可以大大提升语言的运行速度。

PHP 8 引入了两种 JIT 编译引擎,Tracing JIT 和 Function JIT,其中最值得期待的是 Tracing JIT。在基准测试中,速度有 3 倍提升;在一些长时间运行的应用当中,也有 1.5~2 倍的提升。参考下图,可惜,这个基于 WordPress 的博客提升只有一倍。

PHP 8 JIT 性能表现
PHP 8 JIT 性能表现

所有的功能都要以性能为基础,PHP 从 v7 开始就很努力地提升性能,加上它的功能一直封装的很好,所以我一直觉得 PHP 是服务器端开发最好的语言。

一堆语法糖

不知道是不是受了同为 Web 开发语言的 JS 的影响,v7 之后的 PHP 非常放飞,每个版本都引入一堆新语法和语法糖,什么箭头函数、类型系统,基本上只要有用,都给加上。v8 也不例外,有一些语法已经到了我看不懂的程度了……

比如这个 Attributes,我就没太明白,暂时把它理解成装饰器:

// PHP 7
class PostsController
{
    /**
     * @Route("/api/posts/{id}", methods={"GET"})
     */
    public function get($id) { /* ... */ }
}

// PHP 8
class PostsController
{
    #[Route("/api/posts/{id}", methods: ["GET"])]
    public function get($id) { /* ... */ }
}

Laravel

顺便说一下,前些日子 Laravel 也发布了 v8。不过不太一样的地方是,Laravel 的版本号跟 node.js、Ubuntu 采用同一种策略,即每年一个大版本,只有偶数版本会长期支持(LTS)。

总结

活到老学到老,大家加油。

分类
技术

设计并实现自动生成前端编程教学视频的小语言(一 想法篇)

我一直想把录制教学视频的过程变得更稳定可控,而不需要依赖一时的状态。后者常常受到各种影响:比如家里狗叫了、孩子闹了、邻居装修了;或者录到一半突然遇到调不通的 bug;又或者只有 10分钟想录一段,但找不到感觉;等等。

好处

加入我厂后,见识到各种小语言的威力;另一方面,计算机语言经过祛魅,我也不觉得有多难实现。所以我希望能够把录制教学视频的过程语言化,这样会带来几个好处:

  1. 想写就写:哪怕只有几分钟,写上一个小节,或者修改几个错字,都可以;
  2. 想录就录:直接在服务器上生成,不需要考虑周围环境;
  3. 方便多语言:文字翻译后,重录生成其它语言即可;
  4. 方便修改和升级:大部分错误都是口误,或者细节出入,从头录必然不合适,目前来看大部分视频都通过字幕处理。而语言重新录制一遍即可;想补充内容,也很容易,尤其是 API 或者最佳实践变化,需要修改大小 N 处,从语言生成就更具优势。

技术环境与选择

能够实现这个小语言,自然需要依靠整个技术环境的成熟与健全。大概有这么几项:

语音合成

语音合成(TTS)经过 AI 加成,效果相较于过去提升不少,足够为用户接受。再加上难度不大,所以支持的平台很多,价格也不贵,随便选一家即可。

录屏

首先可以选择 OBS。除了 UI 之外,它也提供 API,不过语言只有 Python 或 Lua,我都不是很熟。用它的好处是支持场景配置,我们可以先配好几组场景,然后在需要的地方切换。

如果是 macOS 可以使用 aperture-node,它借助 macOS 的原生 API 实现录屏,性能非常好。不过兼容性不行,考虑到新推出的 M1 芯片性能好功耗低,买一台 Mac mini 专门用来跑生成也不错。

也可以选择 ffmpeg,好处是什么平台都能跑,坏处是什么平台都一般。

效果演示

有 webpack-dev-server 在,效果演示不成问题。

代码编写

目前最大的挑战就在这里——我还不太确定怎么实现代码的自动输入。初步考虑使用 AppleScript 配合 VSCode,如果不行的话就浏览器里跑 VSCode online,然后用 JS。

基础设计

  1. 既然要做教学视频(tutorial),我又很喜欢东南亚,那么语言就叫 tutolang 吧,tuto = 拖拖车,便宜又方便。
  2. 因为最终的目的是生成视频,所以它应该是个声明式语言
  3. 语言教程不能只从上到下顺代码,得能够找到特定位置输入代码然后讲解。这个部分考虑再三之后通过 git 来做最为简单直接。
  4. 目前来看,从前端三个语言的角度生成视频应该是问题不大,后面如何整合其它视频过程需要再考虑。

其它

语言本身肯定要开源,编译器计划也直接 MIT,随便用。然后提供一组自动化的编辑、生成、转码的基础设施,作为服务收费。