编程

asciinema player 终端模拟播放器

2783 2021-12-13 15:06:29

关于

asciinema player 是一款由 Javascript 和 Rust 编写的开源终端 session 播放器。不像其他视频播放器,asciinema 播放器并不能播放大型视频文件(.mp4, .webm 等) ,只能播放被称为 asciicasts 的轻型终端 session 文件。 

Asciicast 是一种终端原生输出的截图,在回放期间会被解释,因此该播放器有自己的解释器(该解释器基于 Paul Williams 的可兼容 ANSI 视频终端解析器)。它的输出全兼容的,被广泛用于终端模拟器如 xterm, Gnome Terminal, iTerm 等。

你可以在 asciinema.org 中查看播放效果。

如果你不想依赖于 asciinema.org , 更倾向于自己管理播放器和记录,也很简单。

快速开始

以下示例显示如何在你自己的网站中使用 asciinema player 。

假定你已经通过如下方式获得终端 session 记录文件(terminal session recording file):

  • 使用 asciinema rec demo.cast 命令将 终端session 记录到本地文件 
  • 通过追加 .cast 到 asciicast 页面网址中(比如: https://asciinema.org/a/28307.cast) ,从 asciinema.org 下载现有的记录文件。

在你的 HTML 页面中使用独立的播放器 bundle

从发布页面下载最新版的播放器 Bundle. 你只需要 asciinema-player.min.jsasciinema-player.css 文件.

首先,添加 asciinema-player.min.js, asciinema-player.css.cast 文件到你的站点资源里。以下的 HTML 片段假定这些文件在网络里服务器根目录下

然后添加你 HTML 文档的需要内容,在空 <div> 里面初始化播放器:

<html>
<head>
  ...
  <link rel="stylesheet" type="text/css" href="/asciinema-player.css" />
  ...
</head>
<body>
  ...
  <div id="demo"></div>
  ...
  <script src="/asciinema-player.min.js"></script>
  <script>
    AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'));
  </script>
</body>
</html>

在你自己的应用包里使用播放器

添加 asciinema-player 到你的依赖包:

npm install --save-dev asciinema-player@3.6.3

在页面里添加空 div 元素(如,<div id="demo"></div>),用于包含播放器。

asciinema-player 模块导入,并使用 create 方法

import * as AsciinemaPlayer from 'asciinema-player';
AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'));

最后,引入播放器的 CSS 文件 - 在 npm 包中 dist/bundle/asciinema-player.css

基本用例

要在你的页面中挂载该播放器,请调用 asciinema-player ES 模块中导出的 create 函数,并带上2个参数: ysource (记录 URL),以及要挂载该播放器的容器的 DOM 元素。

AsciinemaPlayer.create(src, containerElement);

在最常见的情况下,记录是从 URL 中提取的,并且是  asciicast 格式。您可以将其作为完整 URL 传递,例如“https://example.com/demo.cast”,或路径,例如 "/demo.cast"

有关将录音加载到播放器中的更多方法,请参阅源 Source

要传递附加选项,请在挂载播放器时,使用 3 个参数变体:

AsciinemaPlayer.create(src, containerElement, opts);

比如,启用循环和选择 Solarized Dark 主题:

AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
  loop: true,
  theme: 'solarized-dark'
});

可用选项的完整列表请查看选项。

如果您想以编程方式控制播放器,可以使用 create 函数返回的对象暴露的函数:

const player = AsciinemaPlayer.create(src, containerElement);

player.play();

更多详情请查看 API

Source

虽然将记录加载到播放器中最简单的方法是使用 asciicast 文件 URL,但也很容易自定义加载过程,甚至完全替换它。

嵌入记录

如果录制文件很小,并且您希望避免额外的 HTT P请求,则可以使用数据 URL 内联嵌入记录:

AsciinemaPlayer.create('data:text/plain;base64,' + base64encodedAsciicast, containerElement);

比如:

AsciinemaPlayer.create(
  'data:text/plain;base64,eyJ2ZXJzaW9uIjogMiwgIndpZHRoIjogODAsICJoZWlnaHQiOiAyNH0KWzAuMSwgIm8iLCAiaGVsbCJdClswLjUsICJvIiwgIm8gIl0KWzIuNSwgIm8iLCAid29ybGQhXG5cciJdCg==',
  document.getElementById('demo')
);

