-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRecordVideo.py
422 lines (355 loc) · 17.3 KB
/
RecordVideo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
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)