给 Markdown 添加视频支持

发布时间:

最后更新:

都 2020 年了,现在最流行的就是什么直播弹幕短视频,你的博客要是还不支持插视频那可就OUT啦!

本博客目前使用 Markdown 写文,可惜它原生的语法并不支持视频,于是只能自己来实现。

视频好处都有啥? #

视频是个好东西啊,它要不好现在的直播弹幕短视频怎么火的……

咳咳,扯远了,就说写博客,比如写教程啊总会遇到需要动态演示的东西吧,比如我自己的纯CSS解决图片加载的布局移动问题里一开头的动图(其实是视频啦),要用图片或者文字展现的话肯定没有动态的好。

另外玩过 Twitter 的都知道它里面插入的 GIF 图会转换为视频,GIF 是 1987 年发明的东西,早就该进垃圾堆的技术,因为当年浏览器不支持视频才得以流行

在性能上,H.246编码的视频体积仅为GIF的13分之一,虽然 GIF 也有 gifsicle 能压缩一下,但效果仍不如视频。

虽然一些现代化的格式如 WebP 也支持动图,但从我的实际经验来看,文章里大部分较短的动态内容都来源于录屏或是剪辑,它们本来就是视频格式,再去转成动图多此一举。

而特别长的内容就更不适合用动图了,因为动图不能像视频一样边加载边放。

综上所述,视频的支持是一个现代化内容网站必需的功能

语法的选择 #

Markdown版本演进Markdown版本演进

上图来自https://juejin.im/post/5baa5b346fb9a05d2d0225cc#heading-6

主要的几个 Markdown 版本原生都不支持视频,我不知道它的作者是怎么想的,如此重要的功能竟然能没有。既然官方没有,那自然就会有种各样的第三方实现方案,按照本人强迫症的做法当然要对比一番

直接插 HTML #

这是我看到的最多的做法,其优势就是简单,现有的转换库都支持写HTML,但我认为这种方式并不好。

这缺点太多,所以我决定还是得用 Markdown 的方式来做。

GitLab Flavored Markdown #

GitLab Flavored Markdown(下称 GFM)是 Markdown 的一种修改版,它复用了图片的语法,以扩展名来区分媒体的类型,比如![label](None)因为链接是.mp4结尾所以渲染为视频。

GFM 的支持也很广泛,实现又简单,还有 GitLab 背书,自然也是个不错的选择。

但它的缺点也很明显,强制了链接的文件名必须是视频常用的扩展名,然而并不是所有链接都是如此,Twitter 的视频链接就没有扩展名。

要解决这个问题,一种方案是先下载后渲染,就是先渲染个占位元素,然后在 JS 里fetch资源,通过响应来判断是不是视频,再去更新 HTML。但这样做需要搭配 JS 代码,所以并不通用。

另外既然都修改了原始的 Markdown 语义,何不直接另起一个新语法呢?

自己编个语法 #

关于自定义的语法有很多讨论,我认为比较好的一种是使用通用指令语法,它的格式是@指令类型[...](..){...}这样的,前面的@可以换成别的,指令类型用于区分视频、音频、GIF视频等,后面三个括号里的内容可以自由发挥。

最终我决定使用这种语法,它跟原生的图片语法一样简洁,又给足了自由发挥的余地。

通常来说,新的语法最好还是跟现有的保持相似,这里就以语法比较像的图片为基准。圆括号仍然跟图片一样包含视频的链接,方括号里填标签(GIF)或者 poster(视频)。

通用指令语法里,花括号用来放置key = "val"这样的键值对,但它们同样可以放在链接URL的参数上,而且目前本站的图片就是这么做的,为了保持一致我选择不要这个花括号部分。

指令类型包含GIF视频和普通视频,另外Markdown同样不支持插入音频这里也给补上,所以最终的语法为:

解析器的实现 #

注:由于 Safari 不支持前向环视,实现方案已修改,详情见最下面的更新部分