从另外的源加载记录

如果您想自己加载一段记录并将其传递给播放器,请使用 {data:data} 对象作为源参数来创建:

AsciinemaPlayer.create({ data: data }, containerElement);

此处 data 可以是:

  • 包含 v1 或 v2 格式 asciicast 的字符串
  • 以 v1 格式表示 asciicast 的对象

以 v2 格式表示 asciicast 的数组

一个函数,当被调用时返回上面的其中一个(可能是异步的)

如果 data 是函数,则播放器在用户开始播放时调用它。如果使用了预加载选项,则在播放器初始化(在 DOM 中挂载)期间调用该函数。

默认情况下,提供的 data 使用内置的 asciicast 格式解析器进行解析(另请参阅播放其他录制格式)。

支持的 data 规范示例:

// object representing asciicast in v1 format
{version: 1, width: 80, height: 24, stdout: [[1.0, "hello "], [1.0, "world!"]]};

 

// string representing asciicast in v1 format (json)
'{"version": 1, "width": 80, "height": 24, "stdout": [[1.0, "hello "], [1.0, "world!"]]}';

 

// array representing asciicast in v2 format
[
  {version: 2, width: 80, height: 24},
  [1.0, "o", "hello "],
  [2.0, "o", "world!"]
]

 

// string representing asciicast in v2 format (ndjson)
'{"version": 2, "width": 80, "height": 24}\n[1.0, "o", "hello "]\n[2.0, "o", "world!"]';

 

// function returning a string representing asciicast in v2 format (ndjson)
() => '{"version": 2, "width": 80, "height": 24}\n[1.0, "o", "hello "]\n[2.0, "o", "world!"]';

假设您想在页面上的(隐藏的)HTML 标记中嵌入 asciicast 内容,可以使用以下数据源提取并将其传递给播放器:

AsciinemaPlayer.create(
  { data: document.getElementById('asciicast').textContent.trim() },
  document.getElementById('demo')
);

自定义 URL 获取

如果你想从 URL 获取记录,但需要调整 HTTP 请求的执行方式(配置凭据、更改 HTTP 方法等),可以使用 { url: "...", fetchOpts: { ... } } 对象作为源参数。fetchOpts 对象然后被传递给 fetch(作为它的第二个参数)。

比如:

AsciinemaPlayer.create(
  { url: url, fetchOpts: { method: 'POST' } },
  containerElement
);

或者,您可以使用自定义数据源(如前一节所述)并自己调用 fetch:

AsciinemaPlayer.create(
  { data: () => fetch(url, { method: 'POST' }) },
  containerElement
);

播放其他录制格式

默认情况下,使用内置的 asciicast 格式解析器解析录制。

如果您有其他终端会话记录工具(如 script、termrec、ttyrec)生成的记录,您可以使用内置的文件格式解析器之一,或者实现自定义解析器功能。

录制格式解析器可以在 AsciinemaPlayer.create 的 source 参数中指定为字符串(内置)或函数(自定义):

AsciinemaPlayer.create({ url: url, parser: parser }, containerElement);

有关可用的内置解析器以及如何实现自定义解析器的信息,请参阅解析器。

选项

下面是可用于调整播放器的外观及感觉:

cols

类型: number

播放器终端的列数。

未设置时,默认值为 80(直到加载 asciicast)和保存在 asciicast 文件中的终端宽度(加载后)。

建议将其设置为与 asciicast 文件中的值相同的值,以避免播放器在加载时将其大小从 80x24 调整为录制的实际尺寸。

rows

类型: number

播放器终端的行数。

未设置则默认为 24 (直到加载 asciicast) 及保存在 asciicast 文件中的终端高度 (加载后)。

cols 的建议同样适用于此。

autoPlay

类型: boolean

自动播放则设置该选项为 true

默认为 false - 不自动播放。

preload

类型: boolean

如果记录要在播放器初始化时预加载,请将该选项设置为 true

默认为 false - 不预加载。

loop

类型: boolean 或 number

