Skip to content

Instantly share code, notes, and snippets.

@ayosec
Last active March 5, 2021 06:51
Show Gist options
  • Save ayosec/2cd27ca2cfacb1373340b909c3996d4a to your computer and use it in GitHub Desktop.
Save ayosec/2cd27ca2cfacb1373340b909c3996d4a to your computer and use it in GitHub Desktop.
Test for graphics support in Alacritty

Test for graphics support in Alacritty

This script implements a simple video player to test the support for the iTerm2 inline images protocol.

⚠️ This script is not intended to be used as a regular video player. Its only use is to test the performance of graphics support of the terminal.

Requirements

  • Ruby 2.5 or newer.
  • FFmpeg 4 or newer.

Older versions may work, but are untested.

Usage

To render frames from a video invoke the script with the path of the video as its argument:

./video2osc1337.rb some-video.ogv

The script support multiple options to control the performance of the test:

  • -f, --fps

    Number of frames per second to write to the terminal.

  • -n, --frames

    Number of frames from the video to write.

  • -q, --quality

    -q:v parameter for ffmpeg. A lower value improves the output, but requires more bytes to be written to the terminal.

  • -i, --image-format [FORMAT]

    Image format. JPEG by default.

  • -s, --scale

    Scale the image output, See the scale ffmpeg filter documentation for more details.

    For example, to scale the video frames to width 300px

      ./video2osc1337.rb -s 300:-1 some-video.ogv
    
  • -p, --position

    Position in the grid (row,column) to draw video frames.

How it works

The script uses ffmpeg to extract video frames from the video. To receive the frames, the script starts a TCP server, and launches ffmpeg using a tcp://localhost:... URI as the output.

