1

Тема: Вирівнювання часу запису відео з екрана

З python я не дуже знайомий, тільки почав потроху вивчати.
З'явилась потреба в програмі, яка буде записувати відео з конкретного вікна.
Спочатку була думка робити це через OBS, але OBS сильно навантажує систему (ноут слабенький), тому почав шукати рішення на python.
Знайшов відео, і почав робити.
В принципі все зробив, але є нюанси.
На моєму ноуті нормально працює тільки fps 25, якщо ж вище - тоді вихідне відео пришвидшене.
На тому ноуті, де це повинно працювати взагалі доводиться ставити 10 fps, щоб вихідне відео було нормальної швидкості, хоча той ноут набагато слабкіший.
Вікно програми виводиться на tkinter, наче було все нормально, але з'явився баг - після запуску програми її вікно замість 300x130 має розмір 375x200, а після старту запису приймає потрібний розмір.
Власне потрібна допомога в нормалізації швидкості вихідного відео незалежно від вказаного FPS, ChatGPT за добу страждань не зміг у цьому допомогти *WALL*

Буду дуже вдячний за поради й пояснення по коду програми, й допомогу з вирішенням проблем! :)

Код програми:

Прихований текст
from tkinter import*
from datetime import datetime, timedelta
import cv2
import numpy as np
import mss
import pygetwindow as gw
import psutil
from playsound3 import *
import threading
import os
import time

screen = Tk()

temp = 0
after_id = ''
is_recording = False
out = None
recording_thread = None
cont = False
total_time = 0
wtf = None
wtf_open = False

def vars(filename):
    variables = {}

    if not os.path.isfile(filename):
        print("Файл не знайдено.")
        return variables

    try:
        with open(filename, 'r') as file:
            for line in file:
                line = line.strip()
                if '=' in line:
                    key, value = line.split('=', 1)
                    key = key.strip()
                    value = value.strip()
                    try:
                        value = int(value)
                    except ValueError:
                        try:
                            value = float(value)
                        except ValueError:
                            pass
                    variables[key] = value
    except IOError:
        print("Помилка вводу/виводу.")

    return variables

variables = vars('scr.conf')

proc_name = variables.get('proc_name')
win_name = variables.get('win_name')
fps = variables.get('fps')
rs = variables.get('rs')
scale = variables.get('scale')
max_time = variables.get('max_time')

err1 = variables.get('err1')
err2 = variables.get('err2')
greeting = variables.get('greeting')
shelp = variables.get('shelp')

def calculate_bitrate(fps, target_bitrate=116803):
    base_fps = 25
    if fps <= 0:
        raise ValueError("FPS має бути більше нуля")
    
    bitrate = target_bitrate * (fps / base_fps)
    
    return int(bitrate)

def tick():
    global temp, after_id
    after_id = screen.after(1000, tick)
    f_temp = datetime.fromtimestamp(temp).strftime('%M:%S')
    timer.configure(text=str(f_temp))
    temp += 1

def check_proc(proc_name):
    res = ''
    for proc in psutil.process_iter():
        if proc.name() == proc_name:
            res = 'ok'
            break        
    if (res == 'ok'):
        return True
    else:
        return False

def check_wn(name):
    all_windows = gw.getAllWindows()
    
    for window in all_windows:
        if window.title == name:
            return window
    return None

