Support fast forwarding and rewinding
This commit is contained in:
parent
d6824a35ba
commit
f6c9c5e976
|
@ -19,8 +19,8 @@ go get gitlab.com/tslocum/asciinema-editor
|
|||
Execute `asciinema-editor` twice to edit a screencast.
|
||||
|
||||
```bash
|
||||
asciinema-editor --viewer # Start viewer
|
||||
asciinema-editor /tmp/path/to/screen.cast # Start editor
|
||||
asciinema-editor --viewer # Start the viewer
|
||||
asciinema-editor /tmp/path/to/screen.cast # Start the editor
|
||||
```
|
||||
|
||||
**Tip:** A terminal multiplexer such as [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/)
|
||||
|
|
102
editor.go
102
editor.go
|
@ -18,6 +18,10 @@ var (
|
|||
|
||||
editorPaused bool
|
||||
statusBuffer *cview.TextView
|
||||
|
||||
editorCursor time.Duration
|
||||
editorCursorTime time.Time
|
||||
editorStatus string
|
||||
)
|
||||
|
||||
func handleEditor() {
|
||||
|
@ -26,9 +30,9 @@ func handleEditor() {
|
|||
for c := range commandsIn {
|
||||
switch c.Type {
|
||||
case commandStatus:
|
||||
// TODO cache sent time and update realtime
|
||||
statusBuffer.SetText(fmt.Sprintf("%d:%02d", int(c.D.Minutes())%60, int(c.D.Seconds())%60))
|
||||
app.Draw()
|
||||
editorCursor = c.D
|
||||
editorCursorTime = time.Now()
|
||||
editorStatus = c.S
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,37 +52,52 @@ func connectToViewer(address string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runEditor(viewer, single, force bool, controlAddress string) {
|
||||
func updateStatus() {
|
||||
cursor := editorCursor
|
||||
if !editorPaused && editorStatus == "playing" {
|
||||
cursor += time.Since(editorCursorTime)
|
||||
}
|
||||
statusBuffer.SetText(fmt.Sprintf("%d:%02d", int(cursor.Minutes())%60, int(cursor.Seconds())%60))
|
||||
}
|
||||
|
||||
func handleUpdateUI() {
|
||||
t := time.NewTicker(time.Second)
|
||||
for range t.C {
|
||||
updateStatus()
|
||||
app.Draw()
|
||||
}
|
||||
}
|
||||
|
||||
func runEditor(controlAddress string, force bool) {
|
||||
filePath := flag.Arg(0)
|
||||
if filePath == "" {
|
||||
log.Fatal("supply the path to an asciinema .cast file")
|
||||
}
|
||||
|
||||
err := loadCast(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load asciinema screencast: %s", err)
|
||||
}
|
||||
|
||||
err = connectToViewer(controlAddress)
|
||||
if err != nil {
|
||||
if !single {
|
||||
log.Fatalf("failed to connect to viwer: %s", err)
|
||||
if !single {
|
||||
err := connectToViewer(controlAddress)
|
||||
if err != nil {
|
||||
if !single {
|
||||
log.Fatalf("failed to connect to viwer: %s", err)
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
|
||||
app = cview.NewApplication()
|
||||
err = app.Init()
|
||||
err := app.Init()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
app.QueueUpdate(func() {
|
||||
w, h := app.GetScreenSize()
|
||||
if (uint(w) < loadedCast.Header.Width || uint(h) < loadedCast.Header.Height) && !force {
|
||||
log.Fatalf("recording dimensions (%dx%d) are larger than the current terminal (%dx%d). add --force to force playback", loadedCast.Header.Width, loadedCast.Header.Height, w, h)
|
||||
}
|
||||
})
|
||||
if single {
|
||||
app.QueueUpdate(func() {
|
||||
w, h := app.GetScreenSize()
|
||||
if (uint(w) < loadedCast.Header.Width || uint(h) < loadedCast.Header.Height) && !force {
|
||||
log.Fatalf("recording dimensions (%dx%d) are larger than the current terminal (%dx%d). add --force to force playback", loadedCast.Header.Width, loadedCast.Header.Height, w, h)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.EnableMouse(true)
|
||||
|
||||
|
@ -86,19 +105,6 @@ func runEditor(viewer, single, force bool, controlAddress string) {
|
|||
|
||||
app.SetRoot(statusBuffer, true)
|
||||
|
||||
var cursor time.Duration
|
||||
|
||||
go func() {
|
||||
if single {
|
||||
app.Suspend(playFunc(cursor))
|
||||
} else if !viewer {
|
||||
sendCommand(&command{commandLoad, 0, filePath})
|
||||
sendCommand(&command{commandPlay, 60 * time.Second, ""})
|
||||
}
|
||||
}()
|
||||
|
||||
// TODO loop, poll for current timestamp in commandsIn
|
||||
|
||||
quit := func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
app.Stop()
|
||||
return nil
|
||||
|
@ -110,6 +116,7 @@ func runEditor(viewer, single, force bool, controlAddress string) {
|
|||
}
|
||||
|
||||
doPlay := func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
editorCursorTime = time.Now()
|
||||
sendCommand(&command{commandPlay, 0, ""})
|
||||
return nil
|
||||
}
|
||||
|
@ -119,20 +126,47 @@ func runEditor(viewer, single, force bool, controlAddress string) {
|
|||
if editorPaused {
|
||||
sendCommand(&command{commandPause, 0, ""})
|
||||
} else {
|
||||
editorCursorTime = time.Now()
|
||||
sendCommand(&command{commandResume, 0, ""})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
doFastForward := func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
c := editorCursor + 5*time.Second
|
||||
editorCursor = c
|
||||
sendCommand(&command{commandPlay, c, ""})
|
||||
updateStatus()
|
||||
return nil
|
||||
}
|
||||
|
||||
doRewind := func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
c := editorCursor - 5*time.Second
|
||||
if c < 0 {
|
||||
c = 0
|
||||
}
|
||||
editorCursor = c
|
||||
sendCommand(&command{commandPlay, c, ""})
|
||||
updateStatus()
|
||||
return nil
|
||||
}
|
||||
|
||||
inputConfig := cbind.NewConfiguration()
|
||||
inputConfig.Set("Escape", quit)
|
||||
inputConfig.Set("Ctrl+c", quit)
|
||||
inputConfig.Set("Ctrl+d", doInterrupt)
|
||||
inputConfig.Set("Space", doPause)
|
||||
inputConfig.Set("Enter", doPlay)
|
||||
inputConfig.Set("Left", doRewind)
|
||||
inputConfig.Set("Right", doFastForward)
|
||||
|
||||
app.SetInputCapture(inputConfig.Capture)
|
||||
|
||||
go handleUpdateUI()
|
||||
|
||||
castCommand <- &command{commandLoad, 0, filePath}
|
||||
castCommand <- &command{commandPlay, 0, ""}
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
28
main.go
28
main.go
|
@ -3,14 +3,19 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
single bool
|
||||
viewer bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
force bool
|
||||
viewer bool
|
||||
single bool
|
||||
controlAddress string
|
||||
)
|
||||
flag.BoolVar(&force, "force", false, "force playback with insufficiently sized terminal")
|
||||
|
@ -23,10 +28,27 @@ func main() {
|
|||
controlAddress = path.Join(os.TempDir(), "asciinema-editor.sock")
|
||||
}
|
||||
|
||||
if viewer || single {
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-sigc
|
||||
interruptPlayback()
|
||||
resetScreen()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
go handleCast()
|
||||
|
||||
if viewer {
|
||||
runViewer(controlAddress)
|
||||
return
|
||||
}
|
||||
|
||||
runEditor(viewer, single, force, controlAddress)
|
||||
runEditor(controlAddress, force)
|
||||
}
|
||||
|
|
100
player.go
100
player.go
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -11,10 +12,56 @@ import (
|
|||
var (
|
||||
playerCursor time.Duration
|
||||
loadedCast *cast.Cast
|
||||
playing bool
|
||||
castCommand = make(chan *command)
|
||||
interrupt = make(chan struct{})
|
||||
goToTime = make(chan time.Duration)
|
||||
)
|
||||
|
||||
func handleCast() {
|
||||
for c := range castCommand {
|
||||
if commandConn != nil && !single && !viewer {
|
||||
sendCommand(c)
|
||||
continue
|
||||
}
|
||||
|
||||
switch c.Type {
|
||||
case commandLoad:
|
||||
err := loadCast(c.S)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load cast at %s: %s", c.S, err)
|
||||
}
|
||||
case commandStop:
|
||||
interruptPlayback()
|
||||
resetScreen()
|
||||
case commandPlay:
|
||||
if !playing {
|
||||
interruptPlayback()
|
||||
go play(c.D)
|
||||
} else {
|
||||
goToTime <- c.D
|
||||
}
|
||||
case commandPause:
|
||||
interruptPlayback()
|
||||
case commandResume:
|
||||
if !playing {
|
||||
interruptPlayback()
|
||||
go play(c.D)
|
||||
} else {
|
||||
goToTime <- c.D
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadCast(filePath string) error {
|
||||
select {
|
||||
case interrupt <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
|
||||
playing = false
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -31,45 +78,86 @@ func loadCast(filePath string) error {
|
|||
|
||||
// TODO maintain playing goroutine, unpause / fast forward when possible rather than reinstancing
|
||||
func play(at time.Duration) {
|
||||
playing = true
|
||||
|
||||
resetScreen()
|
||||
disableEcho()
|
||||
|
||||
var lastPing time.Time
|
||||
var fastForward time.Duration
|
||||
start := time.Now().Add(at * -1)
|
||||
for _, ev := range loadedCast.EventStream {
|
||||
t := time.Duration(ev.Time * float64(time.Second))
|
||||
if ev.Type == "i" {
|
||||
continue // TODO
|
||||
continue
|
||||
} else if fastForward > 0 && t <= fastForward {
|
||||
fmt.Print(ev.Data)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case <-interrupt:
|
||||
playing = false
|
||||
playerCursor = time.Since(start)
|
||||
sendCommand(&command{commandStatus, playerCursor, ""})
|
||||
sendCommand(&command{commandStatus, playerCursor, "stopped"})
|
||||
return
|
||||
case d := <-goToTime:
|
||||
if d < playerCursor {
|
||||
go play(d)
|
||||
return
|
||||
} else if d > playerCursor {
|
||||
fastForward = d
|
||||
start = time.Now().Add(fastForward * -1)
|
||||
fmt.Print(ev.Data)
|
||||
continue
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
t := time.Duration(ev.Time * float64(time.Second))
|
||||
if time.Since(start) < t {
|
||||
t := time.NewTimer(t - time.Since(start))
|
||||
select {
|
||||
case <-interrupt:
|
||||
playing = false
|
||||
playerCursor = time.Since(start)
|
||||
sendCommand(&command{commandStatus, playerCursor, ""})
|
||||
status := "stopped"
|
||||
if playing {
|
||||
status = "playing"
|
||||
}
|
||||
sendCommand(&command{commandStatus, playerCursor, status})
|
||||
return
|
||||
case d := <-goToTime:
|
||||
if d < playerCursor {
|
||||
go play(d)
|
||||
return
|
||||
} else if d > playerCursor {
|
||||
fastForward = d
|
||||
start = time.Now().Add(fastForward * -1)
|
||||
fmt.Print(ev.Data)
|
||||
continue
|
||||
}
|
||||
case <-t.C:
|
||||
}
|
||||
playerCursor = time.Since(start)
|
||||
if time.Since(lastPing) >= 1*time.Second {
|
||||
sendCommand(&command{commandStatus, playerCursor, ""})
|
||||
status := "stopped"
|
||||
if playing {
|
||||
status = "playing"
|
||||
}
|
||||
sendCommand(&command{commandStatus, playerCursor, status})
|
||||
lastPing = time.Now()
|
||||
}
|
||||
}
|
||||
fmt.Print(ev.Data)
|
||||
}
|
||||
|
||||
playing = false
|
||||
sendCommand(&command{commandStatus, playerCursor, "stopped"})
|
||||
resetScreen()
|
||||
fmt.Print("Playback complete.")
|
||||
|
||||
if !single {
|
||||
fmt.Print("Playback complete.")
|
||||
}
|
||||
}
|
||||
|
||||
func playFunc(at time.Duration) func() {
|
||||
|
|
34
viewer.go
34
viewer.go
|
@ -5,9 +5,7 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
@ -59,42 +57,12 @@ func hostViewer(address string) error {
|
|||
func handleViewer() {
|
||||
go handleRead()
|
||||
go handleWrite()
|
||||
var err error
|
||||
for c := range commandsIn {
|
||||
switch c.Type {
|
||||
case commandLoad:
|
||||
err = loadCast(c.S)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load cast at %s: %s", c.S, err)
|
||||
}
|
||||
case commandStop:
|
||||
interruptPlayback()
|
||||
resetScreen()
|
||||
case commandPlay:
|
||||
interruptPlayback()
|
||||
go play(c.D)
|
||||
case commandPause:
|
||||
interruptPlayback()
|
||||
case commandResume:
|
||||
go play(playerCursor)
|
||||
}
|
||||
castCommand <- c
|
||||
}
|
||||
}
|
||||
|
||||
func runViewer(controlAddress string) {
|
||||
sigc := make(chan os.Signal, 1)
|
||||
signal.Notify(sigc,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGQUIT)
|
||||
go func() {
|
||||
<-sigc
|
||||
interruptPlayback()
|
||||
resetScreen()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
err := hostViewer(controlAddress)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to host viwer: %s", err)
|
||||
|
|
Loading…
Reference in New Issue