一行命令实现录屏,支持热键和鼠标操作,区域帧率格式任你选择

Posted 天元浪子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一行命令实现录屏,支持热键和鼠标操作,区域帧率格式任你选择相关的知识,希望对你有一定的参考价值。

       市面上的录屏工具软件有很多,基本都是窗口程序。毕竟,离开GUI的支持,设置参数、选择录像区域等操作都会变得非常困难。不过,窗口程序也并非无往不胜,即便是屏幕录像这样交互频繁的应用,控制台程序也同样可以大显身手,甚至比窗口程序的效率更高、操作更便捷。

       今天,我带给同学们的是一款命令行模式的录屏软件,可将屏幕指定区域的内容录制成GIF动画文件或MP4、AVI、WMV等格式的视频文件,录像区域、格式、帧率等参数,既可以由命令行传入,也可以通过鼠标和热键来调整。虽然只是实现了录屏功能,却涉及了以下诸多知识点:

  1. 使用pynput模块的keyboard和mouse侦听键盘和鼠标,实现热键机制和鼠标选取
  2. 使用pywin32模块的win32api、win32gui和win32con捕捉当前窗口句柄,实现窗口的隐藏和显示
  3. 使用pillow模块的ImageGrab实现屏幕截图
  4. 使用imageio模块生成GIF或MP4文件
  5. 使用Python标准模块optparse构造linux风格的使用界面,遵循GNU/POSIX语法规则设置参数选项
  6. 使用批处理命令编写批处理文件,最终生成桌面快捷方式

       除了上述知识点外,这款屏幕录像机还用到了定时器、线程、队列等技术,以及生产者-消费者模式,几乎就是一个Python技术博览馆。

1. 监听键盘和鼠标

       尽管pywin32也可以监听键盘和鼠标,但我选择是的pynput模块,因为它实在是太好用了,还可以跨平台。除了监听,pynput模块也可以用来操控键盘和鼠标。pynput模块的安装很简单,直接使用pip安装即可。

pip install pynput

       pynput模块提供了keyboard和mouse两个类用于监听键盘和鼠标,实例化时只需要提供相应的事件函数即可。通过下面的简单例子,新手也很容易掌握pynput的使用要点。友情提示:不要在运行这段代码的命令行窗口中测试鼠标左键,因为点击左键会影响程序执行,导致反应迟滞。

from pynput import keyboard, mouse

def on_click(x, y, button, pressed):
    """鼠标按键"""
    
    action = '按下' if pressed else '弹起'
    if button == mouse.Button.left:
        print('左键%s,(%d,%d)'%(action, x, y))
        
    elif button == mouse.Button.right:
        print('右键%s,(%d,%d)'%(action, x, y))

def on_press(key):
    """键按下"""
    
    if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
        print('Ctr键按下')

def on_release(key):
    """键弹起"""
    
    if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
        print('Ctr键弹起')
    elif key == keyboard.Key.esc:
        print('再见')
        return False

monitor_m = mouse.Listener(on_click=on_click)
monitor_m.start()

monitor_k = keyboard.Listener(on_press=on_press, on_release=on_release)
monitor_k.start()
monitor_k.join()

2. 隐藏或显示控制台窗口

       作为录屏软件,录制时需要将自身窗口隐藏,而开始录制前或录制结束后又需要显示自身窗口。虽然最小化、最大化窗口也可以满足使用要求,但我选择使用pywin32来隐藏或显示控制台窗口。pywin32模块的安装命令如下:

pip install pypiwin32

       下面的3行语句分别实现获取当前窗口句柄、隐藏和显示窗口句柄指定的窗口。

import win32gui, win32api, win32con

hwnd = win32gui.GetForegroundWindow() # 获取最前窗口句柄
win32gui.ShowWindow(hwnd, win32con.SW_HIDE) # 隐藏hwnd指定的窗口
win32gui.ShowWindow(hwnd, win32con.SW_SHOW) # 显示hwnd指定的窗口

3. 屏幕截图

       无所不能的pywin32也可以截屏,据说速度很快。为此我专门测试了全屏幕(1902x1080)截取,发现pywin32和pillow的ImageGrab在速度上堪堪打了个平手,但pywin32截图需要从构建DC开始,大约十几行代码,而ImageGrab只需要一行代码。既然代码简洁,比速度也不逊色,还有什么理由不选择ImageGrab呢?

       ImageGrab子模块提供了一个截屏的函数grab,返回一个PIL图像对象。grab函数接受一个四元组参数用以指定截图区域的左上角和右下角在屏幕上的坐标,若省略参数,grab函数将截取整个屏幕。

from PIL import ImageGrab

im = ImageGrab.grab((1200,600,1920,1080)) # 截取大小为720×480的屏幕区域
im.show()
im = ImageGrab.grab() # 截取整个屏幕
im.show()

       虽然代码中看起来ImageGrab是从PIL模块导入的,但实际上ImageGrab来自pillow模块,这是由于版本历史的原因造成的困惑。如果你还没有安装pillow模块,请使用如下的命令安装:

pip install pillow

4. 生成动画或视频文件

       Python图像库有很多,imageio是后起之秀,也是其中的佼佼者,尤其在动画和视频领域,更是独领风骚。在imageio诞生之前,生成GIF动画需要几百行代码,而且因为依赖库升级频繁,几乎每隔一段时间就需要重写一次。现在有了imageio,一切都变得云淡风轻了。安装imageio时,请一并安装imageio-ffmpeg,这是imageio生成视频文件的依赖库。

pip install imageio
pip install imageio-ffmpeg

       imageio提供了两种生成动画或视频文件的方法:imageio.mimsave函数和imageio.get_writer函数。imageio.mimsave函数接收一个由PIL对象组成的列表作为参数,生成文件前需要将每一帧图像转成PIL对象并存入列表——这意味着生成文件必须在最后一帧图像捕捉完成之后才能开始。

# imageio.mimsave(out_file, pil_list, format='GIF', fps=fps, loop=loop)
# out_file      - 输出文件名
# pil_list      - 列表,元素类型为PIL对象
# format        - 输出格式
# fps           - 帧率(每秒播放的帧数)
# loop          - 循环次数,0表示无限循环(视频格式不支持该参数)

       imageio.get_writer函数类似于open函数,返回了一个文件对象,该对象提供append_data方法,可以将单帧的PIL对象对写入输出文件。写入完成后,不要忘记使用close方法关闭文件对象。使用imageio.get_writer函数生成动画或视频文件,可以很好得支持生产者-消费者模式,捕捉一帧写入一帧,停止录屏后文件即告生成。

# writer = imageio.get_writer(out_file, fps=fps) # gif格式可增加loop参数
# writer.append_data(im_pil) # im_pil为PIL对象
# writer.close()

5. 定时器

       录屏依赖于精准的定时器,遗憾的是Python并没有提供一个像样的定时器,因此只能自己写一个了。关于定时器的介绍,请参考我昨天写的博文《无所不能的Python竟然没有一个像样的定时器?试试这个!》,这里就不再赘述了。

6. 完整代码

       源文件名为ScreenRecorder.py,全部代码不足300行,代码中用到的各个模块和技术要点都已介绍过了,关键之处均有注释。

# -*- coding:utf-8 -*-

import os, time
import optparse
import threading
import imageio
import queue
import numpy as np
from PIL import Image, ImageGrab
import win32gui, win32api, win32con
from pynput import keyboard, mouse

class PyTimer:
    """定时器类"""
    
    def __init__(self, func, *args, **kwargs):
        """构造函数"""
        
        self.func = func
        self.args = args
        self.kwargs = kwargs
        
        self.running = False
    
    def _run_func(self):
        """运行定时事件函数"""
        
        th = threading.Thread(target=self.func, args=self.args, kwargs=self.kwargs)
        th.setDaemon(True)
        th.start()
    
    def _start(self, interval, once):
        """启动定时器的线程函数"""
        
        if interval < 0.010:
            interval = 0.010
        
        if interval < 0.050:
            dt = interval/10
        else:
            dt = 0.005
        
        if once:
            deadline = time.time() + interval
            while time.time() < deadline:
                time.sleep(dt)
            
            # 定时时间到,调用定时事件函数
            self._run_func()
        else:
            self.running = True
            deadline = time.time() + interval
            while self.running:
                while time.time() < deadline:
                    time.sleep(dt)
                
                deadline += interval # 更新下一次定时时间
                if self.running: # 定时时间到,调用定时事件函数
                    self._run_func()
    
    def start(self, interval, once=False):
        """启动定时器
        
        interval    - 定时间隔,浮点型,以秒为单位,最高精度10毫秒
        once        - 是否仅启动一次,默认是连续的
        """
        
        th = threading.Thread(target=self._start, args=(interval, once))
        th.setDaemon(True)
        th.start()
    
    def stop(self):
        """停止定时器"""
        
        self.running = False