#!/usr/bin/env ruby
#
# This script uses ffmpeg and the [1]inline images protocol from iTerm2 to render
# a video on the terminal window.
#
# **IMPORTANT** It is not intended to be used as a regular video player.
# Use only for very unscientific benchmarks.
#
# Frames are extracted by ffmpeg and sent to the script through a local TCP
# server. Then, the frame is written to the terminal with the OSC 1337
# sequence:
#
# OSC 1337 ; File=inline=1 : <base64> ^G
#
# [1]: https://iterm2.com/documentation-images.html
require "socket"
require "json"
require "base64"
require "optionparser"
Options = {
fps: nil,
num_frames: nil,
quality: nil,
image_format: "jpeg",
scale: nil,
position: "1,1",
}
OptionParser.new do |opts|
opts.on("-f", "--fps [FPS]", OptionParser::DecimalInteger, "Frame rate (`-r` option for ffmpeg)") do |fps|
Options[:fps] = fps
end
opts.on("-n", "--frames [NUMBER]", OptionParser::DecimalInteger, "Number of frames to render.") do |frames|
Options[:num_frames] = frames
end
opts.on("-q", "--quality [Q]", OptionParser::DecimalInteger, "Quality (`-q:v` option for ffmpeg)") do |frames|
Options[:quality] = frames
end
opts.on("-i", "--image-format [FORMAT]", "Image format (default: #{Options[:image_format]})") do |format|
Options[:image_format] = format
end
opts.on("-s", "--scale [SCALE]", "Scale. See scale filer in ffmpeg docs for details") do |scale|
Options[:scale] = scale
end
opts.on("-p", "--position [ROW,COLUMN]", "Position for the top-left corner.") do |position|
Options[:position] = position
end
end.parse!
if ARGV.size != 1
STDERR.puts "Usage: #$0 video"
exit 1
end
VIDEO_FILE = ARGV.shift
# Restore terminal after exit
at_exit do
print "\e[2J"
STDOUT.flush
print "\e[?1049l\e[?25h"
end
# Alt-screen, hidden cursor
print "\e[?1049h\e[?25l\e[1;1H\e[2J"
#
# Extract frame rate and dimensions with ffprobe.
VIDEO_METADATA =
begin
ffprobe = %W(
ffprobe
-v error
-print_format json
-select_streams v
-show_streams
#{VIDEO_FILE}
)
stream = IO.popen(ffprobe) do |child|
JSON.parse(child.read).dig("streams", 0)
end
fps =
if stream["r_frame_rate"] =~ %r[(\d+)/(\d+)]
$1.to_f / $2.to_f
else
# Unknown value. Assume 24fps
24.0
end
Struct.new(:width, :height, :fps).new(stream["width"], stream["height"], fps)
end
#
# Start a TCP server to receive frames from ffmpeg.
SERVER = TCPServer.new("localhost", 0)
LISTEN_PORT = SERVER.addr[1]
FRAMES_QUEUE = SizedQueue.new(1000)
Thread.new do
while client = SERVER.accept
data = ''
loop do
new_data = client.recv(4096)
break if new_data.nil? || new_data.empty?
data << new_data
end
FRAMES_QUEUE << data
end
rescue Errno::EINVAL
FRAMES_QUEUE << nil
end
#
# Another background thread to render frames
RENDER = Thread.new do
def monotonic
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
interval = 1.0 / (Options[:fps] || VIDEO_METADATA.fps)
video_metadata = VIDEO_METADATA
frame_bytes = 0
frame_count = 0
frames_dropped = 0
first_frame = nil
cursor_position = Options[:position].scan(/\d+/).join(";")
while frame = FRAMES_QUEUE.shift
break if frame == :stop
first_frame ||= monotonic()
next_frame = monotonic() + interval
# Move the cursor and send the frame using the iTerm2 protocol
print "\e[#{cursor_position}H\e]1337;File=inline=1:"
print Base64.encode64(frame)
print "\a"
frame_count += 1
frame_bytes += frame.bytesize
# Show stats
elapsed = monotonic() - first_frame
print "#{frame_bytes} bytes in #{frame_count} frames"
print " (#{frames_dropped} dropped)" if frames_dropped > 0
print " (#{(frame_bytes / 1024.0 / frame_count).round(0)} Kib/frame)"
puts " after #{elapsed.round(1)} seconds (#{(frame_count / elapsed).round(2)} fps)\e[K"
# Print metadata after the first frame
if video_metadata
print "\n[stream]"
video_metadata.to_h.each_pair do |k, v|
print " #{k}=#{v}"
end
video_metadata = nil
end
# Wait for the next frame, or drop some of them if
# the terminal is too slow.
wait = next_frame - monotonic()
if wait > 0
sleep wait
else
dropped = (-wait / interval).ceil
begin
dropped.times do
FRAMES_QUEUE.shift(true)
frames_dropped += 1
end
rescue ThreadError
break # Queue was empty
end
end
end
end
#
# Launch ffmpeg to extract frames and send them to our TCP server
pid = fork do
ENV["NO_COLOR"] = "1"
command = %W(
ffmpeg
-threads 1
-v error
-i #{VIDEO_FILE}
)
if fps = Options[:fps]
command << "-r" << fps.to_s
end
if n = Options[:num_frames]
command << "-frames" << n.to_s
end
if q = Options[:quality]
command << "-q:v" << q.to_s
end
if s = Options[:scale]
command << "-vf" << "scale=#{s}"
end
command.concat(%W(
-f image2
tcp://localhost:#{LISTEN_PORT}?dummy=%03d.#{Options[:image_format]}
))
exec(*command)
end
#
# Wait until all frames are rendered
begin
Process.waitpid(pid)
if not $?.success?
STDERR.puts "\n\n\033[31mffmpeg failed. Press ENTER to exit.\033[m"
readline
exit
end
SERVER.close_read
FRAMES_QUEUE << :stop
RENDER.join if RENDER.alive?
rescue Interrupt
begin
Process.kill("KILL", pid)
Process.waitpid(pid)
rescue SystemCallError
end
RENDER.terminate
end
puts "\n\n\n\033[32mFinished. Press ENTER to exit.\033[m"
readline
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment