终端播放视频
文章目录
之前见过使用 telnet towel.blinkenlights.nl
命令在终端中播放《星球大战》。于是想自己也做一个。我选择的是《米奇妙妙屋》的片头。
我的思路是这样的:下载视频 -> 将视频提取成一张张图片 -> 将图片转化为像素画 -> 连续播放像素画 -> 放到服务器上使其他人也可以连接
视频切片
使用 FFmpeg 工具将视频切片。我选择将帧率定为 16,即每秒钟播放 16 张“图片”。如果帧率太高,终端会由于自身绘制速度及网络带宽导致刷新缓慢,进而导致视频看起来“很慢”。最后尝试时,Electerm 以及 Termux 的表现都很差,而 Windows 原生的 Shell(无论是 cmd 还是 Powershell)都有更优秀的表现。
使用 FFmpeg 视频切片的命令如下:
1sudo ffmpeg -i vid/vid.mp4 -vf fps=16 pic/frame_%04d.png
参数 | 解释 |
---|---|
-i |
输入文件 |
-v |
设置视频限制 |
执行完上面的命令,我得到了 1354 张图片,这些图片名称依次为 frame_0001.png 到 frame_1354.png。
图像转字符
我使用 jp2a 工具将图片转化为字符。命令为:
1counter=1;
2for img in pic/frame_*.png; do
3 sudo jp2a --colors --color-depth=24 --height=77 "$img" --fill --chars=" ░" --output="txt/frame_$(printf '%04d' $counter).txt";
4 counter=$((counter + 1));
5done
这些命令将 pic/ 目录下的 frame_.png 图像转化成 txt/ 目录下的 frame_.txt 文本文件。
参数 | 解释 |
---|---|
--colors |
使用真彩色 |
--color-deepth |
色彩深度 |
--height |
生成的图像高度 |
--fill |
使用填充 |
--chars |
使用的字符 |
--output |
输出文件 |
--height
生成的图像高度是指这张 ASCII 图像的行数,只有当终端的行数大于等于这个数值时,这张 ASCII 图像才能被正确显示出来。在 Linux 操作系统中,使用 tput lines
或者 echo $LINES
命令可以查看当前终端的行数。
--fill
是指填满行与行之间的空隙,实际上是给字符添加反色效果。在终端中,相邻两行字符之间有空隙,如果添加了反色 \033[7m
则会将空隙填充。
--chars
是指使用的字符。在这里我使用空格和“浅的阴影”(U+2591),这样两个字符在反色之后色块填充更饱满。
最后生成的文件使用 ANSI 转义序列调控颜色,如果直接使用 cat
命令打开,会看到正常显示的图像;使用 vim
命令编辑则会看到转义字符。
连续播放像素画
前面我知道了使用 cat
命令打开这些文件可以正常显示,那么我只需要依次 cat
这些图像就可以实现播放图像。
1for txt in txt/frame_*.txt; do
2 echo -e "\033[H";
3 cat $txt;
4done
输出的 \033[H
也是一个 ANSI 控制字符,用于将光标移动到终端的最开头。如果使用 clear
命令清空屏幕,则会出现屏幕频闪的效果,而将光标移动到终端开头则直接从上一帧图像上绘制,覆盖上一帧,避免屏幕频闪。
挂到服务器上
我在这一步遇到了一些问题。我最初的想法是将播放“视频”的脚本使用 netcat 上。例如我将上面播放的脚本存储为 TerminalVideo.sh
,随后执行命令 nc -zvlp 6666 -e TerminalVideo.sh
,这样其他机器执行 nc -zv 219.217.199.108 6666
命令就可以播放视频了。但是这样做也有诸多问题:
nc
一次只能建立一个连接,不能实现多用户同时连接- 在脚本运行结束后,
nc
连接会自动断开
我想先尝试解决第二个问题,我运行下面的命令尝试持久化 nc 连接:
1while true; do
2 nc -zvlp 6666 -e TerminalVideo.sh
3done
但是这样又出现了新的问题,如果在脚本运行时,用户按下 ^C 强制退出,那么这条 nc 连接就会失效,服务端会持续报错 “Permission Denied”,客户端无法连接。
于是我又把目光放到了 SSH 上。之前在远程控制一台在内网中的设备里我尝试使用免密码的 SCP 传输文件。但是出于安全考虑,我需要将接受文件的用户的 Shell 改为一个空的 Shell,于是有了下面这段代码:
1void main()
2{
3 while (true) {;}
4}
最后测试证明,这段代码编译出来的程序是可以被用作 Shell 的。于是我现在需要做的是,写一个能够自动播放“视频”的程序,随后将其设为一个用户的默认 Shell,而且这个用户不应该有密码,使任何连接到校园网的用户都可以连接。首先我将 txt/ 目录移动到了 /etc 目录下(最开始选择的是 /tmp 目录,但是很快被系统自动清理了),并且下面一段代码:
1// 6666.cpp
2
3#include <fcntl.h>
4#include <sys/ioctl.h>
5#include <unistd.h>
6#include <iostream>
7#include <fstream>
8
9
10using namespace std;
11
12int get_lines() // 检测终端高度
13{
14 struct winsize ws;
15 int fd, result;
16 if ((fd = open("/dev/tty", O_WRONLY)) < 0) return -1;
17 result = ioctl(fd, TIOCGWINSZ, &ws);
18 close(fd);
19 return result ? -1 : ws.ws_row;
20}
21
22int main()
23{
24 int rows = get_lines();
25 if (rows == -1)
26 {
27 cout << endl;
28 cout << "\033[7;31mUnknown Error!\033[0m" << endl;
29 cout << endl;
30 return -1;
31 }
32
33 else if (rows < 80)
34 {
35 cout << endl;
36 cout << "+---------------------------------------------------------------+" << endl;
37 cout << "| The minimum height of your terminal to play this video is \033[7;31m80\033[0m. |" << endl;
38 cout << "| Use \033[7;33mtput lines\033[0m to check your terminal height. |" << endl;
39 cout << "+---------------------------------------------------------------+" << endl;
40 cout << endl;
41 return -1;
42 }
43
44 for (int i = 1; i <= 1354; i++)
45 {
46 cout << "\033[H";
47 char filename[24];
48 sprintf(filename, "/etc/txt/frame_%04d.txt", i);
49
50 fstream file;
51 file.open(filename, ios::in);
52 string line;
53 while (getline(file, line))
54 {
55 cout << line << endl;
56 }
57 }
58 return 0;
59}
多种编程语言都对 ANSI 控制有支持,C++ 也不例外,因此我直接 cout
控制字符,控制字符就能正确执行其功能。
下一步就是建立一个无需密码就可以登录的用户,并将其默认 Shell 设为我们刚刚写的程序。需要执行以下命令:
1sudo adduser mickey
2sudo passwd -d mickey
随后修改 /etc/ssh/sshd_config
文件,修改其中配置 PermitEmptyPasswords yes
,随后重启 SSH 服务 sudo systemctl restart sshd
使修改生效。
下一步使用命令 sudo chsh -s /bin/6666 mickey
修改默认 Shell。
现在来看,功能已经大体实现。但是还有一个问题,在连接上的时候,服务器会输出 MOTD(Message of the Day),然而这些信息不应该对任何人可见,因此在 mickey 用户的家目录下创建 .hushlogin 文件 sudo touch /home/mickey/.hushlogin
以禁用 MOTD。
最终效果如下:
(没有声音)