def rec(state, pn, wn):
    global is_recording, out, cont, total_time
    if (state == 'start' and not is_recording):
        if (check_proc(pn) == False):
            text.configure(text=err1, bg='#d95961')
            playsound('media/error.mp3')
        elif (check_wn(wn) == False):
            text.configure(text=err2, bg='#d95961')
            playsound('media/error.mp3')
        else:
            is_recording = True
            fourcc = cv2.VideoWriter_fourcc(*'XVID')
            now = datetime.now()
            dt_string = now.strftime("%d-%m-%Y_%H-%M-%S")

            w = gw.getWindowsWithTitle(wn)[0]
            if w.isMinimized:
                w.restore()
            w.activate()
            
            new_size = (w.width*scale, w.height*scale)
            out = cv2.VideoWriter('videos/'+wn+'_'+dt_string+'.avi', fourcc, fps, new_size)

            bitrate = calculate_bitrate(fps)
            try:
                out.set(cv2.CAP_PROP_BITRATE, bitrate)
            except Exception as e:
                print("Не вдалося встановити бітрейт через OpenCV. Ігноруємо помилку:", e)

            if (cont == False):
                tick()
                text.configure(text='Запис розпочато', bg='#72e665')
                print('Запис розпочато')
                playsound('media/ok.mp3')
            else:
                print('Відео збережено, запис продовжується')

            elapsed_time = 0
            start_time = datetime.now()
            
            sct = mss.mss()

            while is_recording:                
                current_time = datetime.now()
                elapsed_time = (current_time - start_time).total_seconds()
                monitor = {"top": w.top, "left": w.left, "width": w.width, "height": w.height}
                img = sct.grab(monitor)
                frame = np.array(img)
                frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR)
                frame = cv2.resize(frame, new_size, interpolation=cv2.INTER_LANCZOS4)
                out.write(frame)

                if (total_time >= max_time):
                    click('stop')
                elif (elapsed_time >= rs+1):
                    total_time += elapsed_time

                    out.release()
                    is_recording = False
                    cont = True
                    threading.Thread(target=rec, args=('start', proc_name, win_name)).start()

            if (cont == False):
                text.configure(text='Запис закінчено', bg='#e67065')
                print('Запис зупинено')
                if (out is not None):
                    out.release()
                    cv2.destroyAllWindows()
                    is_recording = False

def func_wtf():
    global wtf_open, wtf
    if (wtf_open == False):
        wtf = Toplevel(screen)
        wtf.title(shelp)
        wtf.geometry('300x200')
        wtf.iconbitmap('media/icon.ico')
        wtf.resizable(width=False, height=False)
        wtf_text = 'Це програма\nдля відеозапису\nвікна виводу зображення\nз пульта!\n'
        wtf_text += 'Шо тут не зрозуміло?'
        wtext = Label(wtf,
                    bg='#cfd7e6',
                    text=wtf_text
                    )
        wtext.place(x=10, y=10, width=280, height=180)        
        wtf_open = True
    else:
        wtf.lift()
        wtf.focus_force()

def click(args):
    if args == 'start':
        threading.Thread(target=rec, args=('start', proc_name, win_name)).start()
    if args == 'stop':
        global is_recording, cont, total_time
        is_recording = False
        cont = False
        total_time = 0

        if recording_thread is not None:
            recording_thread.join()
        
        screen.after_cancel(after_id)
        global temp
        temp = 0
        timer.configure(text='00:00')
    if args == 'wtf':
        func_wtf()

def wtf_on_closing():
    global wtf_open
    if wtf_open == True:
        wtf.destroy()
        wtf_open = False

def on_closing():
    if (wtf_open == True):
        wtf.destroy()
    screen.destroy()

screen.title('Відеозапис')
screen.geometry('300x130')
screen.resizable(width=False, height=False)
screen.config(bg='#cfd7e6')
screen.iconbitmap('media/icon.ico')

text = Label(screen,
             bg='#cfd7e6',
             text=greeting
             )
text.place(x=10, y=10, width=280, height=20)

rec_image = PhotoImage(file='media/rec1.png')
stop_image = PhotoImage(file='media/stop1.png')
start = Button(screen,
             image = rec_image,
             relief = RAISED,
             bd=1,
             cursor='hand2',
             command = lambda:click('start')
             )
start.place(x=10, y=40, width=85, height=40)

stop = Button(screen,
             image = stop_image,
             relief = RAISED,
             bd=1,
             cursor='hand2',
             command = lambda:click('stop')
             )
stop.place(x=105, y=40, width=85, height=40)

timer = Label(screen, bg='#cfd7e6', text = '00:00', font=(35))
timer.place(x=200, y=40, width=90, height=40)

wtf = Button(screen,
             text=shelp,
             relief = RAISED,
             bd=1,
             cursor='hand2',
             command = lambda:click('wtf')
             )
wtf.place(x=10, y=90, width=280, height=30)
if (wtf_open == True):
    wtf.protocol("WM_DELETE_WINDOW", wtf_on_closing)
screen.protocol("WM_DELETE_WINDOW", on_closing)
screen.mainloop()

Файл scr.conf

Прихований текст
proc_name=scrcpy.exe
win_name=DJI RC Pro
fps=60
rs=300
max_time=2100
scale=2
err1=Запусти SCRCPY!
err2=Підключи пульт до ноута!
greeting=Наче працює...
shelp=Шо це таке?