如果要循环播放,请将该选项设置为 true 或者数字。当设为数字时,将会按照给定次数循环播放,然后停止。

默认为 false - 不循环播放。

startAt

类型: number 或 string

在给定的时间播放。

支持的格式:

  • 123 (秒数)
  • "2:03" ("mm:ss")
  • "1:02:03" ("hh:mm:ss")

默认为 0.

speed

类型: number

播放速度。值为 2 则意味着 2x 速度。

默认为 1 - 正常速度。

idleTimeLimit

类型: number

将终端非激活状态限制为给定的秒数。

比如,设置为 2 时,任何高于 2 秒的非激活状态(暂停),都将被”压缩“成 2 秒。

默认:

  • 如果没有在记录时指定,asciicast 标头的 idle_time_limit (当传递 -i <sec>asciinema rec),
  • 没有限制。

theme

类型: string

终端颜色主题。

其中一个:

  • "asciinema"
  • "dracula"
  • "monokai"
  • "nord"
  • "solarized-dark"
  • "solarized-light"
  • "tango"

默认为 "asciinema".

你也可用使用自定义主题。

poster

类型: string

Poster (预览帧) 用来在播放开始前显示。

支持以下的 poster 规范:

  • npt:1:23 - 使用 NPT(普通播放时间)注释显示给定时间的记录”帧“
  • data:text/plain,Poster text - 打印给定的文本

指定 poster 最简单的方法是使用 NPT 格式。比如, npt:1:23 将预载并显示 1 分 23 秒时的终端内容。

示例:

AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
  poster: 'npt:1:23'
});

此外,poster 值为 data:text/plain,This will be printed as poster\n\rThis in second line 将显示文本。所有的 ANSI 转义代码可用于添加颜色及移动光标,以生成美观的 poster。

通过控制系列(又名转义码)使用自定义文本 poster 的示例:

AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
  poster: "data:text/plain,I'm regular \x1b[1;32mI'm bold green\x1b[3BI'm 3 lines down"
});

默认为空白,或者指定 startAt 时,为在 startAt 指定的时间的屏幕内容。

fit

类型: string

关于播放器容器元素的适配(调整大小)行为选择。

可能的值:

  • "width" - 缩放到容器的全宽
  • "height" - 缩放到容器的全高(要求容器元素有固定高度)
  • "both" - 缩放到全宽或劝告,最大化可用空间(要求容器元素有固定高度)
  • false / "none" - 不缩放,使用固定大小字体 (也可参考下面的 fontSize 选项)

默认为

 "width".

Version 2.x of the player supported only the behaviour represented by the false value. If you're upgrading from v2 to v3 and want to preserve the sizing behaviour then include fit: false option.

controls

类型: boolean 或 "auto"

隐藏或者显示用户控制,即底部控制条。

有效值:

  • true - 总是显示控制
  • false - 从不显示控制
  • "auto" - 鼠标移动时,显示控制;否则隐藏。

默认为 "auto".

markers

类型: array

允许提供一个时间线标记列表。更多信息参考后续标记。

没有标签的 marker 示例:

AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
  markers: [5.0, 25.0, 66.6, 176.5]  // time in seconds
});

有标签 marker 示例:

AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
  markers: [
    [5.0,   "Installation"],  // time in seconds + label
    [25.0,  "Configuration"],
    [66.6,  "Usage"],
    [176.5, "Tips & Tricks"]
  ]
});

带有这个选项的 marker 集将覆盖嵌入 asciicast 文件中的所有 marker。

默认为在录制文件中设置的标记。

pauseOnMarkers

类型: boolean

如果 pauseOnMarkers 设置为 true,播放会在遇到的每个标记上自动暂停,并且可以通过按下空格键或单击播放按钮来恢复播放。继续播放,直到遇到下一个标记为止。

这个选项在例如现场演示中很有用:你可以在记录中添加标记,然后在演示过程中播放,并让播放器停在任何你想更详细地解释终端内容的地方。

默认为 false

terminalFontSize

类型: string

终端字体的大小。

只有当 fit:false 选项也是指定时(见上),该选项才有效。

