发布于 2026-01-06 11 阅读
0

视频编码成表情符号⁉️ 存储在数据库中?😱🤯 由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

视频被编码成表情符号⁉️ 存储在数据库里?😱🤯

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

好吧,连我自己都觉得我这次做得太过分了!

我们能否将视频转换为表情符号(虽然有点傻),然后将这些表情符号保存到数据库中(每行“像素”对应一行),并从数据库中提供“图像”……显然可以!

你该这样做吗?不。

好玩吗?有点儿。

它会让你微笑吗?我希望如此,这比我想象的要费劲一些!

为了让你们明白这件事有多荒谬,我不得不:

  • 将视频转换为低分辨率帧
  • 读取每一帧并获取像素数据
  • 将像素数据转换为表情符号
  • 将这些表情符号存储在数据库(Postgres,使用 Supabase)中。
  • 创建一个存储过程来检索帧,以便我可以快速连续地进行 API 调用来播放“视频”。

这里有很多东西需要解读,我们也可以在此过程中学到一些有趣的东西,但首先……让我们来看演示!

点击下方 CodePen 中的大播放按钮,即可欣赏它的完整效果!

演示

警告:这将使用 20MB 的数据流量,因此如果您使用的是 4G 网络,并且您有昂贵的数据流量套餐,请不要点击播放!

该实验最令人惊叹的部分是,我从 Supabase 逐帧单独调用每一帧,不得不承认,延迟非常惊人!

关于我如何存储数据(以及如何创建数据),稍后会详细介绍。

在演示之前还有最后一点需要说明。

我们不把视频存储为表情符号是有原因的,因为它会占用大量空间。

由于上述演示是在 Supabase 的免费套餐上进行的,因此它可能会突然停止工作,因为 5GB 的出口流量只允许视频播放 250 次,之后我就会用掉太多数据!

如果你按下播放键后没有任何反应,那可能是我达到了播放次数上限,以下是你原本会看到的视频:

这个实验的有趣之处。

*和往常一样,这不是教程。*

但是,在此过程中有一些有趣的事情可能对参考有用:

数据转换与存储

这里有几点值得思考。

你看,将像素数据存储为表情符号效率并不高(我之前已经提到过这一点)。

因此,我们需要找到一种方法来最大限度地减少我们存储的数据量。

将视频转换成帧并进行降采样后,我得到了这样的结果:

Rick Astley 的《Never Gonna Give You Up》单帧画面,两侧有宽大的黑边

但这样做存在一些问题。

去除黑条。

啊,80年代,那时候电视机几乎是正方形的,鲻鱼头很流行。

可惜的是,自那时以来,有些事情发生了变化。

我们现在采用 16:9 而不是 4:3 的宽高比,这意味着我们需要应对一些巨大的黑边。

把这些存储为表情符号毫无意义。

现在,我们稍后会介绍如何检索像素数据,但你只需要知道,我是逐行检索的,并且遍历每一行中的每个像素。

所以为了去除那些黑边,我有很多方法可以做到。

但我喜欢简单,所以我的解决方案是这样的:

f(pixelCounter > 36 && pixelCounter < 219){
  //do something with the pixel data otherwise ignore it.
}
Enter fullscreen mode Exit fullscreen mode

由于我的图像宽度为 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.
Enter fullscreen mode Exit fullscreen mode

这样一来,我们只需要采样 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语句,用于将我们的图像数据插入数据库表中。

如果您想测试一下,可以下载这张图片(因为它仅适用于这种特定尺寸的图片,并且只能根据这些图片去除黑边)。

Rick Astley 与黑条采样

这里有一个 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; 
$$;
Enter fullscreen mode Exit fullscreen mode

看起来可能很复杂,但如果我们把它分解开来,其实并没有那么糟糕!

create or replace function get_frames (frame integer) 
Enter fullscreen mode Exit fullscreen mode

第一部分让我们定义一个可重用的函数。这很重要,因为我们之后可以将此函数用作 API 端点!