我的博客使用 markdown-it 来转换Markdown为HTML,markdown-it 的流程分为解析和渲染两部分,所以要给这两个地方编写自己的函数实现。

首先是怎么识别@<指令类型>[...](..)这种文本呢,如果不考虑转义的话倒是一个正则就能搞定,但标签里可能出现方括号,链接里也可能有圆括号,所以转义还是要有的。

Markdown 对括号的转义方式有两种:配对计数和斜杠转义,其中配对计数需要一个变量来存储左括号数量挺麻烦,而且斜杠转义完全能用于所有场景,但反过来配对计数却无法用于右括号单独出现的情况(虽然不常见)。

综上所述,我决定不支持计数了,斜杠转义用一个前向环视(?<!\\)就能解决,再给指令部分加点限制,最后的正则如下:

把它们三个连起来就可以匹配通用指令语法了。

Markdown 有块block和行内inline两种结构,原始的图片语法是属于行内的,这可以实现图文混排,但在使用中发现我并没有图文混排的需求,本站文章的图片都是单独一行。所以我决定新的语法作为块结构,这样可以降低解析函数被调用的频率,提升点性能。

最后要注意一下的是反转义和 XSS 检查,这些函数在 markdown-it 里已有提供:

完整的代码见 kaciras-blog/markdown/blob/master/core/src/directive.ts

用视频模仿GIF #

首先看看 GIF 跟视频的区别,以及怎么解决这些问题:

虽然video有一个autoplay属性来自动播放,但在大部分浏览器里这功能被禁用了,不过仍可以通过 JS 来控制播放与暂停。另外视频的解码开销比较大,如果一个页面里同时播放数十个视频还是会卡的,所以只有在屏幕可见范围的视频才需要播放,超出可视区就要给它停掉。

以上需求都可以通过IntersectionObserver来解决,监视所有 GIF 视频元素,然后通过intersectionRatio判断是进入可视区还是离开来播放或暂停:

javascript
const autoPlay = new IntersectionObserver(entries => {
	for (const { target, intersectionRatio } of entries) {
		intersectionRatio > 0 ? target.play() : target.pause();
	}
});
el.querySelectorAll(".gif").forEach(video => autoPlay.observe(video));

通常浏览器会对的自动播放做限制,为了防止特别大的声音突然播放吓到人,视频元素的play()方法仅在用户点击等交互事件里才能用,或者视频是静音的。因为 GIF 视频本身就没有声音所以这是没问题的。

不同的渲染目标 #

Markdown 渲染出来的 HTML 是跟场景相关的,比如在 RSS 里渲染的结果应尽量简单,毕竟阅读器的样式是没法由我来控制的;而在我的博客网站里,会有一些额外的样式和元素来展现更好的效果,比如提前固定宽高比防止布局移动、居中等。

另外由于 RSS 阅读器不会加载本站的样式表,也不会运行我的 JS,所以阅读器里的 GIF 视频只能跟普通视频一样。

除了此之外评论也使用 Markdown,这又是一个新的渲染目标,因为评论是由用户输入的,所以对其的渲染必须加入一些限制以防滥用。

对无法控制的前端,渲染实现跟解析器写在一起,见上面的链接。

本站的渲染代码:kaciras-blog/markdown/blob/master/core/src/web/media.ts

最终效果 #

这是GIF视频哦

下面是普通的视频:

更新:环视的兼容性 #

没过几天就收到了Sentry的错误报告,在文章页的代码里出现了SyntaxError:

Sentry错误报告Sentry错误报告

仔细一查发现原来 Safari 浏览器不支持正则的前向环视,(傻B苹果,气死我了)。

这个用 iPhone 的哥们触发了21次错误,估计是刷新了好久……

为什么会出这档事呢,因为我没有苹果的设备,也就没测试 Safari。解决方案就是老老实实滚回去手撸 Parser,还好也不复杂。

评论加载中