可能值:

  • 任何有效的 CSS font-size 值,比: "15px"
  • "small"
  • "medium"
  • "big"

默认为 "small".

terminalFontFamily

类型: string

终端 font-family 重写。

使用任何有效的 CSS font-family 值,比如 “'JetBrains Mono', Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace”。更多使用自定义字体的信息,请查看字体。S

terminalLineHeight

类型: number

终端行高重写。

该值与字体大小相关(像 CSS 中的 em 单位)。比如,值为 1 则行高与字体大小相同,行之间没有空白。值为 2 则行高是字体大小的两部等。

默认为 1.33333333.

logger

类型: 类似控制台的对象

将此选项设置为 console ({ logger: console }) 或者任何实现 console API (.log(), .debug(), .info(), .warn(), .error() 方法) 的对象,以启用日志。开发或者调试播放器问题时有用。

API

import * as AsciinemaPlayer from 'asciinema-player';
// skip the above import when using standalone player bundle

const player = AsciinemaPlayer.create(url, containerElement);

create 函数返回的对象 (上面保存为 player 常量) 包含了多个可用于从代码中控制播放器的函数。

比如,开始播放并在开始时打印记录的长度:

player.play().then(() => {
  console.log(`started! duration: ${player.getDuration()}`);
});

以下函数在播放器对象中可用:

getCurrentTime()

返回当前播放的时间,以秒计。

player.getCurrentTime(); // => 1.23

getDuration()

返回记录的长度,以秒计;如果记录还未加载则返回 null

player.getDuration(); // => 123.45

play()

开始记录的播放。如果记录还没预加载则加载记录,并开始播放。

player.play();

该函数返回一个 promise,其在实际开始播放时执行。

player.play().then(() => {
  console.log(`started! duration: ${player.getDuration()}`);
});

如果你想将 ascinema 播放器与页面上的其他元素(例如 <audio>元素)同步,那么你可以使用这个 promise 进行协调。或者,您可以为 play/playing 播放事件添加事件监听器(请参阅下文)。

pause()

暂停播放。

player.pause();

播放立即暂停。

seek(location)

将播放位置改为指定的时间或者标记处。

location 参数可以是:

  • 以秒计的时间,数字,比如 15
  • 百分比位置,字符串,比如 '50%'
  • specific marker by its 0-based index, as { marker: i } object, e.g. { marker: 3 }
  • 前一个标志位, as { marker: 'prev' } object,
  • 下一个标志位, as { marker: 'next' } object.

该函数返回一个 promise,它会在位置实际改变时执行。

player.seek(15).then(() => {
  console.log(`current time: ${player.getCurrentTime()}`);
});

addEventListener(eventName, handler)

添加事件监听器,将 handler 的 this 绑定到播放器对象。

play 事件

play 事件在播放初始化时发送,要么是点击播放按钮,或者调用 player.play() 方法,但是还没开始时。

player.addEventListener('play', () => {
  console.log('play!');
})

playing 事件

playing 事件在实际播放开始或者从暂停中恢复时发送。e

player.addEventListener('playing', () => {
  console.log(`playing! we're at: ${this.getCurrentTime()}`);
})

pause 事件

pause 事件在暂停时派发。

player.addEventListener('pause', () => {
  console.log("paused!");
})

ended 事件

ended 事件在记录到达结尾时停止播放时发送。

player.addEventListener('ended', () => {
  console.log("ended!");
})

input 事件

input event is dispatched for every keyboard input that was recorded.

Callback's 1st argument is an object with data field, which contains registered input value. Usually this is ASCII character representing a key, but may be a control character, like "\r" (enter), "\u0001" (ctrl-a), "\u0003" (ctrl-c), etc. See input events in asciicast file format for more information.

This event can be used to play keyboard typing sound or display key presses on the screen amongst other use cases.

player.addEventListener('input', ({data}) => {
  console.log('input!', JSON.stringify(data));
})

inputOffset source option can be used to shift fired input events in time, e.g. when you need them to fire earlier due to audio latency etc:

const player = AsciinemaPlayer.create({
  url: '/demo.cast',
  inputOffset: -1.0
}, document.getElementById('demo'));