RETURNS table(frametext text)
LANGUAGE plpgsql 
as $$
Enter fullscreen mode Exit fullscreen mode

这里我们定义返回类型。括号中的部分对应于我们要返回的列(如果有多列,我们会为每一列设置列名和类型)。

RETURN QUERY SELECT STRING_AGG("rowdata", CHR(13)) AS frame
FROM video2
WHERE frameid = frame
GROUP BY frameid
ORDER BY frameid ASC;
end; 
$$;
Enter fullscreen mode Exit fullscreen mode

最后一部分是我们的 SQL 查询(并将其返回,因为这是一个函数)。

STRING_AGG你可能不太熟悉这个函数。它的功能很简单,就是将列和字符连接起来。我们获取rowdata数据,然后将其与CHR(13)换行符组合,生成输出字符串。

但要让这个函数正常工作,关键在于这个GROUP BY语句。如果没有它,STRING_AGG函数就无法运行,因为它不知道应该将多行数据聚合在一起。

然后我们只需要ORDER BY——这只是为了确保我们按顺序获取每一行数据,以便图像有意义!

创建我们的查询和 API 端点

Supabase 的妙处在于,现在我们只需一步就能将该函数用作 API 端点。

我们只需要一个访问控制策略。

您可以在项目的“身份验证”>“策略”下找到它。

我对 Postgres 还不太熟悉,但幸运的是 Supabase 提供了一个现成的实用模板,让我可以授予读取权限。

Supabase 控制面板截图,启用所有用户的读取权限模板选项

这样一来,我现在就可以将该函数作为 API 端点调用了!

(如果您不确定在哪里可以找到该信息,它位于“数据库”>“函数”中,您会看到类似这样的行:)

包含列的数据行

调用我们的 API 端点

我们最不需要的就是获取数据。

为此,我们需要我们的SUPABASE_URL和我们的密钥(SUPABASE_KEY)。

由于我是 Supabase 新手,所以花了一点时间才找到,但它们位于“项目设置”>“API”下。

Supabase 项目 URL 和项目 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)
Enter fullscreen mode Exit fullscreen mode

现在我们准备出发了!

要调用 API 端点,您可以使用.rpc

我的函数看起来是这样的:

supabase.rpc('get_frames', { frame: num })
    .then((data) => {
       //do stuff
    })
    .catch((err) => {
      // catch error
    })

Enter fullscreen mode Exit fullscreen mode

其中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; 
$$;
Enter fullscreen mode Exit fullscreen mode

一次性返回所有帧,节省网络请求。

这样一来,由于我们一次性拥有所有数据,渲染速度就会快得多(但初始加载时间会更长)。

这是一个我之前没有解释过的重要观点,所以我想在这里补充说明一下我为什么这样做。

你可能会问,为什么?

问得好!

我喜欢通过这类看似无意义的事情来学习。

我以前从未真正使用过 Supabase,他们问我是否愿意为他们写一篇文章来庆祝Supabase 的发布周

所以我想试试它们。(现在想想,做点“正常”的事情或许更好,这样我就不会用掉我那 5GB 的免费出站流量了,这对于大多数应用程序来说已经很慷慨了,但对于这个来说就不够了!哈哈)。

我相信他们让我写东西的时候,是希望我能写一篇教程或者一些有用的东西,但我们都知道我不会写那种东西!

此外,我还想更多地了解这个<canvas>元素以及如何操作像素数据,因为我有一个更愚蠢的想法来使用它(在浏览器中用表情符号玩 Doom 3 - 是的,我是认真的!🤣)。

总之,希望你也学到了一些东西,但如果没有,我希望你至少能欣赏我的滑稽举动。

哦,还有最后一件事:我用表情符号恶搞了你

对我来说这是个巨大的胜利,但对你来说估计是更大的失败,所以这也是我这么做的另一个原因!🤣💗

祝大家一周愉快,欢迎在评论区留言告诉我你们的想法。

文章来源:https://dev.to/grahamthedev/video-encoded-as-emojis-stored-in-a-db-mp1