Class: Gemba::RecorderDecoder
- Inherits:
-
Object
- Object
- Gemba::RecorderDecoder
- Defined in:
- lib/gemba/recorder_decoder.rb
Overview
Decodes a .grec file and encodes it to a playable video via ffmpeg.
Two-pass approach to avoid writing massive intermediate files:
Pass 1: Extract audio to a small tempfile (~10MB/min), count frames,
collect per-frame change percentages.
Pass 2: Decode video frames one at a time and pipe to ffmpeg's stdin.
Only one decoded video frame is in memory at a time, so RAM usage stays constant regardless of recording length.
Defined Under Namespace
Classes: FfmpegNotFound, FormatError
Constant Summary collapse
- DEFAULT_VIDEO_CODEC =
'libx264'- DEFAULT_AUDIO_CODEC =
'aac'
Class Method Summary collapse
-
.decode(trec_path, output_path, video_codec: DEFAULT_VIDEO_CODEC, audio_codec: DEFAULT_AUDIO_CODEC, scale: nil, ffmpeg_args: nil, progress: true) ⇒ Hash
Decode a .grec file and encode to a playable video file.
-
.stats(trec_path) ⇒ Hash
Quick scan of a .grec file — no ffmpeg needed.
Instance Method Summary collapse
- #decode ⇒ Object
-
#initialize(trec_path, output_path, video_codec: DEFAULT_VIDEO_CODEC, audio_codec: DEFAULT_AUDIO_CODEC, scale: nil, ffmpeg_args: nil, progress: true) ⇒ RecorderDecoder
constructor
A new instance of RecorderDecoder.
-
#stats ⇒ Object
Quick stats scan — reads only header + 1 byte per frame.
Constructor Details
#initialize(trec_path, output_path, video_codec: DEFAULT_VIDEO_CODEC, audio_codec: DEFAULT_AUDIO_CODEC, scale: nil, ffmpeg_args: nil, progress: true) ⇒ RecorderDecoder
Returns a new instance of RecorderDecoder.
59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/gemba/recorder_decoder.rb', line 59 def initialize(trec_path, output_path, video_codec: DEFAULT_VIDEO_CODEC, audio_codec: DEFAULT_AUDIO_CODEC, scale: nil, ffmpeg_args: nil, progress: true) @trec_path = trec_path @output_path = output_path @video_codec = video_codec @audio_codec = audio_codec @scale = scale @ffmpeg_args = ffmpeg_args @progress = progress end |
Class Method Details
.decode(trec_path, output_path, video_codec: DEFAULT_VIDEO_CODEC, audio_codec: DEFAULT_AUDIO_CODEC, scale: nil, ffmpeg_args: nil, progress: true) ⇒ Hash
Decode a .grec file and encode to a playable video file.
51 52 53 54 55 56 57 |
# File 'lib/gemba/recorder_decoder.rb', line 51 def self.decode(trec_path, output_path, video_codec: DEFAULT_VIDEO_CODEC, audio_codec: DEFAULT_AUDIO_CODEC, scale: nil, ffmpeg_args: nil, progress: true) new(trec_path, output_path, video_codec: video_codec, audio_codec: audio_codec, scale: scale, ffmpeg_args: ffmpeg_args, progress: progress).decode end |
.stats(trec_path) ⇒ Hash
Quick scan of a .grec file — no ffmpeg needed. Reads header + per-frame change bytes, skips video/audio data.
37 38 39 |
# File 'lib/gemba/recorder_decoder.rb', line 37 def self.stats(trec_path) new(trec_path, nil).stats end |
Instance Method Details
#decode ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 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 |
# File 'lib/gemba/recorder_decoder.rb', line 112 def decode check_ffmpeg! header = nil frame_count = 0 total_change = 0 # Pass 1: parse header, extract audio to tempfile, count frames. # Video chunks are skipped (seek, not read) to keep this fast. audio_tmp = Tempfile.new(['trec_audio', '.raw']) audio_tmp.binmode File.open(@trec_path, 'rb') do |f| header = read_header(f) until f.eof? break if (f) change_pct = f.read(1)&.unpack1('C') or break total_change += change_pct video_len = read_u32(f) or break f.seek(video_len, IO::SEEK_CUR) audio_len = read_u32(f) or break audio_tmp.write(f.read(audio_len)) if audio_len > 0 frame_count += 1 end end audio_tmp.flush fps = header[:fps_num].to_f / header[:fps_den] frame_size = header[:width] * header[:height] * 4 # Pass 2: decode video frames and pipe to ffmpeg. encode(header, fps, audio_tmp.path, frame_count) { output_path: @output_path, frame_count: frame_count, width: header[:width], height: header[:height], fps: fps, avg_change_pct: frame_count > 0 ? total_change.to_f / frame_count : 0, raw_video_size: frame_count * frame_size, audio_rate: header[:audio_rate], audio_channels: header[:audio_channels], } ensure audio_tmp&.close! end |
#stats ⇒ Object
Quick stats scan — reads only header + 1 byte per frame.
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 |
# File 'lib/gemba/recorder_decoder.rb', line 72 def stats header = nil frame_count = 0 total_change = 0 File.open(@trec_path, 'rb') do |f| header = read_header(f) until f.eof? break if (f) change_pct = f.read(1)&.unpack1('C') or break total_change += change_pct video_len = read_u32(f) or break f.seek(video_len, IO::SEEK_CUR) audio_len = read_u32(f) or break f.seek(audio_len, IO::SEEK_CUR) frame_count += 1 end end fps = header[:fps_num].to_f / header[:fps_den] frame_size = header[:width] * header[:height] * 4 { frame_count: frame_count, width: header[:width], height: header[:height], fps: fps, duration: frame_count / fps, avg_change_pct: frame_count > 0 ? total_change.to_f / frame_count : 0, raw_video_size: frame_count * frame_size, audio_rate: header[:audio_rate], audio_channels: header[:audio_channels], } end |