Ver3.5 실시간 화면 캡처 & 녹화 프로그램 완전 정복: 소스코드부터 실전 구현까지

실시간 화면 캡처 & 녹화 프로그램 완전 정복: 소스코드부터 실전 구현까지
화면 캡처와 녹화 프로그램은 개발자, 유튜버, 강의 제작자, 게이머 등 정말 다양한 분야에서 필수적으로 쓰이는 도구야. 근데 막상 "직접 만들어볼까?" 하면 어디서부터 시작해야 할지 막막한 경우가 많지. 이 글에서는 Python과 C#을 중심으로, 실제로 동작하는 화면 캡처 및 녹화 프로그램 소스코드를 단계별로 완전 분해해서 설명해줄게. 라이브러리 선택부터 실전 코드, 최적화 팁까지 싹 다 담았으니까 끝까지 읽어봐! 🔥
화면 캡처/녹화 프로그램은 크게 5가지 핵심 모듈로 구성돼. 이미지 캡처, 영상 녹화, 오디오 캡처, 주석/편집, 저장/출력. 이 모듈들이 유기적으로 연결되어 하나의 완성된 프로그램이 되는 거야. 각 모듈별로 어떤 라이브러리를 쓰는지, 어떻게 코드로 구현하는지 지금부터 하나씩 파헤쳐볼게! 🔍
Python에서 화면 캡처를 구현할 때 가장 많이 쓰이는 라이브러리들이야. 각각의 특징을 알아두면 상황에 맞게 골라 쓸 수 있어!
| 라이브러리 | 특징 | 속도 | 용도 |
|---|---|---|---|
| mss | 가장 빠른 스크린샷 라이브러리 | ⚡⚡⚡ | 실시간 녹화, 고FPS 캡처 |
| Pillow (PIL) | 이미지 처리 종합 라이브러리 | ⚡⚡ | 이미지 편집, 저장 형식 변환 |
| pyautogui | GUI 자동화 + 캡처 | ⚡ | 자동화 스크립트, 단순 캡처 |
| OpenCV (cv2) | 컴퓨터 비전 + 영상 처리 | ⚡⚡⚡ | 영상 녹화, 실시간 처리 |
| PyAudio | 오디오 입출력 | ⚡⚡ | 마이크/시스템 사운드 캡처 |
# 필수 라이브러리 설치 명령어
pip install mss
pip install Pillow
pip install pyautogui
pip install opencv-python
pip install pyaudio
pip install numpy
가장 기본적인 전체 화면 캡처부터 시작해보자. mss 라이브러리를 쓰면 진짜 빠르고 간단하게 구현할 수 있어!
import mss
import mss.tools
from PIL import Image
import datetime
import os
class ScreenCapture:
"""
실시간 화면 캡처 클래스
- 전체 화면 캡처
- 특정 영역 캡처
- 다중 모니터 지원
"""
def __init__(self, save_dir="captures"):
self.save_dir = save_dir
self.sct = mss.mss()
# 저장 디렉토리 생성
if not os.path.exists(save_dir):
os.makedirs(save_dir)
def capture_full_screen(self, monitor_index=1):
"""전체 화면 캡처 (monitor_index: 1=첫번째 모니터)"""
monitor = self.sct.monitors[monitor_index]
screenshot = self.sct.grab(monitor)
# PIL Image로 변환
img = Image.frombytes(
"RGB",
screenshot.size,
screenshot.bgra,
"raw",
"BGRX"
)
return img
def capture_region(self, x, y, width, height):
"""특정 영역만 캡처"""
region = {
"top": y,
"left": x,
"width": width,
"height": height
}
screenshot = self.sct.grab(region)
img = Image.frombytes(
"RGB",
screenshot.size,
screenshot.bgra,
"raw",
"BGRX"
)
return img
def save_screenshot(self, img, filename=None, format="PNG"):
"""캡처된 이미지 저장"""
if filename is None:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screenshot_{timestamp}.{format.lower()}"
filepath = os.path.join(self.save_dir, filename)
img.save(filepath, format=format)
print(f"✅ 저장 완료: {filepath}")
return filepath
def capture_all_monitors(self):
"""모든 모니터 캡처"""
screenshots = []
for i, monitor in enumerate(self.sct.monitors[1:], 1):
screenshot = self.sct.grab(monitor)
img = Image.frombytes(
"RGB",
screenshot.size,
screenshot.bgra,
"raw",
"BGRX"
)
screenshots.append((i, img))
return screenshots
# 사용 예시
if __name__ == "__main__":
capturer = ScreenCapture(save_dir="my_captures")
# 전체 화면 캡처 후 저장
img = capturer.capture_full_screen()
capturer.save_screenshot(img, format="PNG")
# 특정 영역 캡처 (x=100, y=100, 너비=800, 높이=600)
region_img = capturer.capture_region(100, 100, 800, 600)
capturer.save_screenshot(region_img, "region_capture.png")
mss는 초당 약 60~100프레임을 캡처할 수 있는 반면, pyautogui.screenshot()은 초당 약 5~15프레임 수준이야. 실시간 녹화가 목적이라면 무조건 mss를 써야 해!
이제 진짜 핵심인 실시간 녹화 코드야. mss로 프레임을 캡처하고, OpenCV로 영상 파일로 인코딩하는 방식이야. 스레드를 활용해서 부드럽게 동작하도록 구현했어!
import mss
import cv2
import numpy as np
import threading
import time
import datetime
import os
from PIL import Image
class ScreenRecorder:
"""
실시간 화면 녹화 클래스
- 멀티스레드 기반 녹화
- FPS 제어 기능
- 일시정지/재개 지원
- MP4/AVI 출력 지원
"""
def __init__(self, fps=30, output_dir="recordings"):
self.fps = fps
self.output_dir = output_dir
self.is_recording = False
self.is_paused = False
self.record_thread = None
self.video_writer = None
self.frame_count = 0
self.start_time = None
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def _get_screen_size(self):
"""현재 화면 크기 반환"""
with mss.mss() as sct:
monitor = sct.monitors[1]
return monitor["width"], monitor["height"]
def _setup_video_writer(self, filename, width, height):
"""VideoWriter 초기화"""
# MP4 코덱 설정 (H.264)
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
# AVI를 원하면: fourcc = cv2.VideoWriter_fourcc(*'XVID')
writer = cv2.VideoWriter(
filename,
fourcc,
self.fps,
(width, height)
)
return writer
def _record_loop(self, filename, region=None):
"""녹화 루프 (별도 스레드에서 실행)"""
with mss.mss() as sct:
if region:
monitor = region
else:
monitor = sct.monitors[1]
width = monitor["width"]
height = monitor["height"]
self.video_writer = self._setup_video_writer(
filename, width, height
)
frame_interval = 1.0 / self.fps
self.start_time = time.time()
while self.is_recording:
frame_start = time.time()
# 일시정지 상태면 대기
if self.is_paused:
time.sleep(0.05)
continue
# 화면 캡처
screenshot = sct.grab(monitor)
# numpy 배열로 변환 (BGR 형식)
frame = np.array(screenshot)
frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)
# 프레임 쓰기
self.video_writer.write(frame)
self.frame_count += 1
# FPS 제어 (정확한 타이밍 유지)
elapsed = time.time() - frame_start
sleep_time = frame_interval - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
# 녹화 종료 처리
self.video_writer.release()
print(f"🎬 녹화 완료! 총 {self.frame_count}프레임 저장")
def start(self, filename=None, region=None):
"""녹화 시작"""
if self.is_recording:
print("⚠️ 이미 녹화 중입니다!")
return
if filename is None:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(
self.output_dir,
f"recording_{timestamp}.mp4"
)
self.is_recording = True
self.is_paused = False
self.frame_count = 0
self.record_thread = threading.Thread(
target=self._record_loop,
args=(filename, region),
daemon=True
)
self.record_thread.start()
print(f"🔴 녹화 시작! 파일: {filename}")
return filename
def pause(self):
"""녹화 일시정지"""
if self.is_recording and not self.is_paused:
self.is_paused = True
print("⏸️ 녹화 일시정지")
def resume(self):
"""녹화 재개"""
if self.is_recording and self.is_paused:
self.is_paused = False
print("▶️ 녹화 재개")
def stop(self):
"""녹화 중지"""
if not self.is_recording:
print("⚠️ 녹화 중이 아닙니다!")
return
self.is_recording = False
if self.record_thread:
self.record_thread.join(timeout=5)
elapsed = time.time() - self.start_time
print(f"⏹️ 녹화 중지 | 경과시간: {elapsed:.1f}초 | FPS: {self.frame_count/elapsed:.1f}")
def get_status(self):
"""현재 녹화 상태 반환"""
if not self.is_recording:
return "대기 중"
elif self.is_paused:
return "일시정지"
else:
elapsed = time.time() - self.start_time if self.start_time else 0
return f"녹화 중 ({elapsed:.0f}초, {self.frame_count}프레임)"
# 사용 예시
if __name__ == "__main__":
recorder = ScreenRecorder(fps=30)
# 전체 화면 녹화 시작
recorder.start()
time.sleep(5) # 5초 녹화
recorder.pause()
time.sleep(2) # 2초 일시정지
recorder.resume()
time.sleep(3) # 3초 더 녹화
recorder.stop() # 녹화 종료
cv2.VideoWriter_fourcc(*'mp4v')로 생성된 MP4 파일은 일부 플레이어에서 재생이 안 될 수 있어. 이럴 때는 FFmpeg를 이용해서 후처리로 H.264 인코딩을 다시 해주거나, fourcc = cv2.VideoWriter_fourcc(*'avc1')를 시도해봐. macOS에서는 avc1이 더 잘 동작해!
화면만 녹화하면 반쪽짜리잖아? 오디오까지 같이 녹음해서 완성된 영상을 만들어보자. PyAudio로 마이크 입력을 캡처하고, 나중에 FFmpeg로 영상과 합치는 방식이야!
import pyaudio
import wave
import threading
import time
import subprocess
import os
class AudioRecorder:
"""
오디오 녹음 클래스
- 마이크 입력 녹음
- WAV 형식으로 임시 저장
- 영상과 병합 지원
"""
CHUNK = 1024 # 버퍼 크기
FORMAT = pyaudio.paInt16 # 16비트 PCM
CHANNELS = 2 # 스테레오
RATE = 44100 # 샘플링 레이트 (44.1kHz)
def __init__(self, output_dir="recordings"):
self.output_dir = output_dir
self.is_recording = False
self.audio_thread = None
self.frames = []
self.audio_filename = None
def _record_loop(self, filename):
"""오디오 녹음 루프"""
p = pyaudio.PyAudio()
stream = p.open(
format=self.FORMAT,
channels=self.CHANNELS,
rate=self.RATE,
input=True,
frames_per_buffer=self.CHUNK
)
self.frames = []
while self.is_recording:
data = stream.read(self.CHUNK, exception_on_overflow=False)
self.frames.append(data)
# 스트림 종료
stream.stop_stream()
stream.close()
p.terminate()
# WAV 파일로 저장
wf = wave.open(filename, 'wb')
wf.setnchannels(self.CHANNELS)
wf.setsampwidth(p.get_sample_size(self.FORMAT))
wf.setframerate(self.RATE)
wf.writeframes(b''.join(self.frames))
wf.close()
print(f"🎵 오디오 저장 완료: {filename}")
def start(self, filename=None):
"""녹음 시작"""
if filename is None:
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.output_dir, f"audio_{timestamp}.wav")
self.audio_filename = filename
self.is_recording = True
self.audio_thread = threading.Thread(
target=self._record_loop,
args=(filename,),
daemon=True
)
self.audio_thread.start()
print(f"🎙️ 오디오 녹음 시작!")
return filename
def stop(self):
"""녹음 중지"""
self.is_recording = False
if self.audio_thread:
self.audio_thread.join(timeout=3)
return self.audio_filename
def merge_video_audio(video_path, audio_path, output_path):
"""
FFmpeg를 사용해 영상과 오디오 합치기
FFmpeg가 시스템에 설치되어 있어야 함!
"""
command = [
'ffmpeg',
'-i', video_path, # 영상 입력
'-i', audio_path, # 오디오 입력
'-c:v', 'copy', # 영상 코덱 그대로 복사
'-c:a', 'aac', # 오디오 AAC 인코딩
'-strict', 'experimental',
'-shortest', # 짧은 쪽 기준으로 자르기
output_path,
'-y' # 덮어쓰기 허용
]
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode == 0:
print(f"✅ 영상+오디오 합치기 완료: {output_path}")
# 임시 파일 삭제
os.remove(video_path)
os.remove(audio_path)
else:
print(f"❌ 오류 발생: {result.stderr}")
return output_path
# 화면+오디오 동시 녹화 통합 예시
class FullRecorder:
"""화면 + 오디오 동시 녹화"""
def __init__(self, fps=30, output_dir="recordings"):
self.screen_recorder = ScreenRecorder(fps=fps, output_dir=output_dir)
self.audio_recorder = AudioRecorder(output_dir=output_dir)
self.output_dir = output_dir
self.video_file = None
self.audio_file = None
def start(self, output_filename=None):
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
temp_video = os.path.join(self.output_dir, f"temp_video_{timestamp}.mp4")
temp_audio = os.path.join(self.output_dir, f"temp_audio_{timestamp}.wav")
if output_filename is None:
output_filename = os.path.join(
self.output_dir, f"full_recording_{timestamp}.mp4"
)
self.video_file = self.screen_recorder.start(temp_video)
self.audio_file = self.audio_recorder.start(temp_audio)
self.output_filename = output_filename
print("🔴 전체 녹화 시작 (화면 + 오디오)")
def stop(self):
self.screen_recorder.stop()
self.audio_recorder.stop()
time.sleep(0.5) # 파일 쓰기 완료 대기
# 영상과 오디오 합치기
merge_video_audio(
self.video_file,
self.audio_file,
self.output_filename
)
print(f"🎉 최종 파일 저장: {self.output_filename}")
soundcard 라이브러리를 쓰면 더 쉽게 구현 가능해! macOS에서는 BlackHole 같은 가상 오디오 드라이버가 필요해. Linux는 PulseAudio의 monitor 소스를 활용하면 돼.
요즘 개발자들 사이에서 README나 문서에 GIF 넣는 게 트렌드잖아ㅋㅋ 화면 일부를 GIF로 녹화하는 기능도 구현해보자!
import mss
from PIL import Image
import time
import datetime
import os
class GifRecorder:
"""
화면을 GIF로 녹화하는 클래스
- 특정 영역 GIF 녹화
- 프레임 수 제한으로 파일 크기 관리
- 색상 최적화 지원
"""
def __init__(self, fps=10, max_duration=30, output_dir="gifs"):
self.fps = fps
self.max_duration = max_duration # 최대 녹화 시간(초)
self.output_dir = output_dir
self.frames = []
self.is_recording = False
if not os.path.exists(output_dir):
os.makedirs(output_dir)
def record(self, region=None, filename=None):
"""
GIF 녹화 (동기 방식)
region: {"top": y, "left": x, "width": w, "height": h}
"""
self.frames = []
frame_interval = 1.0 / self.fps
max_frames = int(self.fps * self.max_duration)
with mss.mss() as sct:
if region is None:
monitor = sct.monitors[1]
else:
monitor = region
print(f"🎞️ GIF 녹화 시작 (최대 {self.max_duration}초)")
self.is_recording = True
for i in range(max_frames):
if not self.is_recording:
break
frame_start = time.time()
# 캡처
screenshot = sct.grab(monitor)
img = Image.frombytes(
"RGB",
screenshot.size,
screenshot.bgra,
"raw",
"BGRX"
)
# GIF 최적화: 256색으로 변환
img = img.convert("P", palette=Image.ADAPTIVE, colors=256)
self.frames.append(img)
# FPS 제어
elapsed = time.time() - frame_start
sleep_time = frame_interval - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
self.is_recording = False
return self.save_gif(filename)
def save_gif(self, filename=None):
"""수집된 프레임을 GIF로 저장"""
if not self.frames:
print("⚠️ 저장할 프레임이 없습니다!")
return None
if filename is None:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.output_dir, f"recording_{timestamp}.gif")
duration_ms = int(1000 / self.fps) # 프레임당 지속시간(ms)
self.frames[0].save(
filename,
save_all=True,
append_images=self.frames[1:],
duration=duration_ms,
loop=0, # 0 = 무한 반복
optimize=True # 파일 크기 최적화
)
file_size = os.path.getsize(filename) / 1024 / 1024
print(f"✅ GIF 저장 완료: {filename} ({file_size:.1f}MB, {len(self.frames)}프레임)")
return filename
def stop(self):
"""녹화 중지"""
self.is_recording = False
# 사용 예시
if __name__ == "__main__":
gif_recorder = GifRecorder(fps=10, max_duration=5)
# 특정 영역 5초 GIF 녹화
region = {"top": 0, "left": 0, "width": 800, "height": 600}
gif_recorder.record(region=region)
1️⃣ FPS를 낮춰 (10fps 이하 권장)
2️⃣ 캡처 영역을 작게 설정
3️⃣
optimize=True 옵션 사용4️⃣
gifsicle 같은 외부 도구로 후처리 압축
Windows 환경에서 더 강력한 성능을 원한다면 C#이 최고야. GDI+를 이용한 전통적인 방식부터 DirectX 기반의 고성능 캡처까지 알아보자!
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
namespace ScreenCaptureApp
{
/// <summary>
/// GDI+ 기반 화면 캡처 클래스
/// Windows Forms 환경에서 동작
/// </summary>
public class ScreenCapture
{
private string _saveDirectory;
public ScreenCapture(string saveDirectory = "captures")
{
_saveDirectory = saveDirectory;
Directory.CreateDirectory(saveDirectory);
}
/// <summary>
/// 전체 화면 캡처
/// </summary>
public Bitmap CaptureFullScreen()
{
// 전체 가상 화면 크기 (다중 모니터 포함)
Rectangle bounds = SystemInformation.VirtualScreen;
Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height);
using (Graphics g = Graphics.FromImage(bitmap))
{
// 화면에서 비트맵으로 복사
g.CopyFromScreen(
bounds.Location,
Point.Empty,
bounds.Size,
CopyPixelOperation.SourceCopy
);
}
return bitmap;
}
/// <summary>
/// 특정 영역 캡처
/// </summary>
public Bitmap CaptureRegion(Rectangle region)
{
Bitmap bitmap = new Bitmap(region.Width, region.Height);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(
region.Location,
Point.Empty,
region.Size,
CopyPixelOperation.SourceCopy
);
}
return bitmap;
}
/// <summary>
/// 특정 창(Window) 캡처
/// </summary>
public Bitmap CaptureWindow(IntPtr windowHandle)
{
// 창의 위치와 크기 가져오기
RECT rect;
GetWindowRect(windowHandle, out rect);
Rectangle bounds = new Rectangle(
rect.Left, rect.Top,
rect.Right - rect.Left,
rect.Bottom - rect.Top
);
return CaptureRegion(bounds);
}
/// <summary>
/// 비트맵을 파일로 저장
/// </summary>
public string SaveBitmap(Bitmap bitmap, string filename = null,
ImageFormat format = null)
{
if (format == null) format = ImageFormat.Png;
if (filename == null)
{
string ext = format == ImageFormat.Jpeg ? "jpg" : "png";
filename = $"screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.{ext}";
}
string filepath = Path.Combine(_saveDirectory, filename);
bitmap.Save(filepath, format);
Console.WriteLine($"✅ 저장 완료: {filepath}");
return filepath;
}
/// <summary>
/// 클립보드에 복사
/// </summary>
public void CopyToClipboard(Bitmap bitmap)
{
Clipboard.SetImage(bitmap);
Console.WriteLine("📋 클립보드에 복사 완료!");
}
// Windows API 선언
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[System.Runtime.InteropServices.StructLayout(
System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct RECT
{
public int Left, Top, Right, Bottom;
}
}
// 사용 예시
class Program
{
static void Main(string[] args)
{
var capture = new ScreenCapture("my_captures");
// 전체 화면 캡처 및 저장
using (Bitmap screenshot = capture.CaptureFullScreen())
{
capture.SaveBitmap(screenshot, format: ImageFormat.Png);
capture.CopyToClipboard(screenshot);
}
// 특정 영역 캡처 (x=0, y=0, 너비=1920, 높이=1080)
Rectangle region = new Rectangle(0, 0, 1920, 1080);
using (Bitmap regionShot = capture.CaptureRegion(region))
{
capture.SaveBitmap(regionShot, "region.png");
}
}
}
}
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.IO;
using System.Collections.Generic;
// NuGet: SharpAvi 패키지 필요
// Install-Package SharpAvi
using SharpAvi;
using SharpAvi.Output;
using SharpAvi.Codecs;
namespace ScreenRecorderApp
{
/// <summary>
/// SharpAvi를 이용한 화면 녹화 클래스
/// AVI 형식으로 출력 (후처리로 MP4 변환 가능)
/// </summary>
public class ScreenRecorder : IDisposable
{
private readonly int _fps;
private readonly Rectangle _captureArea;
private AviWriter _writer;
private IAviVideoStream _videoStream;
private Thread _recordThread;
private bool _isRecording;
private bool _isPaused;
private int _frameCount;
private DateTime _startTime;
public ScreenRecorder(int fps = 30, Rectangle? captureArea = null)
{
_fps = fps;
_captureArea = captureArea ??
new Rectangle(0, 0,
Screen.PrimaryScreen.Bounds.Width,
Screen.PrimaryScreen.Bounds.Height);
}
public string Start(string outputPath = null)
{
if (_isRecording)
{
Console.WriteLine("⚠️ 이미 녹화 중입니다!");
return null;
}
if (outputPath == null)
{
outputPath = $"recording_{DateTime.Now:yyyyMMdd_HHmmss}.avi";
}
// AVI 라이터 초기화
_writer = new AviWriter(outputPath)
{
FramesPerSecond = _fps,
EmitIndex1 = true
};
// 비디오 스트림 생성 (Motion JPEG 코덱)
_videoStream = _writer.AddMotionJpegVideoStream(
_captureArea.Width,
_captureArea.Height,
quality: 70 // 화질 (1-100)
);
_isRecording = true;
_isPaused = false;
_frameCount = 0;
_startTime = DateTime.Now;
_recordThread = new Thread(RecordLoop)
{
IsBackground = true,
Priority = ThreadPriority.AboveNormal
};
_recordThread.Start();
Console.WriteLine($"🔴 녹화 시작! 파일: {outputPath}");
return outputPath;
}
private void RecordLoop()
{
int frameInterval = 1000 / _fps; // ms 단위
byte[] buffer = new byte[
_captureArea.Width * _captureArea.Height * 4
];
while (_isRecording)
{
if (_isPaused)
{
Thread.Sleep(50);
continue;
}
var frameStart = DateTime.Now;
// 화면 캡처
using (Bitmap bitmap = new Bitmap(
_captureArea.Width, _captureArea.Height))
{
using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(
_captureArea.Location,
Point.Empty,
_captureArea.Size,
CopyPixelOperation.SourceCopy
);
}
// 비트맵 데이터를 버퍼로 복사
var bitmapData = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppRgb
);
System.Runtime.InteropServices.Marshal.Copy(
bitmapData.Scan0, buffer, 0, buffer.Length
);
bitmap.UnlockBits(bitmapData);
}
// 프레임 쓰기
lock (_videoStream)
{
_videoStream.WriteFrame(true, buffer, 0, buffer.Length);
}
_frameCount++;
// FPS 제어
var elapsed = (DateTime.Now - frameStart).TotalMilliseconds;
int sleepTime = (int)(frameInterval - elapsed);
if (sleepTime > 0)
Thread.Sleep(sleepTime);
}
}
public void Pause()
{
_isPaused = true;
Console.WriteLine("⏸️ 녹화 일시정지");
}
public void Resume()
{
_isPaused = false;
Console.WriteLine("▶️ 녹화 재개");
}
public void Stop()
{
_isRecording = false;
_recordThread?.Join(5000);
var duration = DateTime.Now - _startTime;
Console.WriteLine($"⏹️ 녹화 완료 | {duration.TotalSeconds:F1}초 | {_frameCount}프레임");
}
public void Dispose()
{
if (_isRecording) Stop();
_writer?.Close();
}
}
}
캡처 프로그램에서 빠질 수 없는 게 바로 마우스로 영역을 드래그해서 선택하는 UI야! Python + tkinter로 구현해보자.
import tkinter as tk
from PIL import Image, ImageTk
import mss
import mss.tools
class RegionSelector:
"""
마우스 드래그로 캡처 영역을 선택하는 UI
- 반투명 오버레이
- 실시간 선택 영역 표시
- 좌표 반환
"""
def __init__(self):
self.start_x = None
self.start_y = None
self.end_x = None
self.end_y = None
self.rect = None
self.selected_region = None
def select_region(self):
"""영역 선택 UI 실행 후 선택된 영역 반환"""
# 현재 화면 캡처 (배경으로 사용)
with mss.mss() as sct:
monitor = sct.monitors[0] # 전체 가상 화면
screenshot = sct.grab(monitor)
bg_image = Image.frombytes(
"RGB", screenshot.size, screenshot.bgra, "raw", "BGRX"
)
# tkinter 전체화면 창 생성
root = tk.Tk()
root.attributes('-fullscreen', True)
root.attributes('-alpha', 0.3) # 반투명
root.attributes('-topmost', True)
root.configure(bg='black')
root.config(cursor="crosshair")
# 배경 이미지 설정
bg_photo = ImageTk.PhotoImage(bg_image)
canvas = tk.Canvas(
root,
width=bg_image.width,
height=bg_image.height,
highlightthickness=0
)
canvas.pack()
canvas.create_image(0, 0, anchor='nw', image=bg_photo)
# 안내 텍스트
canvas.create_text(
bg_image.width // 2, 30,
text="드래그하여 캡처 영역을 선택하세요 | ESC: 취소",
fill='white',
font=('Arial', 16, 'bold')
)
def on_mouse_press(event):
self.start_x = event.x
self.start_y = event.y
if self.rect:
canvas.delete(self.rect)
def on_mouse_drag(event):
if self.rect:
canvas.delete(self.rect)
# 선택 영역 사각형 그리기
self.rect = canvas.create_rectangle(
self.start_x, self.start_y,
event.x, event.y,
outline='#00ff88',
width=2,
fill='#00ff88',
stipple='gray25' # 반투명 효과
)
# 크기 표시
w = abs(event.x - self.start_x)
h = abs(event.y - self.start_y)
canvas.delete('size_label')
canvas.create_text(
event.x + 10, event.y + 10,
text=f"{w} × {h}",
fill='#00ff88',
font=('Arial', 12, 'bold'),
tags='size_label'
)
def on_mouse_release(event):
self.end_x = event.x
self.end_y = event.y
# 선택 영역 계산
x1 = min(self.start_x, self.end_x)
y1 = min(self.start_y, self.end_y)
x2 = max(self.start_x, self.end_x)
y2 = max(self.start_y, self.end_y)
if x2 - x1 > 10 and y2 - y1 > 10: # 최소 크기 체크
self.selected_region = {
"left": x1,
"top": y1,
"width": x2 - x1,
"height": y2 - y1
}
root.destroy()
def on_escape(event):
self.selected_region = None
root.destroy()
# 이벤트 바인딩
canvas.bind('<ButtonPress-1>', on_mouse_press)
canvas.bind('<B1-Motion>', on_mouse_drag)
canvas.bind('<ButtonRelease-1>', on_mouse_release)
root.bind('<Escape>', on_escape)
root.mainloop()
return self.selected_region
# 사용 예시
if __name__ == "__main__":
selector = RegionSelector()
region = selector.select_region()
if region:
print(f"선택된 영역: {region}")
# 선택 영역 캡처
capturer = ScreenCapture()
img = capturer.capture_region(
region["left"], region["top"],
region["width"], region["height"]
)
capturer.save_screenshot(img)
else:
print("영역 선택이 취소되었습니다.")
FFmpeg는 화면 녹화 프로그램의 심장이라고 할 수 있어. Python에서 subprocess로 FFmpeg를 직접 호출하거나, ffmpeg-python 라이브러리를 통해 더 편하게 쓸 수 있어!
import subprocess
import mss
import numpy as np
import sys
class FFmpegRecorder:
"""
FFmpeg 파이프를 이용한 고품질 화면 녹화
- H.264 인코딩
- 실시간 스트리밍 가능
- 최고 품질의 출력
"""
def __init__(self, fps=30, output_dir="recordings"):
self.fps = fps
self.output_dir = output_dir
self.process = None
self.is_recording = False
# FFmpeg 설치 확인
try:
subprocess.run(['ffmpeg', '-version'],
capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
raise RuntimeError("FFmpeg가 설치되어 있지 않습니다!")
def start(self, output_filename, width=None, height=None,
quality='medium', region=None):
"""
FFmpeg 파이프 녹화 시작
quality: 'low', 'medium', 'high', 'lossless'
"""
with mss.mss() as sct:
if region:
monitor = region
else:
monitor = sct.monitors[1]
if width is None:
width = monitor["width"]
if height is None:
height = monitor["height"]
# 품질별 CRF 값 (낮을수록 고품질, 파일 큼)
crf_map = {
'low': '35',
'medium': '23',
'high': '18',
'lossless': '0'
}
crf = crf_map.get(quality, '23')
# FFmpeg 명령어 구성
ffmpeg_cmd = [
'ffmpeg',
'-y', # 덮어쓰기
'-f', 'rawvideo', # 입력 형식: 원시 비디오
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24', # 픽셀 형식
'-s', f'{width}x{height}', # 해상도
'-r', str(self.fps), # FPS
'-i', '-', # stdin에서 입력
'-c:v', 'libx264', # H.264 인코딩
'-preset', 'ultrafast', # 인코딩 속도 (ultrafast/fast/medium/slow)
'-crf', crf, # 품질
'-pix_fmt', 'yuv420p', # 출력 픽셀 형식 (호환성)
'-movflags', '+faststart', # 웹 스트리밍 최적화
output_filename
]
# FFmpeg 프로세스 시작
self.process = subprocess.Popen(
ffmpeg_cmd,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE
)
self.is_recording = True
self.monitor = region or None
self.width = width
self.height = height
print(f"🔴 FFmpeg 녹화 시작! ({width}x{height} @ {self.fps}fps, 품질: {quality})")
return output_filename
def capture_and_write_frame(self):
"""프레임 캡처 후 FFmpeg에 전송"""
if not self.is_recording or not self.process:
return False
with mss.mss() as sct:
monitor = self.monitor or sct.monitors[1]
screenshot = sct.grab(monitor)
frame = np.array(screenshot)
frame = frame[:, :, :3] # BGRA -> BGR
try:
self.process.stdin.write(frame.tobytes())
return True
except BrokenPipeError:
print("❌ FFmpeg 파이프 오류!")
return False
def stop(self):
"""녹화 중지"""
if not self.is_recording:
return
self.is_recording = False
if self.process:
self.process.stdin.close()
self.process.wait()
# FFmpeg 오류 확인
stderr_output = self.process.stderr.read().decode()
if self.process.returncode != 0:
print(f"⚠️ FFmpeg 경고: {stderr_output[-500:]}")
else:
print("✅ FFmpeg 녹화 완료!")
def record_for_duration(self, output_filename, duration_seconds, region=None):
"""지정된 시간 동안 녹화"""
import time
self.start(output_filename, region=region)
frame_interval = 1.0 / self.fps
end_time = time.time() + duration_seconds
frame_count = 0
while time.time() < end_time and self.is_recording:
frame_start = time.time()
if not self.capture_and_write_frame():
break
frame_count += 1
elapsed = time.time() - frame_start
sleep_time = frame_interval - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
self.stop()
print(f"📊 총 {frame_count}프레임 녹화 완료")
# 사용 예시
if __name__ == "__main__":
import time
recorder = FFmpegRecorder(fps=30)
# 10초 동안 고품질 녹화
recorder.record_for_duration(
"output_high_quality.mp4",
duration_seconds=10,
quality='high'
)
fast: 균형 잡힌 설정 → 일반 녹화에 추천
medium: 기본값, 품질과 속도 균형
slow/veryslow: 최고 압축률, 인코딩 느림 → 후처리 변환에 적합
지금까지 만든 기능들을 하나의 GUI 프로그램으로 합쳐보자! 실제로 쓸 수 있는 수준의 화면 캡처/녹화 프로그램이야. 재능넷 같은 플랫폼에서 이런 완성형 소스코드를 공유하면 진짜 인기 많을 것 같은데ㅋㅋ 😄
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import time
import os
import mss
import cv2
import numpy as np
from PIL import Image, ImageTk
import datetime
class ScreenCaptureApp:
"""
완성형 화면 캡처/녹화 GUI 프로그램
- 스크린샷 캡처
- 영상 녹화 (MP4)
- 영역 선택
- 실시간 미리보기
"""
def __init__(self, root):
self.root = root
self.root.title("🎬 화면 캡처/녹화 프로그램")
self.root.geometry("480x600")
self.root.resizable(False, False)
self.root.configure(bg='#f8f7f4')
# 상태 변수
self.is_recording = False
self.recorder = None
self.record_thread = None
self.frame_count = 0
self.start_time = None
self.selected_region = None
self.output_dir = os.path.join(os.path.expanduser("~"), "Desktop", "Captures")
os.makedirs(self.output_dir, exist_ok=True)
self._build_ui()
self._update_timer()
def _build_ui(self):
"""UI 구성"""
# 헤더
header = tk.Frame(self.root, bg='#3a2d6a', height=70)
header.pack(fill='x')
header.pack_propagate(False)
tk.Label(
header,
text="🎬 화면 캡처 & 녹화",
font=('Arial', 18, 'bold'),
bg='#3a2d6a', fg='white'
).pack(expand=True)
# 미리보기 영역
preview_frame = tk.Frame(self.root, bg='#2d2d2d', height=200)
preview_frame.pack(fill='x', padx=16, pady=12)
preview_frame.pack_propagate(False)
self.preview_label = tk.Label(
preview_frame,
text="미리보기",
bg='#2d2d2d', fg='#888',
font=('Arial', 12)
)
self.preview_label.pack(expand=True)
# 미리보기 업데이트 버튼
tk.Button(
self.root,
text="🔄 미리보기 갱신",
command=self._update_preview,
bg='#e8e0f7', fg='#3a2d6a',
font=('Arial', 10),
relief='flat', padx=10, pady=5
).pack(pady=4)
# 구분선
ttk.Separator(self.root, orient='horizontal').pack(fill='x', padx=16, pady=8)
# 설정 영역
settings_frame = tk.Frame(self.root, bg='#f8f7f4')
settings_frame.pack(fill='x', padx=16)
# FPS 설정
fps_frame = tk.Frame(settings_frame, bg='#f8f7f4')
fps_frame.pack(fill='x', pady=4)
tk.Label(fps_frame, text="FPS:", bg='#f8f7f4',
font=('Arial', 11)).pack(side='left')
self.fps_var = tk.IntVar(value=30)
fps_spin = tk.Spinbox(
fps_frame, from_=5, to=60,
textvariable=self.fps_var,
width=5, font=('Arial', 11)
)
fps_spin.pack(side='left', padx=8)
# 품질 설정
tk.Label(fps_frame, text="품질:", bg='#f8f7f4',
font=('Arial', 11)).pack(side='left', padx=(16,0))
self.quality_var = tk.StringVar(value='medium')
quality_combo = ttk.Combobox(
fps_frame,
textvariable=self.quality_var,
values=['low', 'medium', 'high', 'lossless'],
width=8, state='readonly'
)
quality_combo.pack(side='left', padx=8)
# 영역 선택
region_frame = tk.Frame(settings_frame, bg='#f8f7f4')
region_frame.pack(fill='x', pady=4)
self.region_label = tk.Label(
region_frame,
text="📐 영역: 전체 화면",
bg='#f8f7f4', fg='#4a3f7a',
font=('Arial', 10)
)
self.region_label.pack(side='left')
tk.Button(
region_frame,
text="영역 선택",
command=self._select_region,
bg='#d4e8f7', fg='#3a7a9a',
font=('Arial', 9), relief='flat',
padx=8, pady=3
).pack(side='right')
tk.Button(
region_frame,
text="전체 화면",
command=self._reset_region,
bg='#e8e0f7', fg='#4a3f7a',
font=('Arial', 9), relief='flat',
padx=8, pady=3
).pack(side='right', padx=4)
# 저장 경로
path_frame = tk.Frame(settings_frame, bg='#f8f7f4')
path_frame.pack(fill='x', pady=4)
self.path_label = tk.Label(
path_frame,
text=f"💾 {self.output_dir[:40]}...",
bg='#f8f7f4', fg='#666',
font=('Arial', 9)
)
self.path_label.pack(side='left')
tk.Button(
path_frame,
text="변경",
command=self._change_output_dir,
bg='#f0f0f0', fg='#444',
font=('Arial', 9), relief='flat',
padx=6, pady=2
).pack(side='right')
# 구분선
ttk.Separator(self.root, orient='horizontal').pack(fill='x', padx=16, pady=8)
# 상태 표시
self.status_label = tk.Label(
self.root,
text="⏸ 대기 중",
font=('Arial', 12, 'bold'),
bg='#f8f7f4', fg='#666'
)
self.status_label.pack(pady=4)
self.timer_label = tk.Label(
self.root,
text="00:00:00",
font=('Arial', 24, 'bold'),
bg='#f8f7f4', fg='#3a2d6a'
)
self.timer_label.pack(pady=4)
# 버튼 영역
btn_frame = tk.Frame(self.root, bg='#f8f7f4')
btn_frame.pack(pady=12)
# 스크린샷 버튼
tk.Button(
btn_frame,
text="📸 스크린샷",
command=self._take_screenshot,
bg='#7acba8', fg='white',
font=('Arial', 12, 'bold'),
relief='flat', padx=16, pady=10,
width=10
).pack(side='left', padx=6)
# 녹화 시작/중지 버튼
self.record_btn = tk.Button(
btn_frame,
text="🔴 녹화 시작",
command=self._toggle_recording,
bg='#e87a9a', fg='white',
font=('Arial', 12, 'bold'),
relief='flat', padx=16, pady=10,
width=10
)
self.record_btn.pack(side='left', padx=6)
# 일시정지 버튼
self.pause_btn = tk.Button(
btn_frame,
text="⏸ 일시정지",
command=self._toggle_pause,
bg='#e8b87a', fg='white',
font=('Arial', 12, 'bold'),
relief='flat', padx=16, pady=10,
width=10,
state='disabled'
)
self.pause_btn.pack(side='left', padx=6)
# 폴더 열기
tk.Button(
self.root,
text="📁 저장 폴더 열기",
command=self._open_output_dir,
bg='#f0f0f0', fg='#444',
font=('Arial', 10), relief='flat',
padx=12, pady=6
).pack(pady=4)
def _update_preview(self):
"""미리보기 업데이트"""
try:
with mss.mss() as sct:
monitor = self.selected_region or sct.monitors[1]
screenshot = sct.grab(monitor)
img = Image.frombytes(
"RGB", screenshot.size, screenshot.bgra, "raw", "BGRX"
)
# 미리보기 크기로 리사이즈
img.thumbnail((448, 196), Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
self.preview_label.configure(image=photo, text='')
self.preview_label.image = photo
except Exception as e:
print(f"미리보기 오류: {e}")
def _select_region(self):
"""영역 선택"""
self.root.withdraw() # 메인 창 숨기기
time.sleep(0.3)
selector = RegionSelector()
region = selector.select_region()
self.root.deiconify() # 메인 창 다시 표시
if region:
self.selected_region = region
self.region_label.config(
text=f"📐 {region['width']}x{region['height']} "
f"({region['left']}, {region['top']})"
)
self._update_preview()
def _reset_region(self):
"""전체 화면으로 리셋"""
self.selected_region = None
self.region_label.config(text="📐 영역: 전체 화면")
self._update_preview()
def _take_screenshot(self):
"""스크린샷 촬영"""
try:
with mss.mss() as sct:
monitor = self.selected_region or sct.monitors[1]
screenshot = sct.grab(monitor)
img = Image.frombytes(
"RGB", screenshot.size, screenshot.bgra, "raw", "BGRX"
)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.output_dir, f"screenshot_{timestamp}.png")
img.save(filename)
self.status_label.config(
text=f"✅ 저장: screenshot_{timestamp}.png",
fg='#7acba8'
)
self.root.after(3000, lambda: self.status_label.config(
text="⏸ 대기 중", fg='#666'
))
except Exception as e:
messagebox.showerror("오류", f"스크린샷 실패: {e}")
def _toggle_recording(self):
"""녹화 시작/중지 토글"""
if not self.is_recording:
self._start_recording()
else:
self._stop_recording()
def _start_recording(self):
"""녹화 시작"""
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.output_dir, f"recording_{timestamp}.mp4")
fps = self.fps_var.get()
self.recorder = ScreenRecorder(fps=fps)
self.recorder.start(filename, region=self.selected_region)
self.is_recording = True
self.start_time = time.time()
self.frame_count = 0
self.record_btn.config(text="⏹ 녹화 중지", bg='#cc4444')
self.pause_btn.config(state='normal')
self.status_label.config(text="🔴 녹화 중...", fg='#cc4444')
def _stop_recording(self):
"""녹화 중지"""
if self.recorder:
self.recorder.stop()
self.is_recording = False
self.recorder = None
self.record_btn.config(text="🔴 녹화 시작", bg='#e87a9a')
self.pause_btn.config(state='disabled', text="⏸ 일시정지")
self.status_label.config(text="✅ 녹화 완료!", fg='#7acba8')
self.timer_label.config(text="00:00:00")
self.root.after(3000, lambda: self.status_label.config(
text="⏸ 대기 중", fg='#666'
))
def _toggle_pause(self):
"""일시정지/재개 토글"""
if not self.recorder:
return
if not self.recorder.is_paused:
self.recorder.pause()
self.pause_btn.config(text="▶ 재개")
self.status_label.config(text="⏸ 일시정지", fg='#e8b87a')
else:
self.recorder.resume()
self.pause_btn.config(text="⏸ 일시정지")
self.status_label.config(text="🔴 녹화 중...", fg='#cc4444')
def _update_timer(self):
"""타이머 업데이트 (100ms마다)"""
if self.is_recording and self.start_time:
elapsed = time.time() - self.start_time
hours = int(elapsed // 3600)
minutes = int((elapsed % 3600) // 60)
seconds = int(elapsed % 60)
self.timer_label.config(
text=f"{hours:02d}:{minutes:02d}:{seconds:02d}"
)
self.root.after(100, self._update_timer)
def _change_output_dir(self):
"""저장 경로 변경"""
new_dir = filedialog.askdirectory(initialdir=self.output_dir)
if new_dir:
self.output_dir = new_dir
display = new_dir if len(new_dir) <= 40 else f"...{new_dir[-37:]}"
self.path_label.config(text=f"💾 {display}")
def _open_output_dir(self):
"""저장 폴더 열기"""
import subprocess
if os.name == 'nt': # Windows
subprocess.Popen(['explorer', self.output_dir])
elif sys.platform == 'darwin': # macOS
subprocess.Popen(['open', self.output_dir])
else: # Linux
subprocess.Popen(['xdg-open', self.output_dir])
# 메인 실행
if __name__ == "__main__":
root = tk.Tk()
app = ScreenCaptureApp(root)
root.mainloop()
브라우저에서도 화면 캡처가 가능해! Screen Capture API를 사용하면 별도 프로그램 설치 없이 웹에서 화면을 캡처하고 녹화할 수 있어. 요즘 웹 앱 개발할 때 진짜 많이 쓰이는 기술이야!
// 웹 기반 화면 캡처 및 녹화 (JavaScript)
// HTML: <video id="preview"></video>
// HTML: <button id="startBtn">녹화 시작</button>
class WebScreenRecorder {
constructor() {
this.mediaStream = null;
this.mediaRecorder = null;
this.recordedChunks = [];
this.isRecording = false;
}
/**
* 화면 캡처 스트림 시작
* @param {Object} options - 캡처 옵션
*/
async startCapture(options = {}) {
const defaultOptions = {
video: {
cursor: 'always', // 커서 포함
displaySurface: 'monitor' // 전체 모니터
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
};
const captureOptions = { ...defaultOptions, ...options };
try {
// 화면 캡처 권한 요청
this.mediaStream = await navigator.mediaDevices.getDisplayMedia(
captureOptions
);
console.log('✅ 화면 캡처 스트림 시작!');
return this.mediaStream;
} catch (error) {
if (error.name === 'NotAllowedError') {
throw new Error('사용자가 화면 공유를 거부했습니다.');
}
throw error;
}
}
/**
* 스크린샷 캡처 (단일 프레임)
*/
async captureScreenshot() {
if (!this.mediaStream) {
await this.startCapture({ audio: false });
}
const videoTrack = this.mediaStream.getVideoTracks()[0];
// ImageCapture API 사용
const imageCapture = new ImageCapture(videoTrack);
const bitmap = await imageCapture.grabFrame();
// Canvas에 그리기
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
// PNG로 변환
const blob = await new Promise(resolve => {
canvas.toBlob(resolve, 'image/png');
});
return blob;
}
/**
* 녹화 시작
*/
async startRecording(options = {}) {
if (!this.mediaStream) {
await this.startCapture();
}
this.recordedChunks = [];
// 지원되는 MIME 타입 확인
const mimeType = this._getSupportedMimeType();
const recorderOptions = {
mimeType,
videoBitsPerSecond: options.videoBitrate || 2500000, // 2.5Mbps
audioBitsPerSecond: options.audioBitrate || 128000 // 128kbps
};
this.mediaRecorder = new MediaRecorder(
this.mediaStream,
recorderOptions
);
// 데이터 수집
this.mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
// 녹화 완료 처리
this.mediaRecorder.onstop = () => {
this._processRecording();
};
// 오류 처리
this.mediaRecorder.onerror = (error) => {
console.error('녹화 오류:', error);
};
// 100ms마다 데이터 수집
this.mediaRecorder.start(100);
this.isRecording = true;
console.log(`🔴 웹 녹화 시작! (${mimeType})`);
}
/**
* 녹화 중지
*/
stopRecording() {
if (!this.isRecording) return;
this.mediaRecorder.stop();
this.isRecording = false;
// 스트림 종료
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = null;
}
console.log('⏹️ 녹화 중지');
}
/**
* 녹화된 영상 처리 및 다운로드
*/
_processRecording() {
const blob = new Blob(this.recordedChunks, {
type: this.mediaRecorder.mimeType
});
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
// 자동 다운로드
const a = document.createElement('a');
a.href = url;
a.download = `recording_${timestamp}.webm`;
a.click();
// 메모리 해제
setTimeout(() => URL.revokeObjectURL(url), 1000);
const sizeMB = (blob.size / 1024 / 1024).toFixed(2);
console.log(`✅ 녹화 파일 저장 완료! (${sizeMB}MB)`);
}
/**
* 지원되는 MIME 타입 반환
*/
_getSupportedMimeType() {
const types = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm',
'video/mp4'
];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return 'video/webm'; // 기본값
}
/**
* 스크린샷 다운로드
*/
async downloadScreenshot() {
const blob = await this.captureScreenshot();
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const a = document.createElement('a');
a.href = url;
a.download = `screenshot_${timestamp}.png`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
console.log('📸 스크린샷 저장 완료!');
}
}
// 사용 예시
const recorder = new WebScreenRecorder();
// 스크린샷
document.getElementById('screenshotBtn').addEventListener('click', async () => {
await recorder.downloadScreenshot();
});
// 녹화 시작/중지
document.getElementById('recordBtn').addEventListener('click', async () => {
if (!recorder.isRecording) {
await recorder.startRecording({ videoBitrate: 5000000 }); // 5Mbps
document.getElementById('recordBtn').textContent = '⏹ 녹화 중지';
} else {
recorder.stopRecording();
document.getElementById('recordBtn').textContent = '🔴 녹화 시작';
}
});
getDisplayMedia() API는 Chrome 72+, Firefox 66+, Edge 79+에서 지원해. Safari는 macOS 13+ (Ventura)부터 지원 시작했어. 모바일 브라우저는 아직 지원이 제한적이야. 항상 navigator.mediaDevices.getDisplayMedia가 존재하는지 먼저 체크하는 게 좋아!
# NVIDIA GPU가 있을 때 FFmpeg NVENC 하드웨어 인코딩 사용
# CPU 부하를 획기적으로 줄일 수 있음!
def get_ffmpeg_cmd_with_gpu(output_file, width, height, fps):
"""
GPU 가속 FFmpeg 명령어 생성
NVIDIA: h264_nvenc
AMD: h264_amf
Intel: h264_qsv
"""
# NVIDIA GPU 사용 가능 여부 확인
try:
result = subprocess.run(
['ffmpeg', '-encoders'],
capture_output=True, text=True
)
has_nvenc = 'h264_nvenc' in result.stdout
has_amf = 'h264_amf' in result.stdout
has_qsv = 'h264_qsv' in result.stdout
except:
has_nvenc = has_amf = has_qsv = False
# 사용 가능한 하드웨어 인코더 선택
if has_nvenc:
encoder = 'h264_nvenc'
extra_args = ['-preset', 'p4', '-rc', 'vbr', '-cq', '23']
print("🚀 NVIDIA NVENC 하드웨어 인코딩 사용!")
elif has_amf:
encoder = 'h264_amf'
extra_args = ['-quality', 'balanced']
print("🚀 AMD AMF 하드웨어 인코딩 사용!")
elif has_qsv:
encoder = 'h264_qsv'
extra_args = ['-global_quality', '23']
print("🚀 Intel QuickSync 하드웨어 인코딩 사용!")
else:
encoder = 'libx264'
extra_args = ['-preset', 'ultrafast', '-crf', '23']
print("💻 소프트웨어 인코딩 사용 (GPU 없음)")
cmd = [
'ffmpeg', '-y',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-pix_fmt', 'bgr24',
'-s', f'{width}x{height}',
'-r', str(fps),
'-i', '-',
'-c:v', encoder,
*extra_args,
'-pix_fmt', 'yuv420p',
output_file
]
return cmd
import queue
import threading
class OptimizedRecorder:
"""
프레임 큐를 이용한 최적화된 녹화 클래스
- 캡처 스레드와 인코딩 스레드 분리
- 메모리 사용량 제어
- 드롭 프레임 최소화
"""
MAX_QUEUE_SIZE = 30 # 최대 버퍼 프레임 수
def __init__(self, fps=30):
self.fps = fps
self.frame_queue = queue.Queue(maxsize=self.MAX_QUEUE_SIZE)
self.is_running = False
self.dropped_frames = 0
self.total_frames = 0
def _capture_thread(self, region=None):
"""캡처 전용 스레드 (고우선순위)"""
frame_interval = 1.0 / self.fps
with mss.mss() as sct:
monitor = region or sct.monitors[1]
while self.is_running:
frame_start = time.perf_counter()
# 캡처
screenshot = sct.grab(monitor)
frame = np.array(screenshot)[:, :, :3]
# 큐에 추가 (가득 차면 오래된 프레임 드롭)
try:
self.frame_queue.put_nowait(frame)
self.total_frames += 1
except queue.Full:
# 큐가 가득 찼을 때 가장 오래된 프레임 제거 후 새 프레임 추가
try:
self.frame_queue.get_nowait()
self.frame_queue.put_nowait(frame)
self.dropped_frames += 1
except queue.Empty:
pass
# FPS 제어
elapsed = time.perf_counter() - frame_start
sleep_time = frame_interval - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
def _encode_thread(self, ffmpeg_process):
"""인코딩 전용 스레드"""
while self.is_running or not self.frame_queue.empty():
try:
frame = self.frame_queue.get(timeout=1.0)
ffmpeg_process.stdin.write(frame.tobytes())
except queue.Empty:
continue
except BrokenPipeError:
break
def record(self, output_file, duration=None, region=None):
"""최적화된 녹화 실행"""
with mss.mss() as sct:
monitor = region or sct.monitors[1]
width = monitor["width"]
height = monitor["height"]
# FFmpeg 프로세스 시작
cmd = get_ffmpeg_cmd_with_gpu(output_file, width, height, self.fps)
process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.DEVNULL)
self.is_running = True
self.dropped_frames = 0
self.total_frames = 0
# 스레드 시작
capture_t = threading.Thread(
target=self._capture_thread, args=(region,),
daemon=True
)
encode_t = threading.Thread(
target=self._encode_thread, args=(process,),
daemon=True
)
capture_t.start()
encode_t.start()
# 지정 시간 후 종료
if duration:
time.sleep(duration)
self.stop()
capture_t.join()
encode_t.join()
process.stdin.close()
process.wait()
drop_rate = self.dropped_frames / max(self.total_frames, 1) * 100
print(f"📊 총 {self.total_frames}프레임 | 드롭: {self.dropped_frames} ({drop_rate:.1f}%)")
def stop(self):
self.is_running = False
완성된 프로그램을 exe 파일로 만들어서 배포해보자! PyInstaller를 쓰면 Python 없이도 실행 가능한 독립 실행 파일을 만들 수 있어. 이런 완성형 프로그램을 재능넷에서 공유하면 다른 개발자들한테 진짜 도움이 많이 될 것 같아!
# PyInstaller로 단일 exe 파일 생성
# 먼저 설치: pip install pyinstaller
# 기본 패키징
pyinstaller --onefile --windowed screen_capture_app.py
# 아이콘 포함
pyinstaller --onefile --windowed --icon=app_icon.ico screen_capture_app.py
# 숨겨진 임포트 포함 (mss, cv2 등)
pyinstaller --onefile --windowed \
--hidden-import=mss \
--hidden-import=cv2 \
--hidden-import=PIL \
--hidden-import=numpy \
screen_capture_app.py
# spec 파일로 세밀한 제어
# screen_capture_app.spec 파일 생성 후 수정:
# a = Analysis(['screen_capture_app.py'],
# pathex=[],
# binaries=[],
# datas=[('assets', 'assets')], # 리소스 파일 포함
# hiddenimports=['mss', 'cv2'],
# ...)
✅ 필요한 Visual C++ 런타임 확인 (Windows)
✅ 안티바이러스 오탐 방지를 위한 코드 서명 고려
✅ 관리자 권한 필요 여부 확인 (UAC 매니페스트)
✅ 다중 모니터 환경 테스트
✅ 고DPI 디스플레이 (4K) 대응 확인
| 문제 | 원인 | 해결법 |
|---|---|---|
| 🖤 검은 화면 캡처 | 하드웨어 가속 앱 (게임, 영상) | DirectX 캡처 방식 사용 또는 Windows.Graphics.Capture API |
| ⚡ FPS 불안정 | GIL, 단일 스레드 병목 | 멀티스레딩, 프레임 큐 구현 |
| 🔊 오디오 싱크 불일치 | 캡처/녹음 시작 시간 차이 | 동시 시작, FFmpeg -shortest 옵션 |
| 💾 파일 크기 과다 | 무압축 또는 낮은 CRF | CRF 값 조정, 적절한 코덱 선택 |
| 🖥️ 다중 모니터 오류 | 모니터 인덱스 혼동 | mss.monitors 배열 확인 (0=전체, 1~=개별) |
| 🔒 권한 오류 (macOS) | 화면 녹화 권한 미허용 | 시스템 환경설정 → 보안 → 화면 녹화 허용 |
| 📱 고DPI 좌표 오류 | DPI 스케일링 미적용 | DPI 인식 설정 또는 스케일 팩터 적용 |
# Windows 고DPI 대응 코드
import ctypes
def set_dpi_awareness():
"""Windows에서 DPI 인식 설정"""
try:
# Windows 8.1 이상: Per-Monitor DPI Awareness
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except AttributeError:
try:
# Windows Vista 이상: System DPI Awareness
ctypes.windll.user32.SetProcessDPIAware()
except AttributeError:
pass
# 프로그램 시작 시 호출
set_dpi_awareness()
# DPI 스케일 팩터 가져오기
def get_dpi_scale():
"""현재 DPI 스케일 팩터 반환"""
hdc = ctypes.windll.user32.GetDC(0)
dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, 88) # LOGPIXELSX
ctypes.windll.user32.ReleaseDC(0, hdc)
return dpi / 96.0 # 96 DPI = 100% 스케일
scale = get_dpi_scale()
print(f"현재 DPI 스케일: {scale:.1f}x ({scale*100:.0f}%)")
직접 처음부터 만들기 어렵다면 오픈소스 프로젝트를 참고하거나 기여해보는 것도 좋은 방법이야! 아래 프로젝트들은 실제로 많이 쓰이고 코드 품질도 높아서 공부하기 딱이야.
C++ Qt FFmpeg 크로스플랫폼
Python 크로스플랫폼 고성능
C++ Qt Linux
C# Windows .NET
1️⃣ 타인의 화면을 동의 없이 캡처하는 것은 불법일 수 있어
2️⃣ DRM 보호된 콘텐츠(넷플릭스, 왓챠 등) 캡처는 저작권법 위반
3️⃣ 업무용 프로그램에서 화면 캡처 기능을 사용할 때는 회사 정책 확인 필요
4️⃣ 개인정보가 포함된 화면 캡처 시 개인정보보호법 준수 필요
mss + Pillow 조합이 가장 빠르고 간단해2. Python 화면 녹화:
mss + OpenCV 또는 FFmpeg 파이프 방식 추천3. C# 화면 캡처: GDI+ 방식이 기본, 고성능은 DirectX/Windows.Graphics.Capture
4. 오디오 동시 녹음:
PyAudio로 별도 녹음 후 FFmpeg로 합치기5. 웹 기반:
getDisplayMedia() API + MediaRecorder로 브라우저에서 구현6. 성능 최적화: 멀티스레딩 + GPU 가속 + 프레임 큐가 핵심
7. 배포: PyInstaller로 단일 exe 파일 생성
이 글에서 다룬 소스코드들은 모두 실제로 동작하는 코드야. 각자의 프로젝트에 맞게 수정해서 사용해봐! 화면 캡처/녹화 프로그램은 한번 제대로 만들어두면 정말 다양한 곳에 활용할 수 있어. 유튜브 강의 제작, 버그 리포트, 게임 클립 저장, 원격 지원 등등... 가능성은 무한대야! 🚀
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.