defmodule TTYCast.Replay do
@moduledoc false
alias TTYCast.Terminal
@spec snapshot(TTYCast.Cast.t(), keyword()) :: {:ok, binary() | map()} | {:error, term()}
def snapshot(cast, opts \\ []) do
format = Keyword.get(opts, :format, :plain)
target_us = target_time_us(cast, opts)
max_scrollback = Keyword.get(opts, :max_scrollback, 20_000)
with {:ok, terminal} <-
Terminal.start(cast.header.width, cast.header.height, max_scrollback: max_scrollback) do
try do
{start_us, replay_events} = restore_keyframe(cast, terminal, target_us)
replay_events
|> Enum.filter(&(TTYCast.Events.event_time(&1) > start_us))
|> replay_until(terminal, target_us)
Terminal.snapshot(terminal, format)
after
Terminal.stop(terminal)
end
end
end
defp target_time_us(cast, opts) do
cond do
time_us = Keyword.get(opts, :time_us) -> time_us
time_ms = Keyword.get(opts, :time_ms) -> time_ms * 1_000
time = Keyword.get(opts, :time) -> trunc(time * 1_000_000)
true -> Map.get(cast.index, :duration_us, 0)
end
end
defp restore_keyframe(cast, terminal, target_us) do
case nearest_keyframe(cast, target_us) do
nil ->
{0, TTYCast.Events.list(cast, end_us: target_us)}
{keyframe_us, vt} ->
:ok = Terminal.write(terminal, vt)
{keyframe_us, TTYCast.Events.list(cast, start_us: keyframe_us, end_us: target_us)}
end
end
defp nearest_keyframe(cast, target_us) do
cast.index.chunks
|> Enum.take_while(&(&1.start_t_us <= target_us))
|> Enum.reverse()
|> Enum.find_value(fn chunk_index ->
case TTYCast.read_chunk!(cast, chunk_index) do
%{keyframe: %{t_us: keyframe_us, vt: vt}} when keyframe_us <= target_us ->
{keyframe_us, vt}
_chunk ->
nil
end
end)
end
defp replay_until(events, terminal, target_us) do
events
|> Enum.take_while(&(TTYCast.Events.event_time(&1) <= target_us))
|> Enum.each(fn
{:output, _t_us, bytes} -> Terminal.write(terminal, bytes)
{:resize, _t_us, columns, rows} -> Terminal.resize(terminal, columns, rows)
_event -> :ok
end)
end
end