class ScreenRecorder:
    """屏幕记录器"""
    
    def __init__(self, out, fps=10, nfs=1000, loop=0):
        """构造函数"""
        
        self.format = ('.gif', '.mp4', '.avi', '.wmv')
        
        ext = os.path.splitext(out)[1].lower()
        if not ext in self.format:
            raise ValueError('不支持的文件格式:%s'%ext)
        
        self.out = out
        self.ext = ext
        self.fps = fps
        self.nfs = nfs
        self.loop = loop
        
        self.cw = win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
        self.ch = win32api.GetSystemMetrics(win32con.SM_CYSCREEN)
        self.set_box((0, 0, self.cw, self.ch))
        
        self.ctr_is_pressed = False
        self.hidding = False
        self.recording = False
        self.pos_click = (0,0)
        self.q = None
        
        self.hwnd = self._find_self()
        self.info = None
        self.help()
        self.status()
    
    def _find_self(self):
        """找到当前Python解释器的窗口句柄"""
        
        return win32gui.GetForegroundWindow() # 获取最前窗口句柄
    
    def set_box(self, box):
        """设置记录区域"""
        
        x0, y0, x1, y1 = box
        dx, dy = (x1-x0)%16, (y1-y0)%16
        dx0, dx1 = dx//2, dx-dx//2
        dy0, dy1 = dy//2, dy-dy//2
        
        self.box = (x0+dx0, y0+dy0, x1-dx1, y1-dy1)
    
    def help(self):
        """热键提示"""
        
        print('---------------------------------------------')
        print('Ctr + 回车键:隐藏/显示窗口')
        print('Ctr + 鼠标左键或右键拖拽:设置记录区域')
        print('Ctr + PageUp/PageDown:更改记录格式')
        print('Ctr + Up/Down:调整帧率')
        print('Ctr + 空格键:开始/停止记录')
        print('Esc:退出')
        print()
    
    def status(self):
        """当前状态"""
        
        if self.info:
            print('\\r%s'%(' '*len(self.info.encode('gbk')),), end='', flush=True)
        
        recording_text = '正在记录' if self.recording else '准备就绪'
        if self.ext == 'gif':
            loop_str = '循环%d次'%self.loop if self.loop > 0 else '循环'
        else:
            loop_str = '不循环'
        
        self.info = '\\r输出文件:%s | 帧率:%d | 区域:%s'%(self.out, self.fps, str(self.box))
        print(self.info, end='', flush=True)
    
    def start(self):
        """开始记录"""
        
        self.q = queue.Queue(100)
        self.timer = PyTimer(self.capture)
        self.timer.start(1/self.fps)
        
        th = threading.Thread(target=self.produce)
        th.setDaemon(True)
        th.start()
    
    def stop(self):
        """停止记录"""
        
        self.timer.stop()
    
    def capture(self):
        """截屏"""
        
        if not self.q.full():
            im = ImageGrab.grab(self.box)
            self.q.put(im)
    
    def produce(self):
        """生成动画或视频文件"""
        
        if self.ext == '.gif':
            writer = imageio.get_writer(self.out, fps=self.fps, loop=self.loop)
        else:
            writer = imageio.get_writer(self.out, fps=self.fps)
        
        n = 0
        while self.recording and n < self.nfs:
            if self.q.empty():
                time.sleep(0.01)
            else:
                im = np.array(self.q.get())
                writer.append_data(im)
                n += 1
        
        writer.close()
    
    def on_press(self, key):
        """键按下"""
        
        if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
            self.ctr_is_pressed = True
    
    def on_release(self, key):
        """键释放"""
        
        if key == keyboard.Key.ctrl_l or key == keyboard.Key.ctrl_r:
            self.ctr_is_pressed = False
        elif key == keyboard.Key.space and self.ctr_is_pressed:
            if self.recording: # 停止记录
                self.stop()
                self.recording = False
                if self.hidding:
                    win32gui.ShowWindow(self.hwnd, win32con.SW_SHOW) # 显示窗口
                    self.hidding = False
            else: # 开始记录
                self.start()
                self.recording = True
                if not self.hidding:
                    win32gui.ShowWindow(self.hwnd, win32con.SW_HIDE) # 隐藏窗口
                    self.hidding = True
        elif key == keyboard.Key.enter and self.ctr_is_pressed:
            if self.hidding: # 显示窗口
                win32gui.ShowWindow(self.hwnd, win32con.SW_SHOW) # 显示窗口
                self.hidding = False
                self.status()
            else: # 隐藏窗口
                win32gui.ShowWindow(self.hwnd, win32con.SW_HIDE) # 隐藏窗口
                self.hidding = True
        elif (key == keyboard.Key.page_down or key == keyboard.Key.page_up) and self.ctr_is_pressed:
            i = self.format.index(self.ext)
            if key == keyboard.Key.page_down:
                self.ext = self.format[(i+1)%len(self.format)]
            else:
                self.ext = self.format[(i-1)%len(self.format)]
            
            folder = os.path.split(self.out)[0]
            dt_str = time.strftime('%Y%m%d%H%M%S')
            self.out = os.path.join(folder, '%s%s'%(dt_str, self.ext))
            self.status()
        elif key == keyboard.Key.left and self.ctr_is_pressed:
            if self.fps > 1:
                self.fps -= 1
                self.status()
        elif key == keyboard.Key.right and self.ctr_is_pressed:
            if self.fps < 40:
                self.fps += 1
                self.status()
        elif key == keyboard.Key.esc:
            print('\\n程序已结束')
            return False
    
    def on_click(self, x, y, button, pressed):
        """鼠标按键"""
        
        if (button == mouse.Button.left or button == mouse.Button.right) and self.ctr_is_pressed:
            if pressed:
                self.pos_click = (x, y)
            elif self.pos_click != (x, y):
                x0, y0 = self.pos_click
                self.set_box((min(x0,x), min(y0,y), max(x0,x), max(y0,y)))
                self.status()

def parse_args():
    """获取参数"""

    parser = optparse.OptionParser()
    
    parser.add_option('-o', '--out', action='store', type='string', dest='out', default='', help='输出文件名')
    parser.add_option('-f', '--fps', action='store', type='int', dest='fps', default='10', help='帧率')
    parser.add_option('-n', '--nfs', action='store', type='int', dest='nfs', default='1000', help='最大帧数')
    parser.add_option('-l', '--loop', action='store', type=以上是关于一行命令实现录屏,支持热键和鼠标操作,区域帧率格式任你选择的主要内容,如果未能解决你的问题,请参考以下文章

ffmpeg录屏/录音/录摄像头----命令行实现

Mac上一款优秀录屏软件:Screenium 3 支持m1芯片

快速核对两个表格数据

在 MacOS 中使用鼠标和热键移动窗口 [关闭]

Qt音视频开发43-采集屏幕桌面并推流(支持分辨率/矩形区域/帧率等设置/实时性极高)

FFmpeg mac录屏