mirror of
https://github.com/asciinema/asciinema.git
synced 2025-12-16 03:38:03 +01:00
Add --stream option to play command
This commit is contained in:
@@ -236,7 +236,7 @@ Available options:
|
|||||||
Stdin recording allows for capturing of all characters typed in by the user in
|
Stdin recording allows for capturing of all characters typed in by the user in
|
||||||
the currently recorded shell. This may be used by a player (e.g.
|
the currently recorded shell. This may be used by a player (e.g.
|
||||||
[asciinema-player](https://github.com/asciinema/asciinema-player)) to display
|
[asciinema-player](https://github.com/asciinema/asciinema-player)) to display
|
||||||
pressed keys. Because it's basically a key-logging (scoped to a single shell
|
pressed keys. Because it's basically key-logging (scoped to a single shell
|
||||||
instance), it's disabled by default, and has to be explicitly enabled via
|
instance), it's disabled by default, and has to be explicitly enabled via
|
||||||
`--stdin` option.
|
`--stdin` option.
|
||||||
|
|
||||||
@@ -293,6 +293,11 @@ Available options:
|
|||||||
|
|
||||||
- `-i, --idle-time-limit=<sec>` - Limit replayed terminal inactivity to max `<sec>` seconds
|
- `-i, --idle-time-limit=<sec>` - Limit replayed terminal inactivity to max `<sec>` seconds
|
||||||
- `-s, --speed=<factor>` - Playback speed (can be fractional)
|
- `-s, --speed=<factor>` - Playback speed (can be fractional)
|
||||||
|
- `--stream=<stream>` - Recorded stream to play (see below)
|
||||||
|
|
||||||
|
By default the output stream (`o`) is played. This is what you want in most
|
||||||
|
cases. If you recorded the input stream (`i`) with `asciinema rec --stdin` then
|
||||||
|
you can replay it with `asciinema play --stream=i <filename>`.
|
||||||
|
|
||||||
> For the best playback experience it is recommended to run `asciinema play` in
|
> For the best playback experience it is recommended to run `asciinema play` in
|
||||||
> a terminal of dimensions not smaller than the one used for recording, as
|
> a terminal of dimensions not smaller than the one used for recording, as
|
||||||
|
|||||||
@@ -178,6 +178,11 @@ For help on a specific command run:
|
|||||||
type=positive_float,
|
type=positive_float,
|
||||||
default=cfg.play_speed,
|
default=cfg.play_speed,
|
||||||
)
|
)
|
||||||
|
parser_play.add_argument(
|
||||||
|
"--stream",
|
||||||
|
help="recorded stream to play (o, i)",
|
||||||
|
default="o",
|
||||||
|
)
|
||||||
parser_play.add_argument(
|
parser_play.add_argument(
|
||||||
"filename", help='local path, http/ipfs URL or "-" (read from stdin)'
|
"filename", help='local path, http/ipfs URL or "-" (read from stdin)'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from typing import Any, Generator, List, Optional
|
from typing import Any, Generator, Iterable, List, Optional
|
||||||
|
|
||||||
|
|
||||||
def to_relative_time(
|
def to_relative_time(
|
||||||
events: Generator[List[Any], None, None]
|
events: Iterable[Any],
|
||||||
) -> Generator[List[Any], None, None]:
|
) -> Generator[List[Any], None, None]:
|
||||||
prev_time = 0
|
prev_time = 0
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ def to_relative_time(
|
|||||||
|
|
||||||
|
|
||||||
def to_absolute_time(
|
def to_absolute_time(
|
||||||
events: Generator[List[Any], None, None]
|
events: Iterable[Any],
|
||||||
) -> Generator[List[Any], None, None]:
|
) -> Generator[List[Any], None, None]:
|
||||||
time = 0
|
time = 0
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ def to_absolute_time(
|
|||||||
|
|
||||||
|
|
||||||
def cap_relative_time(
|
def cap_relative_time(
|
||||||
events: Generator[List[Any], None, None], time_limit: Optional[float]
|
events: Iterable[Any], time_limit: Optional[float]
|
||||||
) -> Generator[List[Any], None, None]:
|
) -> Iterable[Any]:
|
||||||
if time_limit:
|
if time_limit:
|
||||||
return (
|
return (
|
||||||
[min(delay, time_limit), type_, data]
|
[min(delay, time_limit), type_, data]
|
||||||
@@ -36,6 +36,6 @@ def cap_relative_time(
|
|||||||
|
|
||||||
|
|
||||||
def adjust_speed(
|
def adjust_speed(
|
||||||
events: Generator[List[Any], None, None], speed: Any
|
events: Iterable[Any], speed: Any
|
||||||
) -> Generator[List[Any], None, None]:
|
) -> Generator[List[Any], None, None]:
|
||||||
return ([delay / speed, type_, data] for delay, type_, data in events)
|
return ([delay / speed, type_, data] for delay, type_, data in events)
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
from codecs import StreamReader
|
from codecs import StreamReader
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
from typing import Any, Dict, Generator, List, Optional, TextIO, Union
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Generator,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
TextIO,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
from .events import to_absolute_time
|
from .events import to_absolute_time
|
||||||
|
|
||||||
@@ -26,16 +35,16 @@ class Asciicast:
|
|||||||
}
|
}
|
||||||
return header
|
return header
|
||||||
|
|
||||||
|
def events(self, type_: Optional[str]) -> Iterable[List[Any]]:
|
||||||
|
if type_ in [None, "o"]:
|
||||||
|
return to_absolute_time(self.__stdout_events())
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
def __stdout_events(self) -> Generator[List[Any], None, None]:
|
def __stdout_events(self) -> Generator[List[Any], None, None]:
|
||||||
for time, data in self.__attrs["stdout"]:
|
for time, data in self.__attrs["stdout"]:
|
||||||
yield [time, "o", data]
|
yield [time, "o", data]
|
||||||
|
|
||||||
def events(self) -> Any:
|
|
||||||
return self.stdout_events()
|
|
||||||
|
|
||||||
def stdout_events(self) -> Generator[List[Any], None, None]:
|
|
||||||
return to_absolute_time(self.__stdout_events())
|
|
||||||
|
|
||||||
|
|
||||||
class open_from_file:
|
class open_from_file:
|
||||||
FORMAT_ERROR: str = "only asciicast v1 format can be opened"
|
FORMAT_ERROR: str = "only asciicast v1 format can be opened"
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ class Asciicast:
|
|||||||
self.v2_header = header
|
self.v2_header = header
|
||||||
self.idle_time_limit = header.get("idle_time_limit")
|
self.idle_time_limit = header.get("idle_time_limit")
|
||||||
|
|
||||||
def events(self) -> Generator[Any, None, None]:
|
def events(self, type_: Optional[str]) -> Generator[List[Any], None, None]:
|
||||||
for line in self.__file:
|
if type_ is None:
|
||||||
yield json.loads(line)
|
for line in self.__file:
|
||||||
|
yield json.loads(line)
|
||||||
def stdout_events(self) -> Generator[List[Any], None, None]:
|
else:
|
||||||
for time, type_, data in self.events():
|
for line in self.__file:
|
||||||
if type_ == "o":
|
event = json.loads(line)
|
||||||
yield [time, type_, data]
|
if event[1] == type_:
|
||||||
|
yield event
|
||||||
|
|
||||||
|
|
||||||
def build_from_header_and_file(
|
def build_from_header_and_file(
|
||||||
@@ -76,7 +77,7 @@ def get_duration(path_: str) -> Any:
|
|||||||
first_line = f.readline()
|
first_line = f.readline()
|
||||||
with open_from_file(first_line, f) as a:
|
with open_from_file(first_line, f) as a:
|
||||||
last_frame = None
|
last_frame = None
|
||||||
for last_frame in a.stdout_events():
|
for last_frame in a.events("o"):
|
||||||
pass
|
pass
|
||||||
return last_frame[0]
|
return last_frame[0]
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class CatCommand(Command):
|
|||||||
with open("/dev/tty", "rt", encoding="utf-8") as stdin:
|
with open("/dev/tty", "rt", encoding="utf-8") as stdin:
|
||||||
with raw(stdin.fileno()):
|
with raw(stdin.fileno()):
|
||||||
with asciicast.open_from_url(self.filename) as a:
|
with asciicast.open_from_url(self.filename) as a:
|
||||||
for _, _type, text in a.stdout_events():
|
for _, _type, text in a.events("o"):
|
||||||
sys.stdout.write(text)
|
sys.stdout.write(text)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PlayCommand(Command):
|
|||||||
self.filename = args.filename
|
self.filename = args.filename
|
||||||
self.idle_time_limit = args.idle_time_limit
|
self.idle_time_limit = args.idle_time_limit
|
||||||
self.speed = args.speed
|
self.speed = args.speed
|
||||||
|
self.stream = args.stream
|
||||||
self.player = player if player is not None else Player()
|
self.player = player if player is not None else Player()
|
||||||
self.key_bindings = {
|
self.key_bindings = {
|
||||||
"pause": config.play_pause_key,
|
"pause": config.play_pause_key,
|
||||||
@@ -28,7 +29,11 @@ class PlayCommand(Command):
|
|||||||
try:
|
try:
|
||||||
with asciicast.open_from_url(self.filename) as a:
|
with asciicast.open_from_url(self.filename) as a:
|
||||||
self.player.play(
|
self.player.play(
|
||||||
a, self.idle_time_limit, self.speed, self.key_bindings
|
a,
|
||||||
|
idle_time_limit=self.idle_time_limit,
|
||||||
|
speed=self.speed,
|
||||||
|
key_bindings=self.key_bindings,
|
||||||
|
stream=self.stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
except asciicast.LoadError as e:
|
except asciicast.LoadError as e:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class Player: # pylint: disable=too-few-public-methods
|
|||||||
idle_time_limit: Optional[int] = None,
|
idle_time_limit: Optional[int] = None,
|
||||||
speed: float = 1.0,
|
speed: float = 1.0,
|
||||||
key_bindings: Optional[Dict[str, Any]] = None,
|
key_bindings: Optional[Dict[str, Any]] = None,
|
||||||
|
stream: str = "o",
|
||||||
) -> None:
|
) -> None:
|
||||||
if key_bindings is None:
|
if key_bindings is None:
|
||||||
key_bindings = {}
|
key_bindings = {}
|
||||||
@@ -22,10 +23,17 @@ class Player: # pylint: disable=too-few-public-methods
|
|||||||
with open("/dev/tty", "rt", encoding="utf-8") as stdin:
|
with open("/dev/tty", "rt", encoding="utf-8") as stdin:
|
||||||
with raw(stdin.fileno()):
|
with raw(stdin.fileno()):
|
||||||
self._play(
|
self._play(
|
||||||
asciicast, idle_time_limit, speed, stdin, key_bindings
|
asciicast,
|
||||||
|
idle_time_limit,
|
||||||
|
speed,
|
||||||
|
stdin,
|
||||||
|
key_bindings,
|
||||||
|
stream,
|
||||||
)
|
)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
self._play(asciicast, idle_time_limit, speed, None, key_bindings)
|
self._play(
|
||||||
|
asciicast, idle_time_limit, speed, None, key_bindings, stream
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _play( # pylint: disable=too-many-locals
|
def _play( # pylint: disable=too-many-locals
|
||||||
@@ -34,23 +42,24 @@ class Player: # pylint: disable=too-few-public-methods
|
|||||||
speed: float,
|
speed: float,
|
||||||
stdin: Optional[TextIO],
|
stdin: Optional[TextIO],
|
||||||
key_bindings: Dict[str, Any],
|
key_bindings: Dict[str, Any],
|
||||||
|
stream: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
idle_time_limit = idle_time_limit or asciicast.idle_time_limit
|
idle_time_limit = idle_time_limit or asciicast.idle_time_limit
|
||||||
pause_key = key_bindings.get("pause")
|
pause_key = key_bindings.get("pause")
|
||||||
step_key = key_bindings.get("step")
|
step_key = key_bindings.get("step")
|
||||||
|
|
||||||
stdout = asciicast.stdout_events()
|
events = asciicast.events(stream)
|
||||||
stdout = ev.to_relative_time(stdout)
|
events = ev.to_relative_time(events)
|
||||||
stdout = ev.cap_relative_time(stdout, idle_time_limit)
|
events = ev.cap_relative_time(events, idle_time_limit)
|
||||||
stdout = ev.to_absolute_time(stdout)
|
events = ev.to_absolute_time(events)
|
||||||
stdout = ev.adjust_speed(stdout, speed)
|
events = ev.adjust_speed(events, speed)
|
||||||
|
|
||||||
base_time = time.time()
|
base_time = time.time()
|
||||||
ctrl_c = False
|
ctrl_c = False
|
||||||
paused = False
|
paused = False
|
||||||
pause_time: Optional[float] = None
|
pause_time: Optional[float] = None
|
||||||
|
|
||||||
for t, _type, text in stdout:
|
for t, _type, text in events:
|
||||||
delay = t - (time.time() - base_time)
|
delay = t - (time.time() - base_time)
|
||||||
|
|
||||||
while stdin and not ctrl_c and delay > 0:
|
while stdin and not ctrl_c and delay > 0:
|
||||||
|
|||||||
Reference in New Issue
Block a user