# srec: A simple GUI for screen recording # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, version 3. from __future__ import annotations from dataclasses import dataclass from datetime import datetime import os import subprocess import sys from typing import Any, Callable import gi gi.require_version("Gtk", "4.0") gi.require_version("GLib", "2.0") from gi.repository import Gtk # nopep8 pylint: disable=wrong-import-position from gi.repository import GLib # nopep8 pylint: disable=wrong-import-position @dataclass class Stream: process: subprocess.Popen[bytes] @staticmethod def start(command: list[str]) -> Stream: # pylint: disable=consider-using-with return Stream(process=subprocess.Popen(command, stdin=subprocess.PIPE)) def stop(self) -> None: stdin = self.process.stdin assert stdin is not None try: stdin.write(b'q') stdin.flush() except BrokenPipeError: print("Stream already stopped?", file=sys.stderr) self.process.wait() recording: Stream | None = None def make_filename() -> str: directory = os.environ.get( 'XDG_VIDEOS_DIR', os.path.expanduser('~/Videos/SRec')) os.makedirs(directory, exist_ok=True) timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') return os.path.join(directory, f'srec {timestamp}.mkv') def video_source(stack: Gtk.Stack) -> list[str]: if stack.get_child_by_name('not_recording').get_first_child().get_active(): return ['-f', 'x11grab', '-i', ':0.0+0,0'] return ['-f', 'v4l2', '-i', '/dev/video0'] def find_size_display(stack: Gtk.Stack) -> Gtk.Label: return stack.get_child_by_name( 'recording').get_first_child().get_next_sibling() def summarize_size(n: int) -> str: if n > 100_000_000: m = int(n / (1024 * 1024)) return f'{m}M' if n > 100_000: k = int(n / 1024) return f'{k}K' return str(n) def on_start_recording(_: Gtk.Button, stack: Gtk.Stack) -> None: global recording # pylint: disable=global-statement assert recording is None filename = make_filename() size_display = find_size_display(stack) def update_size_display() -> Any: try: size = summarize_size(os.stat(filename).st_size) except FileNotFoundError: size = '--' size_display.set_label(f'{size}') return GLib.SOURCE_REMOVE if recording is None else GLib.SOURCE_CONTINUE GLib.timeout_add_seconds(1, update_size_display) recording = Stream.start( ['ffmpeg', '-framerate', '25'] + video_source(stack) + ['-f', 'pulse', '-ac', '2', '-i', 'default', filename]) stack.set_visible_child_name("recording") def on_stop_recording(_: Gtk.Button, stack: Gtk.Stack) -> None: global recording # pylint: disable=global-statement assert recording is not None recording.stop() recording = None stack.set_visible_child_name("not_recording") def make_button(label: str, action: Callable[[ Gtk.Button, Gtk.Stack], None], stack: Gtk.Stack) -> Gtk.Button: button = Gtk.Button(label=label) button.connect('clicked', action, stack) button.set_margin_top(10) button.set_margin_start(10) button.set_margin_end(10) button.set_margin_bottom(10) return button def make_share_control() -> Gtk.CheckButton: can_share = os.path.exists('/sys/module/v4l2loopback') control = Gtk.CheckButton( label='Share Webcam', sensitive=can_share, active=can_share) control.set_margin_start(20) return control def on_activate(app: Gtk.Application) -> None: win = Gtk.ApplicationWindow(application=app) win.set_title('SRec') win.set_icon_name('srec') stack = Gtk.Stack() nr_box = Gtk.Box() nr_box.set_orientation(Gtk.Orientation.VERTICAL) screen = Gtk.CheckButton(label='Screen') nr_box.append(screen) nr_box.append(Gtk.CheckButton(label='Webcam', active=True, group=screen)) nr_box.append(make_share_control()) nr_box.append(make_button("Start Recording", on_start_recording, stack)) stack.add_named(nr_box, "not_recording") r_box = Gtk.Box() r_box.set_orientation(Gtk.Orientation.VERTICAL) r_box.append(make_button("Stop Recording", on_stop_recording, stack)) r_box.append(Gtk.Label(use_markup=True, justify=Gtk.Justification.CENTER)) stack.add_named(r_box, "recording") win.set_child(stack) win.present() def main() -> None: app = Gtk.Application(application_id='net.chkno.srec') app.connect('activate', on_activate) app.run(None) if __name__ == '__main__': main()