player.addEventListener('input', ({data}) => {
  // this is now fired 1 sec ahead of original key press time
  playSound(data);
})

Note: input events are dispatched only for asciicasts recorded with --stdin option, e.g. asciinema rec --stdin demo.cast.

marker 事件

marker event is dispatched for every marker encountered during playback.

Callback's 1st argument is an object with index, time and label fields, which represent marker's index (0-based), time and label respectively.

The following example shows how to implement looping over a section of a recording with combination of marker event and seek method:

player.addEventListener('marker', ({ index, time, label }) => {
  console.log(`marker! ${index} - ${time} - ${label}`);

  if (index == 3) {
    player.seek({ marker: 2 });
  }
})

dispose()

使用此函数清理播放器,即关闭播放器,释放所有资源并将其从 DOM 中删除。

字体

默认情况下,播放器通过将 font-family 值设置为像 “Consolas, Menlo, 'Bitstream Vera Sans Mono', monospace” 这样,使用网络安全、平台指定的 monospace 字体。

你可以通过在 CSS 中添加 @font-face 定义并且使用 terminalFontFamily 选项调用 AsciinemaPlayer.create 来使用任何自定义的 monospace 字体。常规字体是必要的,粗体(weight 700)是推荐的,斜体是可选的(斜体很少用于终端)。

如果你要在 shell 中使用图标或者其他符号,你可以使用 Nerd 字体。

比如要使用 Fira Code Nerd 字体,请试试这样:

/* app.css */

@font-face {
    font-family: "FiraCode Nerd Font";
    src:    local(Fira Code Bold Nerd Font Complete Mono),
            url("/fonts/Fira Code Bold Nerd Font Complete Mono.ttf") format("truetype");
    font-stretch: normal;
    font-style: normal;
    font-weight: 700;
}

@font-face {
    font-family: "FiraCode Nerd Font";
    src:    local(Fira Code Regular Nerd Font Complete Mono),
            url("/fonts/Fira Code Regular Nerd Font Complete Mono.ttf") format("truetype");
    font-stretch: normal;
}

 

// app.js

AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
  terminalFontFamily: "'FiraCode Nerd Font', monospace"
});

请注意,播放器在安装到页面上时会执行字体度量(宽度/高度)的测量,因此强烈建议在调用 create 之前确保已加载所选字体。这可以通过使用 CSS 字体加载 API 来实现:

document.fonts.load("1em FiraCode Nerd Font").then(() => {
  AsciinemaPlayer.create('/demo.cast', document.getElementById('demo'), {
    terminalFontFamily: "'FiraCode Nerd Font', monospace"
  });
}

标记

标记(marker)是记录时间线上的特定点,可用于记录中的导航或播放器的自动化。

有几种方法可以指定在播放器中使用的标记:

AsciinemaPlayer.create 中使用 marker 选项,

  • 在记录中嵌入标记 - 请参阅 asciinema 记录器文档中的标记。
  • 另请参见标记事件。

键盘快捷键

以下键盘快捷键当前可用(播放器元素聚焦):

  • 空格 - 播放 / 暂停
  • f - 切换全屏模式
  • ← / → - 后退 5 秒 / 前进 5 秒
  • Shift + ← / Shift + → - 后退 10% / 前进 10%
  • [ - 后退到前一个标记
  • ] - 前进到下一个标记
  • 0, 1, 2 ... 9 - 跳转到 0%, 10%, 20% ... 90%
  • . - 步进记录一次一帧(暂停时)

开发

该项目需要 Node.jsnpmRust,方可开发和编译相关任务,因此请确保安装了最新版本。 

要编译该项目:

git clone https://github.com/asciinema/asciinema-player
cd asciinema-player
git submodule update --init
rustup target add wasm32-unknown-unknown
npm install
npm run build
npm run bundle

这生成:

  • dist/index.js - ES 模块, 要导入到您的 JS 捆绑包中
  • dist/bundle/asciinema-player.js - 单独的 player 脚本,直接从网站链接
  • dist/bundle/asciinema-player.min.js - 上述的minimized 版本
  • dist/bundle/asciinema-player.css - 样式表,直接从网站中链接或包含在 CSS 捆绑包中
  •