record-camera-and-screen/RecordVideo.py

422 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import datetime,time,sys,os,signal,re,winreg
from datetime import datetime
import subprocess,threading
from subprocess import CalledProcessError
# from multiprocessing import Process
from threading import Thread
import ctypes,inspect
import RecordType
from RecordType import *
import RecordConfig
from RecordConfig import *
import logging
import RunCMD
from RunCMD import get_ffmpeg_path
from winreg import HKEY_CURRENT_USER, OpenKey, QueryInfoKey, EnumValue, SetValueEx, CloseKey, REG_SZ, KEY_READ, KEY_SET_VALUE
class RecordVideo():
'''
ffmpeg -f dshow -i video="@device_pnp_\\\\?\\usb#vid_04f2&pid_b354&mi_00#7&30d7ad30&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global":audio="@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{571529B3-7DB3-42A3-ADEF-BBD82925C15D}" -acodec libmp3lame -vcodec libx265 -preset:v ultrafast -tune:v zerolatency -s 1920x1080 -r 7 -y record_camera_20180408_182541.mkv
'''
def __init__(self, record_video=True, record_voice=True):
# print('视频录制初始化中...')
# self.record_video=record_video
# self.record_voice=record_voice
#录制状态
self.recording = False
self.exception_exit = False
self.record_type=RecordType.Camera
#文件名称
self.file_name='record'
#文件后缀
self.file_suffix='.mkv'
self.process = None
self.record_thread_name='record'
self.record_thread=None
self.file_dir = ''
self.load()
def load(self):
#日志
self.logger = logging.getLogger(__name__)
self.logger.setLevel(level = logging.INFO)
handler = logging.FileHandler('log.txt')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.load_config()
def load_config(self):
rc = RecordConfig()
self.config = rc.config
#摄像头名称
self.camera_name=rc.config.get('devices','camera_device_name')
#麦克风名称
self.voice_device_name=rc.config.get('devices','voice_device_name')
#录制屏幕名称
self.screen_name=rc.config.get('devices','screen_device_name')
#系统声音设备名称
self.system_voice_device_name=rc.config.get('devices','system_voice_device_name')
#视频编码
self.video_codec=rc.config.get('record','vcodec')
#分辨率
self.resolution=rc.config.get('record','resolution')
#是否自适应屏幕录制分辨率
self.adaptive_screen_resolution = rc.config.getboolean('record', 'adaptive_screen_resolution')
#帧率
self.brate=rc.config.getfloat('record','frame_rate')
#文件目录
self.file_dir= os.path.abspath(rc.config.get('record','file_dir'))
#线程数
self.threads = rc.config.getint('record','threads')
self.logger.info('camera device name: %s' % self.camera_name)
self.logger.info('voice device name: %s' % self.voice_device_name)
self.logger.info('screen device name: %s' % self.screen_name)
self.logger.info('system voice device name: %s' % self.system_voice_device_name)
self.logger.info('vcodec: %s' % self.video_codec)
self.logger.info('resolution: %s' % self.resolution)
self.logger.info('frame rate: %s' % self.brate)
self.logger.info('save dir: %s' % self.file_dir)
def start_ffmpeg(self,cmd, shell = True):
try:
print('录制中...')
self.logger.info('录制中...')
# print('cmd:\n%s' % cmd)
start_time = datetime.now()
self.process=subprocess.Popen(cmd, shell=shell, universal_newlines = True, stdin = subprocess.PIPE, stderr = subprocess.STDOUT, stdout = subprocess.PIPE)
line = ''
while self.recording:
# print(cmd)
# print(self.recording)
# tmp_out = self.process.stdout.readline()
line += str(self.process.stdout.readline())
# print('test tmp out:%s' % tmp_out)
#文字输出编码错误记录
#异常UnicodeDecodeError: 'gbk' codec can't decode byte 0xb4 in position 2881: illegal multibyte sequence
#原因cmd输出包含中文字符
#解决方案universal_newlines = False
#缺陷需要以字节形式的q来控制退出:write(b'q')
#最终原因及解决方案引起gbk编码错误的原因是文件名的中文与数字的连接符号由下划线'_'改成了横杠'-'。
#为什么这个修改会引起运行时ffmpeg报编码错误推测终究还是ffmpeg对中文编码的支持问题。
#最终方案即文件名中的中文后的连接符号改回下划线。
now = datetime.now()
if (now - start_time).total_seconds() >2:
# self.logger.info('recording...')
ffmpeg_running = False
if self.process:
ffmpeg_running = self.process.poll() is None
log_text = 'ffmpeg 运行状态:%s' % ('运行中' if ffmpeg_running else '终止')
print(log_text)
self.logger.info(log_text)
else:
txt= 'ffmpeg子进程已终止.'
print(txt)
self.logger.warning(txt)
if not ffmpeg_running:
print(self.process.communicate())
raise CalledProcessError(self.process.returncode, cmd)
# self.logger.info(line)
print(line)
line = ''
start_time = now
# print(line)
if not self.recording:
self.process.stdin.write('q')
print(self.process.communicate())
break
except CalledProcessError as e:
log_txt = 'ffmpeg异常终止:\nreturn code: %d\ncmd:\n%s' % (e.returncode, e.cmd)
print(log_txt)
self.logger.warning(log_txt)
self.recording = False
self.exception_exit = True
print('process is None?:%s' % (self.process is None))
# self.stop_record()
except Exception as x:
print('未捕获的异常:')
print(x)
# self.logger.info(self.process.communicate())
# print('over')
def record(self, cmd='ffmpeg -h', target = None):
if target:
cmd = os.path.join(get_ffmpeg_path(), cmd)
print('cmd: \n%s' % cmd)
self.logger.info('record cmd:\n %s' % cmd)
self.record_thread = Thread(name=self.record_thread_name, target= target, args = (cmd,), daemon=True)
self.record_thread.start()
self.recording=True
self.exception_exit = False
print('record thread,ident:%d' % self.record_thread.ident)
# th.join()
def stop_record(self):
# print('threading active thread count:%d' % threading.active_count())
try:
self.recording = False
self.logger.info('录制将停止...')
if self.process:
self.logger.info('ffmpeg进程状态: %s' % (self.process.poll() is not None))
if self.process.returncode:
print('subprocess return code:%d' % self.process.returncode)
print('record thread status: %s' % self.record_thread.is_alive())
if self.record_thread.is_alive():
self.record_thread.join(1)
print('record thread status: %s' % self.record_thread.is_alive())
except (Exception,KeyboardInterrupt) as e:
print('kill exception:\n %s' % e)
self.logger.warning('kill exception:\n %s' % e)
def record_camera(self):
if self.camera_name and self.voice_device_name:
self.record_type=RecordType.Camera
record_cmd='ffmpeg -f dshow -i video=\"%s\":audio=\"%s\" -acodec libmp3lame -vcodec %s -preset:v ultrafast -tune:v zerolatency -s %s -r %d -threads %d -y %s' %(
self.deal_with_device_name(self.camera_name),
self.deal_with_device_name(self.voice_device_name),
self.video_codec,
self.resolution,
self.brate,
self.threads,
self.get_file_name()
)
# print(record_cmd)
self.record(record_cmd, self.start_ffmpeg)
def get_screen_device(self):
pass
def record_screen(self, resolution='1024x768'):
if self.screen_name and self.system_voice_device_name:
self.record_type=RecordType.Screen
if self.adaptive_screen_resolution is not True:
resolution = self.resolution
device_cmd_str = ''
if self.screen_name.lower().find('gdigrab') >=0:
#使用gdigrab录制屏幕
device_cmd_str = '-f dshow -i audio="{}" -f gdigrab -i desktop'.format(self.system_voice_device_name)
else:
device_cmd_str = '-f dshow -i video="{}":audio="{}"'.format(self.screen_name, self.system_voice_device_name)
record_cmd='ffmpeg {} -acodec libmp3lame -vcodec {} -preset:v ultrafast -tune:v zerolatency -s {} -r {} -threads {} -y {}'.format(
device_cmd_str,
self.video_codec,
# '1024x768', #屏幕录制分辨率固定
resolution,
self.brate,
self.threads,
self.get_file_name()
)
self.record(record_cmd, self.start_ffmpeg)
def check_device(self):
#简单验证摄像头设置是否为空
ready = True
l_msg = ''
if not self.camera_name:
ready = False
l_msg += '摄像头设备为空\n'
if not self.voice_device_name:
ready = False
l_msg += '麦克风设备为空\n'
if not self.screen_name:
ready = False
l_msg += '屏幕录制驱动为空\n'
if not self.system_voice_device_name:
ready = False
l_msg += '系统声音录制驱动为空\n'
if ready:
l_msg = '设备检测正常'
print(l_msg)
self.logger.info(l_msg)
return ready
def check_run_state(self):
#验证运行有效性逻辑:
#一、判断是否正常安装
#判断条件软件安装时在注册表“HKEY_CURRENT_USER\\SOFTWARE\\Gutin\\Record“记录下安装目录
#二、非正常安装有效时间为三个月,且只能发生一次
#验证当前运行目录是否存在注册表中
qualified = False
run_dir = os.path.abspath('.')
# print('run_dir:%s' % run_dir)
reg_path = 'SOFTWARE\\Gutin\\Record'
feature_name ='InstallDir'
key = OpenKey(HKEY_CURRENT_USER, reg_path, access = KEY_READ)
items = QueryInfoKey(key)
for i in range(items[1]):
item = EnumValue(key, i)
name = item[0]
value = item[1]
type = item[2]
if name and name.find(feature_name)>=0:
if os.path.samefile(value, run_dir):
qualified = True
break;
if not qualified:
#查找非正常安装目录记录
#记录以运行目录的hash值作为键名值为首次运行的时间
time_format= '%Y-%m-%d %H:%M:%S'
run_time = None
unqualified_key_name = 'Unqualified'
has_unqualified = False
for i in range(items[1]):
item = EnumValue(key, i)
name = item[0]
value = item[1]
type = item[2]
if name == unqualified_key_name:
has_unqualified = True
run_time = value
if not has_unqualified:
#如果不存在,创建
run_time = datetime.now().strftime(time_format)
key = OpenKey(HKEY_CURRENT_USER, reg_path, access = KEY_SET_VALUE)
SetValueEx(key, unqualified_key_name, 0, REG_SZ, run_time)
#判断时限
# now = datetime.strptime('2018-06-22 12:11:51', time_format)
now = datetime.now()
run_time_obj = datetime.strptime(run_time, time_format)
print('first run_time:%s' % run_time)
print('now:%s' % now.strftime(time_format))
qual_days = 91 - (now - run_time_obj).days
# print('qualified days:%d' % (qual_days))
# qual_hours = (now - run_time_obj).total_seconds() // 3600
if qual_days > 0:
# if 5 - qual_hours > 0:
qualified = True
CloseKey(key)
return qualified
def debug_camera(self):
try:
play_cmd = ['ffplay','-f','dshow','-i','video={}'.format(self.camera_name),'-window_title','按q退出','-noborder']
self.record(play_cmd, self.play)
except Exception as e:
print(e)
def play(self, cmd):
try:
t_process=subprocess.Popen(cmd, shell= False, universal_newlines = True, stderr = subprocess.STDOUT, stdout = subprocess.PIPE)
while True:
line = t_process.stdout.readline()
print(line)
if line == '':
if t_process.poll() is not None:
break
t_process.communicate()
except (Exception, KeyboardInterrupt) as e:
print(e)
def deal_with_device_name(self,device_name):
# print(device_name)
# new_name=device_name.replace('\\','\\\\')
# print(new_name)
# return new_name
return device_name
def get_file_name(self):
date_dir = datetime.now().strftime('%Y-%m-%d')
time_str = datetime.now().strftime('%Y-%m-%d-%H%M%S')
video_type_name = ''
if self.record_type == RecordType.Camera:
# video_type_name = 'camera'
video_type_name = '摄像头'
if self.record_type == RecordType.Screen:
video_type_name = '屏幕'
# video_type_name = 'screen'
today_file_dir = os.path.join(self.file_dir, date_dir)
if not os.path.exists(today_file_dir):
os.mkdir(today_file_dir)
file_name = os.path.join(today_file_dir, '{}_{}{}'.format(video_type_name, time_str, self.file_suffix))
print('recording file name: %s' % file_name)
return file_name
def _async_raise(self, tid, exctype):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
print('async_raise res value:%d' % res)
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def kill_process(self, process_name='ffmpeg'):
cmd='tasklist | findstr {}'.format(process_name)
output_strs, output_errs=self.run_cmd(cmd)
pid=[]
if output_strs:
print('find "%s" result: \n%s' % (process_name, ''.join(output_strs)))
for output_str in output_strs:
find_re=re.search(r'({}.+?)\s*([0-9]+)'.format(process_name),output_str)
if find_re:
full_process_name=find_re.group(1).strip()
pid=find_re.group(2).strip()
print('计划结束任务:{}@pid {}'.format( full_process_name, pid ))
task_kill_cmd = 'taskkill /T /F /pid {}'.format(pid)
# status, output = subprocess.getstatusoutput(task_kill_cmd)
# if status == 1:
# print('任务成功被结束:')
# else:
# print('任务结束失败:')
# print(output)
else:
print('not found task about "%s" in tasklist' % process_name )
# def stop_thread(self,thread):
# self._async_raise(thread.ident, SystemExit)