use std::io::Write;
use std::time::{Duration, Instant};
use super::format::{EventType, Transcript, TranscriptEvent};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum PlaybackSpeed {
#[default]
Realtime,
Speed(f64),
Instant,
}
#[derive(Debug, Clone)]
pub struct PlaybackOptions {
pub speed: PlaybackSpeed,
pub max_idle: Duration,
pub show_input: bool,
pub pause_at_markers: bool,
}
impl Default for PlaybackOptions {
fn default() -> Self {
Self {
speed: PlaybackSpeed::Realtime,
max_idle: Duration::from_secs(5),
show_input: false,
pause_at_markers: false,
}
}
}
impl PlaybackOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_speed(mut self, speed: PlaybackSpeed) -> Self {
self.speed = speed;
self
}
#[must_use]
pub const fn with_max_idle(mut self, max: Duration) -> Self {
self.max_idle = max;
self
}
#[must_use]
pub const fn with_show_input(mut self, show: bool) -> Self {
self.show_input = show;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlayerState {
Stopped,
Playing,
Paused,
Finished,
}
pub struct Player<'a> {
transcript: &'a Transcript,
index: usize,
options: PlaybackOptions,
state: PlayerState,
start_time: Option<Instant>,
last_event_time: Duration,
}
impl<'a> Player<'a> {
#[must_use]
pub fn new(transcript: &'a Transcript) -> Self {
Self {
transcript,
index: 0,
options: PlaybackOptions::default(),
state: PlayerState::Stopped,
start_time: None,
last_event_time: Duration::ZERO,
}
}
#[must_use]
pub const fn with_options(mut self, options: PlaybackOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub const fn state(&self) -> PlayerState {
self.state
}
#[must_use]
pub const fn position(&self) -> usize {
self.index
}
#[must_use]
pub const fn total_events(&self) -> usize {
self.transcript.events.len()
}
#[must_use]
pub fn current_time(&self) -> Duration {
if self.index < self.transcript.events.len() {
self.transcript.events[self.index].timestamp
} else {
self.transcript.duration()
}
}
pub fn play(&mut self) {
self.state = PlayerState::Playing;
self.start_time = Some(Instant::now());
}
pub const fn pause(&mut self) {
self.state = PlayerState::Paused;
}
pub const fn stop(&mut self) {
self.state = PlayerState::Stopped;
self.index = 0;
self.start_time = None;
self.last_event_time = Duration::ZERO;
}
pub fn seek(&mut self, index: usize) {
self.index = index.min(self.transcript.events.len());
if self.index < self.transcript.events.len() {
self.last_event_time = self.transcript.events[self.index].timestamp;
}
}
pub fn next_event(&mut self) -> Option<&TranscriptEvent> {
if self.state != PlayerState::Playing || self.index >= self.transcript.events.len() {
if self.index >= self.transcript.events.len() {
self.state = PlayerState::Finished;
}
return None;
}
let event = &self.transcript.events[self.index];
self.index += 1;
self.last_event_time = event.timestamp;
Some(event)
}
#[must_use]
pub fn delay_to_next(&self) -> Duration {
if self.index >= self.transcript.events.len() {
return Duration::ZERO;
}
let next_time = self.transcript.events[self.index].timestamp;
let delay = next_time.saturating_sub(self.last_event_time);
let delay = match self.options.speed {
PlaybackSpeed::Instant => Duration::ZERO,
PlaybackSpeed::Realtime => delay,
PlaybackSpeed::Speed(mult) => Duration::from_secs_f64(delay.as_secs_f64() / mult),
};
delay.min(self.options.max_idle)
}
pub fn play_to<W: Write>(&mut self, writer: &mut W) -> std::io::Result<()> {
self.play();
while let Some(event) = self.next_event() {
let event_type = event.event_type;
let event_data = event.data.clone();
let delay = self.delay_to_next();
if delay > Duration::ZERO {
std::thread::sleep(delay);
}
match event_type {
EventType::Output => {
writer.write_all(&event_data)?;
writer.flush()?;
}
EventType::Input if self.options.show_input => {
writer.write_all(&event_data)?;
writer.flush()?;
}
EventType::Marker if self.options.pause_at_markers => {
self.pause();
break;
}
_ => {}
}
}
Ok(())
}
}
pub fn play_to_stdout(transcript: &Transcript, options: PlaybackOptions) -> std::io::Result<()> {
let mut player = Player::new(transcript).with_options(options);
let mut stdout = std::io::stdout();
player.play_to(&mut stdout)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transcript::format::TranscriptMetadata;
#[test]
fn player_basic() {
let mut transcript = Transcript::new(TranscriptMetadata::new(80, 24));
transcript.push(TranscriptEvent::output(Duration::ZERO, b"hello"));
transcript.push(TranscriptEvent::output(
Duration::from_millis(100),
b" world",
));
let mut player = Player::new(&transcript);
assert_eq!(player.total_events(), 2);
assert_eq!(player.state(), PlayerState::Stopped);
player.play();
assert_eq!(player.state(), PlayerState::Playing);
let event = player.next_event().unwrap();
assert_eq!(event.data, b"hello");
let event = player.next_event().unwrap();
assert_eq!(event.data, b" world");
assert!(player.next_event().is_none());
assert_eq!(player.state(), PlayerState::Finished);
}
#[test]
fn player_instant_speed() {
let mut transcript = Transcript::new(TranscriptMetadata::new(80, 24));
transcript.push(TranscriptEvent::output(Duration::ZERO, b"a"));
transcript.push(TranscriptEvent::output(Duration::from_secs(10), b"b"));
let player = Player::new(&transcript)
.with_options(PlaybackOptions::new().with_speed(PlaybackSpeed::Instant));
assert_eq!(player.delay_to_next(), Duration::ZERO);
}
}