Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support webp #60

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/*.gif filter=lfs diff=lfs merge=lfs -text
**/*.mp4 filter=lfs diff=lfs merge=lfs -text
**/*.webm filter=lfs diff=lfs merge=lfs -text
**/*.webp filter=lfs diff=lfs merge=lfs -text
**/*.png filter=lfs diff=lfs merge=lfs -text
*.tape linguist-language=elixir
themes*.json linguist-generated
Expand Down
56 changes: 27 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ vim demo.tape
```

Tape files consist of a series of [commands](#vhs-command-reference). The commands are
instructions for VHS to perform on its virtual terminal. For a list of all
instructions for VHS to perform on its virtual terminal. For a list of all
possible commands see [the command reference](#vhs-command-reference).

```elixir
Expand Down Expand Up @@ -125,8 +125,8 @@ docker run --rm -v $PWD:/vhs ghcr.io/charmbracelet/vhs <cassette>.tape

Or, download it:

* [Packages][releases] are available in Debian and RPM formats
* [Binaries][releases] are available for Linux, macOS, and Windows
- [Packages][releases] are available in Debian and RPM formats
- [Binaries][releases] are available for Linux, macOS, and Windows

Or, just install it with `go`:

Expand Down Expand Up @@ -159,7 +159,7 @@ vhs cassette.tape
VHS allows you to publish your GIFs to our servers for easy sharing with your
friends and colleagues. Specify which file you want to share, then use the
`publish` sub-command to host it on `vhs.charm.sh`. The output will provide you
with links to share your GIF via browser, HTML, and Markdown.
with links to share your GIF via browser, HTML, and Markdown.

```bash
vhs publish demo.gif
Expand All @@ -180,16 +180,15 @@ vhs serve
<details>
<summary>Configuration Options</summary>

* `VHS_PORT`: The port to listen on (`1976`)
* `VHS_HOST`: The host to listen on (`localhost`)
* `VHS_GID`: The Group ID to run the server as (current user's GID)
* `VHS_UID`: The User ID to run the server as (current user's UID)
* `VHS_KEY_PATH`: The path to the SSH key to use (`.ssh/vhs_ed25519`)
* `VHS_AUTHORIZED_KEYS_PATH`: The path to the authorized keys file (empty, publicly accessible)
- `VHS_PORT`: The port to listen on (`1976`)
- `VHS_HOST`: The host to listen on (`localhost`)
- `VHS_GID`: The Group ID to run the server as (current user's GID)
- `VHS_UID`: The User ID to run the server as (current user's UID)
- `VHS_KEY_PATH`: The path to the SSH key to use (`.ssh/vhs_ed25519`)
- `VHS_AUTHORIZED_KEYS_PATH`: The path to the authorized keys file (empty, publicly accessible)

</details>


Then, simply access VHS from a different machine via `ssh`:

```sh
Expand All @@ -203,16 +202,16 @@ ssh vhs.example.com < demo.tape > demo.gif

There are a few basic types of VHS commands:

* [`Output <path>`](#output): specify file output
* [`Require <program>`](#require): specify required programs for tape file
* [`Set <Setting> Value`](#settings): set recording settings
* [`Type "<characters>"`](#type): emulate typing
* [`Left`](#arrow-keys) [`Right`](#arrow-keys) [`Up`](#arrow-keys) [`Down`](#arrow-keys): arrow keys
* [`Backspace`](#backspace) [`Enter`](#enter) [`Tab`](#tab) [`Space`](#space): special keys
* [`Ctrl[+Alt][+Shift]+<char>`](#ctrl): press control + key and/or modifier
* [`Sleep <time>`](#sleep): wait for a certain amount of time
* [`Hide`](#hide): hide commands from output
* [`Show`](#show): stop hiding commands from output
- [`Output <path>`](#output): specify file output
- [`Require <program>`](#require): specify required programs for tape file
- [`Set <Setting> Value`](#settings): set recording settings
- [`Type "<characters>"`](#type): emulate typing
- [`Left`](#arrow-keys) [`Right`](#arrow-keys) [`Up`](#arrow-keys) [`Down`](#arrow-keys): arrow keys
- [`Backspace`](#backspace) [`Enter`](#enter) [`Tab`](#tab) [`Space`](#space): special keys
- [`Ctrl[+Alt][+Shift]+<char>`](#ctrl): press control + key and/or modifier
- [`Sleep <time>`](#sleep): wait for a certain amount of time
- [`Hide`](#hide): hide commands from output
- [`Show`](#show): stop hiding commands from output

### Output

Expand All @@ -224,6 +223,7 @@ will render them to the respective locations.
Output out.gif
Output out.mp4
Output out.webm
Output out.webp
Output frames/ # a directory of frames as a PNG sequence
```

Expand Down Expand Up @@ -322,6 +322,7 @@ Set the height of the terminal with the `Set Height` command.
```elixir
Set Height 1000
```

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://stuff.charm.sh/vhs/examples/height.gif">
<source media="(prefers-color-scheme: light)" srcset="https://stuff.charm.sh/vhs/examples/height.gif">
Expand Down Expand Up @@ -409,14 +410,12 @@ command.
Set Padding 0
```


<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://stuff.charm.sh/vhs/examples/padding.gif">
<source media="(prefers-color-scheme: light)" srcset="https://stuff.charm.sh/vhs/examples/padding.gif">
<img width="600" alt="Example of setting the padding" src="https://stuff.charm.sh/vhs/examples/padding.gif">
</picture>


#### Set Margin

Set the margin (in pixels) of the video with the `Set Margin` command.
Expand Down Expand Up @@ -463,7 +462,6 @@ Set BorderRadius 10
<img width="400" alt="Example of setting the margin" src="https://vhs.charm.sh/vhs-4nYoy6IsUKmleJANG7N1BH.gif">
</picture>


#### Set Framerate

Set the rate at which VHS captures frames with the `Set Framerate` command.
Expand Down Expand Up @@ -694,7 +692,7 @@ Type "You will see this being typed."
<img width="600" alt="Example of typing something while hidden" src="https://stuff.charm.sh/vhs/examples/hide.gif">
</picture>

***
---

## Continuous Integration

Expand Down Expand Up @@ -724,15 +722,15 @@ It works great with Neovim, Emacs, and so on!

We’d love to hear your thoughts on this project. Feel free to drop us a note!

* [Twitter](https://twitter.com/charmcli)
* [The Fediverse](https://mastodon.social/@charmcli)
* [Discord](https://charm.sh/chat)
- [Twitter](https://twitter.com/charmcli)
- [The Fediverse](https://mastodon.social/@charmcli)
- [Discord](https://charm.sh/chat)

## License

[MIT](https://github.com/charmbracelet/vhs/raw/main/LICENSE)

***
---

Part of [Charm](https://charm.sh).

Expand Down
2 changes: 2 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ func ExecuteOutput(c Command, v *VHS) {
v.Options.Video.Output.Frames = c.Args
case ".webm":
v.Options.Video.Output.WebM = c.Args
case ".webp":
v.Options.Video.Output.WebP = c.Args
default:
v.Options.Video.Output.GIF = c.Args
}
Expand Down
3 changes: 2 additions & 1 deletion examples/demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
# Output <path>.webp Create a WebP output at the given <path>
#
# Require:
# Require <string> Ensure a program is on the $PATH to proceed
Expand Down Expand Up @@ -63,4 +64,4 @@ Set Height 600

Type "echo 'Welcome to VHS!'" Sleep 500ms Enter

Sleep 5s
Sleep 1s
1 change: 1 addition & 0 deletions examples/fixtures/all.tape
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Output examples/fixtures/all.gif
Output examples/fixtures/all.mp4
Output examples/fixtures/all.webm
Output examples/fixtures/all.webp

# Settings:
Set Shell "fish"
Expand Down
1 change: 1 addition & 0 deletions examples/neofetch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Output examples/neofetch/neofetch.gif
Output examples/neofetch/neofetch.mp4
Output examples/neofetch/neofetch.webm
Output examples/neofetch/neofetch.webp
Output examples/neofetch/frames/

Set TypingSpeed 75ms
Expand Down
1 change: 1 addition & 0 deletions examples/neofetch/neofetch.tape
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Output examples/neofetch/neofetch.gif
Output examples/neofetch/neofetch.mp4
Output examples/neofetch/neofetch.webm
Output examples/neofetch/neofetch.webp
Output examples/neofetch/frames/

Set TypingSpeed 75ms
Expand Down
2 changes: 2 additions & 0 deletions lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ func TestLexTapeFile(t *testing.T) {
{STRING, "examples/fixtures/all.mp4"},
{OUTPUT, "Output"},
{STRING, "examples/fixtures/all.webm"},
{OUTPUT, "Output"},
{STRING, "examples/fixtures/all.webp"},
{COMMENT, " Settings:"},
{SET, "Set"},
{SHELL, "Shell"},
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ var (
v.Options.Video.Output.GIF = output
} else if strings.HasSuffix(output, webm) {
v.Options.Video.Output.WebM = output
} else if strings.HasSuffix(output, webp) {
v.Options.Video.Output.WebP = output
} else if strings.HasSuffix(output, mp4) {
v.Options.Video.Output.MP4 = output
}
Expand Down
4 changes: 2 additions & 2 deletions man.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A tape file is a script made up of commands describing what actions to perform i

The following is a list of all possible commands in VHS:

* %Output% <path>.(gif|webm|mp4)
* %Output% <path>.(gif|webm|webp|mp4)
* %Require% <program>
* %Set% <setting> <value>
* %Sleep% <time>
Expand All @@ -48,7 +48,7 @@ The following is a list of all possible commands in VHS:
`

manOutput = `The Output command instructs VHS where to save the output of the recording.
File names with the extension %.gif%, %.webm%, %.mp4% will have the respective file types.
File names with the extension %.gif%, %.webm%, %.webp%, %.mp4% will have the respective file types.
`

manSettings = `The Set command allows VHS to adjust settings in the terminal, such as fonts, dimensions, and themes.
Expand Down
1 change: 1 addition & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func TestParseTapeFile(t *testing.T) {
{Type: OUTPUT, Options: ".gif", Args: "examples/fixtures/all.gif"},
{Type: OUTPUT, Options: ".mp4", Args: "examples/fixtures/all.mp4"},
{Type: OUTPUT, Options: ".webm", Args: "examples/fixtures/all.webm"},
{Type: OUTPUT, Options: ".webp", Args: "examples/fixtures/all.webp"},
{Type: SET, Options: "Shell", Args: "fish"},
{Type: SET, Options: "FontSize", Args: "22"},
{Type: SET, Options: "FontFamily", Args: "DejaVu Sans Mono"},
Expand Down
6 changes: 5 additions & 1 deletion serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,25 @@ var serveCmd = &cobra.Command{
tempFile := filepath.Join(os.TempDir(), fmt.Sprintf("vhs-%d", rand))
defer func() { _ = os.Remove(tempFile) }()
errs := Evaluate(s.Context(), b.String(), s.Stderr(), func(v *VHS) {
var gif, mp4, webm string
var gif, mp4, webm, webp string
switch {
case v.Options.Video.Output.MP4 != "":
tempFile += mp4
mp4 = tempFile
case v.Options.Video.Output.WebM != "":
tempFile += webm
webm = tempFile
case v.Options.Video.Output.WebP != "":
tempFile += webp
webp = tempFile
default:
tempFile += gif
gif = tempFile
}
v.Options.Video.Output.GIF = gif
v.Options.Video.Output.MP4 = mp4
v.Options.Video.Output.WebM = webm
v.Options.Video.Output.WebP = webp
})

if len(errs) > 0 {
Expand Down
1 change: 1 addition & 0 deletions vhs.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ func (vhs *VHS) Render() error {
cmds = append(cmds, MakeGIF(vhs.Options.Video))
cmds = append(cmds, MakeMP4(vhs.Options.Video))
cmds = append(cmds, MakeWebM(vhs.Options.Video))
cmds = append(cmds, MakeWebP(vhs.Options.Video))

for _, cmd := range cmds {
if cmd == nil {
Expand Down
35 changes: 32 additions & 3 deletions video.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Package vhs video.go spawns the ffmpeg process to convert the frames,
// collected by go-rod's screenshots into the input folder, to a GIF, WebM,
// MP4.
// WebP, MP4.
//
// MakeGIF takes several options to modify the behaviour of the ffmpeg process,
// which can be configured through the Set command.
Expand All @@ -25,6 +25,7 @@
const (
mp4 = ".mp4"
webm = ".webm"
webp = ".webp"
gif = ".gif"
)

Expand All @@ -43,6 +44,7 @@
type VideoOutputs struct {
GIF string
WebM string
WebP string
MP4 string
Frames string
}
Expand Down Expand Up @@ -91,7 +93,7 @@
Framerate: defaultFramerate,
Input: randomDir(),
MaxColors: defaultMaxColors,
Output: VideoOutputs{GIF: "", WebM: "", MP4: "", Frames: ""},
Output: VideoOutputs{GIF: "", WebM: "", WebP:"", MP4: "", Frames: ""},

Check failure on line 96 in video.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)

Check failure on line 96 in video.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed (goimports)
Width: defaultWidth,
Height: defaultHeight,
Padding: defaultPadding,
Expand Down Expand Up @@ -307,6 +309,16 @@
"-crf", "30",
"-b:v", "0",
)
} else if filepath.Ext(targetFile) == webp {
args = append(args,
"-vcodec", "libwebp",
"-pix_fmt", "yuv444p",
"-loop", "0",
"-lossless","0",
"-an",
"-crf", "30",
"-b:v", "0",
)
} else if filepath.Ext(targetFile) == mp4 {
args = append(args,
"-vcodec", "libx264",
Expand All @@ -329,7 +341,7 @@
func MakeGIF(opts VideoOptions) *exec.Cmd {
targetFile := opts.Output.GIF

if opts.Output.GIF == "" && opts.Output.WebM == "" && opts.Output.MP4 == "" {
if opts.Output.GIF == "" && opts.Output.WebM == "" && opts.Output.WebP == "" && opts.Output.MP4 == "" {
targetFile = "out.gif"
} else if opts.Output.GIF == "" {
return nil
Expand Down Expand Up @@ -376,3 +388,20 @@
buildFFopts(opts, opts.Output.MP4)...,
)
}


// MakeWebP takes a list of images (as frames) and converts them to an WebP.
func MakeWebP(opts VideoOptions) *exec.Cmd {
if opts.Output.WebP == "" {
return nil
}

log.Println(GrayStyle.Render("Creating " + opts.Output.WebP + "..."))
ensureDir(opts.Output.WebP)

//nolint:gosec
return exec.Command(
"ffmpeg",
buildFFopts(opts, opts.Output.WebP)...,
)
}