视频被编码成表情符号⁉️ 存储在数据库里?😱🤯
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
好吧,连我自己都觉得我这次做得太过分了!
我们能否将视频转换为表情符号(虽然有点傻),然后将这些表情符号保存到数据库中(每行“像素”对应一行),并从数据库中提供“图像”……显然可以!
你该这样做吗?不。
好玩吗?有点儿。
它会让你微笑吗?我希望如此,这比我想象的要费劲一些!
为了让你们明白这件事有多荒谬,我不得不:
- 将视频转换为低分辨率帧
- 读取每一帧并获取像素数据
- 将像素数据转换为表情符号
- 将这些表情符号存储在数据库(Postgres,使用 Supabase)中。
- 创建一个存储过程来检索帧,以便我可以快速连续地进行 API 调用来播放“视频”。
这里有很多东西需要解读,我们也可以在此过程中学到一些有趣的东西,但首先……让我们来看演示!
点击下方 CodePen 中的大播放按钮,即可欣赏它的完整效果!
演示
警告:这将使用 20MB 的数据流量,因此如果您使用的是 4G 网络,并且您有昂贵的数据流量套餐,请不要点击播放!
该实验最令人惊叹的部分是,我从 Supabase 逐帧单独调用每一帧,不得不承认,延迟非常惊人!
关于我如何存储数据(以及如何创建数据),稍后会详细介绍。
在演示之前还有最后一点需要说明。
我们不把视频存储为表情符号是有原因的,因为它会占用大量空间。
由于上述演示是在 Supabase 的免费套餐上进行的,因此它可能会突然停止工作,因为 5GB 的出口流量只允许视频播放 250 次,之后我就会用掉太多数据!
如果你按下播放键后没有任何反应,那可能是我达到了播放次数上限,以下是你原本会看到的视频:
这个实验的有趣之处。
*和往常一样,这不是教程。*
但是,在此过程中有一些有趣的事情可能对参考有用:
数据转换与存储
这里有几点值得思考。
你看,将像素数据存储为表情符号效率并不高(我之前已经提到过这一点)。
因此,我们需要找到一种方法来最大限度地减少我们存储的数据量。
将视频转换成帧并进行降采样后,我得到了这样的结果:
但这样做存在一些问题。
去除黑条。
啊,80年代,那时候电视机几乎是正方形的,鲻鱼头很流行。
可惜的是,自那时以来,有些事情发生了变化。
我们现在采用 16:9 而不是 4:3 的宽高比,这意味着我们需要应对一些巨大的黑边。
把这些存储为表情符号毫无意义。
现在,我们稍后会介绍如何检索像素数据,但你只需要知道,我是逐行检索的,并且遍历每一行中的每个像素。
所以为了去除那些黑边,我有很多方法可以做到。
但我喜欢简单,所以我的解决方案是这样的:
f(pixelCounter > 36 && pixelCounter < 219){
//do something with the pixel data otherwise ignore it.
}
由于我的图像宽度为 256 像素,我直接跳过前 36 个像素和后 36 个像素,因此我只处理每行中具有有意义数据的 182 个像素。
它丑吗?是的!
它效率低吗?是的!
它有效吗?当然有效!
删除部分行数据。
我遇到的另一个问题是尺寸。(她就是这么说的……😱)
即使去掉黑边,我仍然需要存储 182 像素 x 144 行 = 26,208 像素!
这样做的问题在于,每个像素都要存储 4 个表情符号,所以实际上每帧需要 104,832 个表情符号。这太多了!
幸运的是,答案很简单,我们每个“表情符号像素”使用 4 个表情符号,所以我们只需每 4 个像素采样 1 个表情符号即可。
现在我们不能每隔四个像素采样一次,事情没那么简单。如果那样做,我们会丢失太多水平方向的信息。
所以我们实际上是每隔 2 个像素,每 2 行采样一个像素。
因此我们进行抽样:
- x1,y1
- x3,y1
- x5, y1
...
- x1, y3
- x3, y3
- x5, y3
...
etc.
这样一来,我们只需要采样 6552 个像素,每个像素 4 个表情符号,每帧就能显示 23000 个字符。这就足够了!
对数据进行编码
下一步是将这些像素数据转换为表情符号。
我改进这个方法的一个途径是,将所有表情符号按颜色进行分类(与每个 RGB 值最接近的匹配)。
但对于这个愚蠢的实验来说,这工作量有点太大了。
所以我改用了5个表情符号:🟥🟩🟦⬜⬛
对于每个像素,我们会获取每个 RGB 值,然后根据强度显示彩色表情符号,或者显示黑色或白色表情符号。
我最终采用的逻辑如下:
- 检查红色值。
- 它大于 127(255 的一半)吗?
- 如果答案是“是”,则显示红色表情符号。
- 如果“否”,则显示白色或黑色。
- 对绿色和蓝色重复上述步骤。
现在你可能想知道,我们如何决定显示黑色还是白色?
我们通过查看 R + G + B 的组合值来实现这一点。
如果 R + G + B > 500,则显示白色表情符号;否则显示黑色表情符号。
对于如此简单且“二元”的色彩表达方式来说,它的效果出奇地好。
把所有内容整合起来。
所以听着……在我向你展示这段代码之前,请记住,这只是一段一次性使用的、用完即弃的代码,好吗?
我不想让你对这种垃圾代码进行代码审查。
明白了吗?很好。🤣
这段代码还有一些我没有提到的功能:
- 将图像从图像输入加载到画布上,并获取像素数据。
- 将图像数据放到第二个画布上(提示
Uint8ClampedArray很重要……我在这方面卡了好一会儿!) - 获取图像数据本身(另一个有用的技巧:Canvas 中的图像数据以 4 字节块的形式存储在“扁平数组”中,因此您必须为每个像素跳过 4 个数组项。例如,R1、G1、B1、A1,然后 R2、G2、B2、A2)。
- 构建了一个不太优雅的
INSERT语句,用于将我们的图像数据插入数据库表中。
如果您想测试一下,可以下载这张图片(因为它仅适用于这种特定尺寸的图片,并且只能根据这些图片去除黑边)。
这里有一个 CodePen 示例,展示了我用来转换帧的代码……虽然不太美观,但它能用。
最后一部分,数据库查询
说实话,多亏了 Supabase,这部分非常简单,可能只有几个要点值得一提。
数据存储与检索
我决定让它更有趣一些,将每一行像素存储为数据库中的一行。
这呈现出的视觉效果非常有趣:
为了确保此功能正常工作,我们需要两列数据:第一frameid列(以便我们可以查询框架的所有行)和第二rownum列(以便我们可以对框架的行进行排序)。
然后只需在 SQL 编辑器中添加一个自定义函数来检索此信息即可:
create or replace function get_frames (frame integer)
RETURNS table(frametext text)
LANGUAGE plpgsql
as $$
BEGIN
RETURN QUERY SELECT STRING_AGG("rowdata", CHR(13)) AS frame
FROM video2
WHERE frameid = frame
GROUP BY frameid
ORDER BY frameid ASC;
end;
$$;
看起来可能很复杂,但如果我们把它分解开来,其实并没有那么糟糕!
create or replace function get_frames (frame integer)
第一部分让我们定义一个可重用的函数。这很重要,因为我们之后可以将此函数用作 API 端点!
RETURNS table(frametext text)
LANGUAGE plpgsql
as $$
这里我们定义返回类型。括号中的部分对应于我们要返回的列(如果有多列,我们会为每一列设置列名和类型)。
RETURN QUERY SELECT STRING_AGG("rowdata", CHR(13)) AS frame
FROM video2
WHERE frameid = frame
GROUP BY frameid
ORDER BY frameid ASC;
end;
$$;
最后一部分是我们的 SQL 查询(并将其返回,因为这是一个函数)。
STRING_AGG你可能不太熟悉这个函数。它的功能很简单,就是将列和字符连接起来。我们获取rowdata数据,然后将其与CHR(13)换行符组合,生成输出字符串。
但要让这个函数正常工作,关键在于这个GROUP BY语句。如果没有它,STRING_AGG函数就无法运行,因为它不知道应该将多行数据聚合在一起。
然后我们只需要ORDER BY——这只是为了确保我们按顺序获取每一行数据,以便图像有意义!
创建我们的查询和 API 端点
Supabase 的妙处在于,现在我们只需一步就能将该函数用作 API 端点。
我们只需要一个访问控制策略。
您可以在项目的“身份验证”>“策略”下找到它。
我对 Postgres 还不太熟悉,但幸运的是 Supabase 提供了一个现成的实用模板,让我可以授予读取权限。
这样一来,我现在就可以将该函数作为 API 端点调用了!
(如果您不确定在哪里可以找到该信息,它位于“数据库”>“函数”中,您会看到类似这样的行:)
调用我们的 API 端点
我们最不需要的就是获取数据。
为此,我们需要我们的SUPABASE_URL和我们的密钥(SUPABASE_KEY)。
由于我是 Supabase 新手,所以花了一点时间才找到,但它们位于“项目设置”>“API”下。
然后我们只需要 Supabase SDK(我从这里获取的:https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.42.0/dist/umd/supabase.min.js),并创建一个客户端。
var supabase = supabase.createClient(SUPABASE_URL, SUPABASE_KEY)
现在我们准备出发了!
要调用 API 端点,您可以使用.rpc。
我的函数看起来是这样的:
supabase.rpc('get_frames', { frame: num })
.then((data) => {
//do stuff
})
.catch((err) => {
// catch error
})
其中get_frames,`function` 是我之前创建的函数的名称,` { frame: num }variable` 是变量名,`data` 是我想传递给该函数的数据。
至此,我们就完成了!
视频以表情符号编码,存储在数据库中,并通过 Supabase API 提供服务!
编辑/关于表演的说明
正如@mattlewandowski93指出的那样,这样做效率很低,因为我一次请求一帧,这会产生大量的网络请求。
我想测试 Supabase 的延迟,所以这是故意的。
我们可以修改获取帧的函数,使其如下所示:
create or replace function get_all_frames ()
RETURNS table(frametext text)
LANGUAGE plpgsql
as $$
BEGIN
--grab current board
RETURN QUERY SELECT STRING_AGG("rowdata", CHR(13)) AS frame
FROM video2
GROUP BY frameid
ORDER BY frameid ASC;
end;
$$;
一次性返回所有帧,节省网络请求。
这样一来,由于我们一次性拥有所有数据,渲染速度就会快得多(但初始加载时间会更长)。
这是一个我之前没有解释过的重要观点,所以我想在这里补充说明一下我为什么这样做。
你可能会问,为什么?
问得好!
我喜欢通过这类看似无意义的事情来学习。
我以前从未真正使用过 Supabase,他们问我是否愿意为他们写一篇文章来庆祝Supabase 的发布周。
所以我想试试它们。(现在想想,做点“正常”的事情或许更好,这样我就不会用掉我那 5GB 的免费出站流量了,这对于大多数应用程序来说已经很慷慨了,但对于这个来说就不够了!哈哈)。
我相信他们让我写东西的时候,是希望我能写一篇教程或者一些有用的东西,但我们都知道我不会写那种东西!
此外,我还想更多地了解这个<canvas>元素以及如何操作像素数据,因为我有一个更愚蠢的想法来使用它(在浏览器中用表情符号玩 Doom 3 - 是的,我是认真的!🤣)。
总之,希望你也学到了一些东西,但如果没有,我希望你至少能欣赏我的滑稽举动。
哦,还有最后一件事:我用表情符号恶搞了你。
对我来说这是个巨大的胜利,但对你来说估计是更大的失败,所以这也是我这么做的另一个原因!🤣💗
祝大家一周愉快,欢迎在评论区留言告诉我你们的想法。
文章来源:https://dev.to/grahamthedev/video-encoded-as-emojis-stored-in-a-db-mp1




