Module: AsciinemaWin::Export
- Defined in:
- lib/asciinema_win/export.rb
Overview
Video export requires FFmpeg installed and in PATH. This is an OPTIONAL feature - core functionality works without it.
Export module for converting recordings to different formats
Supports export to:
-
Cast (asciicast v2 - copy/convert)
-
HTML (embedded player)
-
SVG (static snapshot)
-
Text (plain text dump)
-
JSON (normalized format)
-
GIF/MP4/WebM (requires FFmpeg - optional)
Constant Summary collapse
- NATIVE_FORMATS =
Export formats supported natively (no external dependencies)
%i[cast html svg txt text json].freeze
- EXTERNAL_FORMATS =
Export formats requiring external tools
%i[gif mp4 webm].freeze
- ALL_FORMATS =
All supported formats
(NATIVE_FORMATS + EXTERNAL_FORMATS).freeze
Class Method Summary collapse
-
.adjust_speed(input_path, output_path, speed: 1.0, max_idle: nil) ⇒ Boolean
Adjust playback speed of a recording.
-
.concatenate(input_paths, output_path, title: nil, gap: 1.0) ⇒ Boolean
Concatenate multiple recordings into one.
-
.export(input_path, output_path, format:, **options) ⇒ Boolean
Export a recording to the specified format.
-
.export_cast(input_path, output_path, title: nil, trim_start: nil, trim_end: nil, speed: 1.0, max_idle: nil, **_options) ⇒ Boolean
Export to asciicast v2 format (copy or transform).
-
.export_html(input_path, output_path, title: nil, theme: "asciinema", autoplay: false, **_options) ⇒ Boolean
Export to HTML with embedded asciinema-player.
-
.export_json(input_path, output_path, **_options) ⇒ Boolean
Export to JSON (normalized format).
-
.export_svg(input_path, output_path, theme: "asciinema", frame: :last, **_options) ⇒ Boolean
Export to SVG with full color support.
-
.export_text(input_path, output_path, strip_ansi: true, **_options) ⇒ Boolean
Export to plain text.
-
.export_video(input_path, output_path, format:, fps: 10, font_size: 14, theme: "asciinema", scale: 1.0, loop_count: -1,, **_options) ⇒ Boolean
Export to video format (GIF, MP4, WebM).
-
.ffmpeg_available? ⇒ Boolean
Check if FFmpeg is available.
-
.thumbnail(input_path, output_path, frame: :last, theme: "asciinema", width: nil, height: nil, **_options) ⇒ Boolean
Generate a thumbnail image from a recording.
Class Method Details
.adjust_speed(input_path, output_path, speed: 1.0, max_idle: nil) ⇒ Boolean
Adjust playback speed of a recording
226 227 228 |
# File 'lib/asciinema_win/export.rb', line 226 def adjust_speed(input_path, output_path, speed: 1.0, max_idle: nil) export_cast(input_path, output_path, speed: speed, max_idle: max_idle) end |
.concatenate(input_paths, output_path, title: nil, gap: 1.0) ⇒ Boolean
Concatenate multiple recordings into one
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/asciinema_win/export.rb', line 124 def concatenate(input_paths, output_path, title: nil, gap: 1.0) raise ExportError, "No input files specified" if input_paths.empty? # Load first file to get dimensions first_reader = Asciicast.load(input_paths.first) first_header = first_reader.header # Determine max dimensions across all files max_width = first_header.width max_height = first_header.height input_paths[1..].each do |path| reader = Asciicast.load(path) max_width = [max_width, reader.header.width].max max_height = [max_height, reader.header.height].max end # Create output header combined_title = title || input_paths.map { |p| File.basename(p, ".cast") }.join(" + ") new_header = Asciicast::Header.new( width: max_width, height: max_height, timestamp: first_header., title: combined_title ) File.open(output_path, "w", encoding: "UTF-8") do |file| writer = Asciicast::Writer.new(file, new_header) current_time = 0.0 input_paths.each_with_index do |path, index| reader = Asciicast.load(path) last_event_time = 0.0 reader.each_event do |event| writer.write_event(Asciicast::Event.new(current_time + event.time, event.type, event.data)) last_event_time = event.time end # Add gap before next recording (except after last) current_time += last_event_time + gap if index < input_paths.length - 1 # Add marker at join point if index < input_paths.length - 1 writer.write_marker(current_time - gap / 2, "joined: #{File.basename(path)}") end end writer.close end true end |
.export(input_path, output_path, format:, **options) ⇒ Boolean
Export a recording to the specified format
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/asciinema_win/export.rb', line 38 def export(input_path, output_path, format:, **) format = format.to_sym unless ALL_FORMATS.include?(format) raise ExportError, "Unsupported format: #{format}. Supported: #{ALL_FORMATS.join(", ")}" end case format when :cast export_cast(input_path, output_path, **) when :html export_html(input_path, output_path, **) when :svg export_svg(input_path, output_path, **) when :txt, :text export_text(input_path, output_path, **) when :json export_json(input_path, output_path, **) when :gif, :mp4, :webm export_video(input_path, output_path, format: format, **) end end |
.export_cast(input_path, output_path, title: nil, trim_start: nil, trim_end: nil, speed: 1.0, max_idle: nil, **_options) ⇒ Boolean
Export to asciicast v2 format (copy or transform)
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/asciinema_win/export.rb', line 71 def export_cast(input_path, output_path, title: nil, trim_start: nil, trim_end: nil, speed: 1.0, max_idle: nil, **) reader = Asciicast.load(input_path) original_header = reader.header # Create new header with potential modifications new_header = Asciicast::Header.new( width: original_header.width, height: original_header.height, timestamp: original_header., idle_time_limit: max_idle || original_header.idle_time_limit, command: original_header.command, title: title || original_header.title, env: original_header.env, theme: original_header.theme ) File.open(output_path, "w", encoding: "UTF-8") do |file| writer = Asciicast::Writer.new(file, new_header) last_time = 0.0 reader.each_event do |event| # Apply trimming if specified next if trim_start && event.time < trim_start next if trim_end && event.time > trim_end # Adjust time for trimming adjusted_time = trim_start ? event.time - trim_start : event.time # Apply speed adjustment adjusted_time /= speed # Apply max idle limit if max_idle && (adjusted_time - last_time) > max_idle adjusted_time = last_time + max_idle end writer.write_event(Asciicast::Event.new(adjusted_time, event.type, event.data)) last_time = adjusted_time end writer.close end true end |
.export_html(input_path, output_path, title: nil, theme: "asciinema", autoplay: false, **_options) ⇒ Boolean
Export to HTML with embedded asciinema-player
238 239 240 241 242 243 244 245 246 247 |
# File 'lib/asciinema_win/export.rb', line 238 def export_html(input_path, output_path, title: nil, theme: "asciinema", autoplay: false, **) info = Asciicast::Reader.info(input_path) cast_content = File.read(input_path, encoding: "UTF-8") title ||= info[:title] || "Terminal Recording" html = generate_html(cast_content, info, title: title, theme: theme, autoplay: autoplay) File.write(output_path, html, encoding: "UTF-8") true end |
.export_json(input_path, output_path, **_options) ⇒ Boolean
Export to JSON (normalized format)
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/asciinema_win/export.rb', line 300 def export_json(input_path, output_path, **) require "json" info = Asciicast::Reader.info(input_path) reader = Asciicast.load(input_path) events = reader.each_event.map do |event| { time: event.time, type: event.type, data: event.data } end data = { header: info, events: events } File.write(output_path, JSON.pretty_generate(data), encoding: "UTF-8") true end |
.export_svg(input_path, output_path, theme: "asciinema", frame: :last, **_options) ⇒ Boolean
Export to SVG with full color support
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/asciinema_win/export.rb', line 256 def export_svg(input_path, output_path, theme: "asciinema", frame: :last, **) info = Asciicast::Reader.info(input_path) reader = Asciicast.load(input_path) # Collect all output output = StringIO.new reader.each_event do |event| output << event.data if event.output? end # Parse ANSI codes and render colored SVG color_theme = Themes.get(theme) svg = generate_colored_svg(output.string, info[:width], info[:height], color_theme) File.write(output_path, svg, encoding: "UTF-8") true end |
.export_text(input_path, output_path, strip_ansi: true, **_options) ⇒ Boolean
Export to plain text
280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/asciinema_win/export.rb', line 280 def export_text(input_path, output_path, strip_ansi: true, **) reader = Asciicast.load(input_path) output = StringIO.new reader.each_event do |event| output << event.data if event.output? end text = output.string text = strip_ansi_codes(text) if strip_ansi File.write(output_path, text, encoding: "UTF-8") true end |
.export_video(input_path, output_path, format:, fps: 10, font_size: 14, theme: "asciinema", scale: 1.0, loop_count: -1,, **_options) ⇒ Boolean
Requires FFmpeg to be installed and in PATH (or FFMPEG_PATH set)
Export to video format (GIF, MP4, WebM)
Uses FFmpeg with 2-pass palette generation for high-quality GIF output. Renders each frame as SVG and pipes directly to FFmpeg for optimal efficiency.
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
# File 'lib/asciinema_win/export.rb', line 336 def export_video(input_path, output_path, format:, fps: 10, font_size: 14, theme: "asciinema", scale: 1.0, loop_count: -1, **) unless ffmpeg_available? raise ExportError, <<~MSG FFmpeg is required for #{format.upcase} export but was not found. To install FFmpeg: 1. Download from https://ffmpeg.org/download.html 2. Add to PATH or set FFMPEG_PATH environment variable Alternatively, use native export formats: #{NATIVE_FORMATS.join(", ")} MSG end # Create temporary directory for frames temp_dir = File.join(Dir.tmpdir, "asciinema_win_#{Process.pid}_#{Time.now.to_i}") FileUtils.mkdir_p(temp_dir) begin # Generate SVG frame files $stderr.puts "Generating frames at #{fps} FPS..." frame_count = generate_video_frames( input_path, temp_dir, fps: fps, font_size: font_size, theme: theme, scale: scale ) if frame_count == 0 raise ExportError, "No frames generated from recording" end $stderr.puts "Generated #{frame_count} frames" # Use FFmpeg to create video $stderr.puts "Encoding #{format.upcase}..." ffmpeg_create_video(temp_dir, output_path, format: format, fps: fps, loop_count: loop_count) $stderr.puts "Video saved to #{output_path}" true ensure # Cleanup temp files FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir) end end |
.ffmpeg_available? ⇒ Boolean
Check if FFmpeg is available
385 386 387 388 389 390 391 392 |
# File 'lib/asciinema_win/export.rb', line 385 def ffmpeg_available? ffmpeg_path = ENV["FFMPEG_PATH"] || "ffmpeg" # Array form: no shell, so a path with spaces works and FFMPEG_PATH # cannot be used for shell injection. system(ffmpeg_path, "-version", out: File::NULL, err: File::NULL) rescue StandardError false end |
.thumbnail(input_path, output_path, frame: :last, theme: "asciinema", width: nil, height: nil, **_options) ⇒ Boolean
Generate a thumbnail image from a recording
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/asciinema_win/export.rb', line 187 def thumbnail(input_path, output_path, frame: :last, theme: "asciinema", width: nil, height: nil, **) info = Asciicast::Reader.info(input_path) reader = Asciicast.load(input_path) # Determine which frame to capture target_time = case frame when :first then 0.0 when :last then info[:duration] when :middle then info[:duration] / 2 when Numeric then frame.to_f else info[:duration] end # Collect output up to target time output = StringIO.new reader.each_event do |event| break if event.time > target_time output << event.data if event.output? end # Parse and render color_theme = Themes.get(theme) parser = AnsiParser.new(width: info[:width], height: info[:height]) lines = parser.parse(output.string) svg = generate_thumbnail_svg(lines, info[:width], info[:height], color_theme, width: width, height: height) File.write(output_path, svg, encoding: "UTF-8") true end |