提交 9b6802a3 authored 作者: zw.wang's avatar zw.wang

feat: [eviz] 萤石云摄像头移动侦测回放支持

上级 31ad1cc8
......@@ -75,6 +75,13 @@ DYNACONF_DINGTALK__SECRET = SECd5986591b53e959d5076b8d53be127b0046eddf1d76b3836b
'oss2'
```
### influxdb 使用保留策略 `one_week`
```
use intelab
CREATE RETENTION POLICY "one_week" ON "intelab" DURATION 168h REPLICATION 1
```
## 本地开发
配置本地配置文件`settings.local.toml`
......@@ -92,6 +99,19 @@ pip install -e .
- StreamRecorder
移动侦测事件取流录制模块,该模块支持多实例部署,用于消费上游EventMerger模块生产的移动侦测事件消息,根据事件的startTime和endTime向APIServer服务查询回放流地址,并进行录制,支持断点录制和视频合并。录制完成的视频将会上传到阿里云OSS,同时将链接写入Mysql。
## camera_ai_config.ai_config_support示例:`0101000000000000`,采用二进制数判断是否启用某一配置,1-11位分别表示
- 1.是否支持云存储,默认值1, 只有开启了云存储的才会启动录制服务
- 2.是否支持移动侦测区域绘制,默认值1
- 3.是否开启全天录制,默认值0,非开启全天录制的任务是6:00-23:00之间的任务,不建议开启全天录制
- 4.是否开启移动侦测,默认值1,开启移动侦测只会针对移动侦测的视频进行云存储,建议开启
- 5.是否摄像头移动检测,默认值0
- 6.是否摄像头遮挡检测,默认值0
- 7.是否支持人体检测,默认值0
- 8.是否支持人脸检测,默认值0
- 9.是否支持人脸口罩检测,默认值0
- 10-16位暂时为0,留作备用
## 部署
修订版本号`setup.py`
......
......@@ -80,15 +80,17 @@ def query(cursor_dict=False):
@query(cursor_dict=True)
def get_camera_info(cursor, conn, camera_code=None):
def get_camera_info(cursor, conn, camera_code=None, platform='isc'):
if camera_code:
_filter = 'where camera_info.device_code = "{}"'.format(camera_code)
else:
_filter = '''
where `is_valid` = 1
where camera_info.platform = "{}"
and camera_info.is_valid = 1
and biz_type is not null
and `point_index_code` is not null
'''
'''.format(platform)
if platform == 'isc':
_filter += ' and `point_index_code` is not null '
sql = '''
select
......@@ -102,9 +104,10 @@ def get_camera_info(cursor, conn, camera_code=None):
ai_config_support, device_code, service_type, biz_type,
region_path_name,
1 as video_plan_type,
network_quality
network_quality,
is_valid, platform, treaty, model, brand, cac.video_stream_url
from camera_info
join camera_ai_config cac
left join camera_ai_config cac
on camera_info.id = cac.camera_info_id
{}
order by create_time;
......@@ -119,24 +122,25 @@ def get_camera_info(cursor, conn, camera_code=None):
@query()
def insert_video_info(cursor, conn, db_table, device_code, start_time, end_time,
biz_type, service_type, status=0,
file_name=None, video_url=None, video_resolution=None):
file_name=None, video_url=None, video_resolution=None, recovered_time=None):
sql = '''
insert {} (
device_code, file_name, start_time, end_time,
video_url, video_resolution,
biz_type, service_type,
status, create_time, update_time)
status, create_time, update_time, expired_time, recovered_time)
value (%(device_code)s, %(file_name)s, %(start_time)s, %(end_time)s,
%(video_url)s, %(video_resolution)s,
%(biz_type)s, %(service_type)s,
%(status)s, now(), now()
%(status)s, now(), now(), date_add(now(),interval 31 day), %(recovered_time)s
)
'''.format(db_table)
cursor.execute(sql, {
'device_code': device_code, 'file_name': file_name,
'start_time': start_time, 'end_time': end_time,
'video_url': video_url, 'video_resolution': video_resolution,
'status': status, 'biz_type': biz_type, 'service_type': service_type
'status': status, 'biz_type': biz_type, 'service_type': service_type,
'recovered_time': recovered_time
})
video_id = cursor.lastrowid
conn.commit()
......
差异被折叠。
import time
from datetime import datetime, timedelta
from dynaconf import settings
from intelab_python_sdk.logger import log
from ils_common_video.db.mysql import get_camera_info
from ils_common_video.utils.eviz_client import EvizVersionClient
class PreEvent:
def __init__(self, start_time=None, end_time=None, ip_address=None):
self.timezone_shift = 8 - settings.get('TIMEZONE_SHIFT', 0)
cur_time = int(datetime.timestamp(datetime.utcnow() + timedelta(hours=self.timezone_shift)))
self.start_time = start_time or ((cur_time - int(settings.get('EVENT_START_HOUR', 3)) * 3600) * 1000)
self.end_time = end_time or ((cur_time - 10 * 60) * 1000)
log.debug('本次获取事件起始时间:%s, 结束时间: %s',
self._strftimestamp(self.start_time),
self._strftimestamp(self.end_time))
self.interval = settings.get('EVENT_INTERVAL', 1 * 60) # 合并事件的间隔时间 30s
self.ysy_client = EvizVersionClient()
self.ip_address = ip_address or settings.get('SERVICE_NAME', '')
def _strftimestamp(self, timestamp):
return time.strftime('%Y-%m-%d %H:%M:%S',
time.localtime(timestamp / 1000 - self.timezone_shift))
def get_alarm_list(self, token, camera_sn):
alarm_list = []
t1 = time.time()
alarms = self.ysy_client.get_alarm_list(
token, camera_sn, self.start_time, self.end_time)
while True:
if not alarms.get('data'):
break
else:
alarm_list.extend(alarms['data'])
size = alarms['page']['size']
if len(alarms['data']) < size:
break
# 获取分页的下一页
page = alarms['page']['page']
page = page + 1
alarms = self.ysy_client.get_alarm_list(
token, camera_sn, self.start_time, self.end_time,
page_start=page, page_size=size)
log.info('共获取到%s条移动报警信息,耗时%s', len(alarm_list), time.time() - t1)
return alarm_list
def merge_alarm_to_event(self, alarm_list, channel=1):
events_list = []
for alarm in alarm_list[::-1]:
# 莹石云时间单位是毫秒
alarm_time = int(alarm['alarmTime']) / 1000
alarm['start_time'] = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(alarm_time))
if alarm['channelNo'] != channel:
continue
# 如果当前告警信息距离上一个事件的结束时间小于80s
# 而且上一个事件的总时长小于30分钟
if len(events_list) > 0 \
and alarm_time - events_list[-1]['end_time'] < 80 \
and events_list[-1]['end_time'] - events_list[-1]['start_time'] < 30 * 60:
events_list[-1]['end_time'] = alarm_time + 80
continue
# 根据告警时间生成事件(前-15s,后+80s)
events_list.append({
'start_time': alarm_time - 15,
'end_time': alarm_time + 80,
})
log.info('合并间隔时间较近的事件后得到事件数%s' % len(events_list))
for event in events_list:
event['start_time'] = self._strftimestamp(event['start_time'] * 1000)
event['end_time'] = self._strftimestamp(event['end_time'] * 1000)
return events_list
def get_events(self, camera_sn):
log.info('处理摄像头%s', camera_sn)
token = EvizVersionClient.get_access_token(camera_sn)
camera = get_camera_info(camera_code=camera_sn)
camera = camera[0] if camera else None
# 使用移动告警接口获取事件
alarm_list = self.get_alarm_list(token, camera_sn)
channel = int(camera_sn.split(':', 1)[1]) if ':' in camera_sn else 1
return camera, self.merge_alarm_to_event(alarm_list, channel)
import time
import os
import pytz
from datetime import datetime, timedelta, time as dtime
from dynaconf import settings
from apscheduler.schedulers.blocking import BlockingScheduler
from intelab_python_sdk.logger import log
from intelab_python_sdk.ffmpeg.ffmpeg_record import FfmpegRecordThread
from ils_common_video.db import mysql, redis
from ils_common_video.db.influxdb import influxdb
from ils_common_video.utils.eviz_client import EvizVersionClient
# from intelab_video.ffmpeg.read_video_resolution import FfprobeThread
service_name = settings.get('SERVICE_NAME', '')
TIMEZONE = 'Asia/Shanghai'
tz = pytz.timezone(TIMEZONE)
class VideoRecord:
def __init__(self):
self.running = {}
self.ip_address = service_name
self.last_check_time = None
self.today = datetime.now(tz)
self.stop_time = datetime.combine(self.today.date(), dtime(settings.get('RECORD_STOP_HOUR', 22), 0), tz)
def create_thread(self, camera_sn, rtmp_url, video_file_path,
video_stream_type='rtmp', video_duration=60):
if video_stream_type == 'rtsp':
thread = FfmpegRecordThread(camera_sn, rtmp_url, video_file_path, video_duration,
rtsp_transport='tcp', stimeout=5000000)
else:
thread = FfmpegRecordThread(camera_sn, rtmp_url, video_file_path, video_duration)
thread.video_stream_type = video_stream_type
thread.daemon = True
thread.start()
self.running[camera_sn] = thread
@staticmethod
def update_rtmp_stream(camera_sn, stream_url, is_hd=False):
ysy_client = EvizVersionClient()
token = ysy_client.get_access_token(camera_sn)
if not token: # 设备未获取到有效token
return stream_url
res = ysy_client.get_live_address(token, camera_sn)
if res['code'] == '200':
data = res['data'][0]
if data['ret'] == '200':
if settings.get('VIDEO_RESOLUTION') == 'HD1080' or is_hd:
stream_url = data['rtmpHd']
else:
stream_url = data['rtmp']
else:
log.error(EvizVersionClient.ret_code.get(
data['ret'], '未知错误{}').format(camera_sn))
if data['ret'] == '60060': # 设备未开通直播
open_result = ysy_client.open_device_live(token, camera_sn)
if open_result['code'] == '200' \
and open_result['data'][0]['ret'] == '200':
return VideoRecord.update_rtmp_stream(camera_sn, stream_url, is_hd)
return stream_url
@staticmethod
def resolution_compare(pattern, string):
try:
p_w, p_h = pattern.split('x')
s_w, s_h = string.split('x')
return int(p_w) * int(p_h) > int(s_w) * int(s_h)
except Exception as e:
log.exception(e)
return False
@staticmethod
def update_video_stream(camera, stream_url):
# 判断视频是否使用高清流
# is_hd = True if VideoRecord.resolution_compare(camera['video_resolution'], '768x432') else False
is_hd = True
stream_url = VideoRecord.update_rtmp_stream(camera['device_code'], stream_url, is_hd=is_hd)
# TODO rtmp直播流地址缓存到云端
# if stream_url:
# mysql.update_video_stream(camera['device_code'], stream_url)
return stream_url
def run_record(self):
sn_list = []
# last_time = datetime.now() - timedelta(minutes=8)
# offline_device_list = db.influxdb_client.get_devices_status(last_time, status=0)
for camera_info in mysql.get_camera_info(platform='eviz'):
# if camera_sn in offline_device_list:
# log.info('设备%s在此刻已经离线', camera_sn)
# db.influxdb_client.insert_restart_second(
# camera_sn, reason='离线')
# continue
if not camera_info['ai_config_support'] or len(camera_info['ai_config_support']) < 5:
continue
if camera_info['ai_config_support'][0] == '0':
# 只有开启了云存储的才会启动录制服务
log.warning('摄像头%s未开启云存储功能,不需要进行录制', camera_info['device_code'])
continue
full_day = True if camera_info['ai_config_support'][2] == '1' else False
movement = True if camera_info['ai_config_support'][3] == '1' else False
sn_list.append(camera_info['device_code'])
rtmp_url = VideoRecord.update_video_stream(camera_info, camera_info['video_stream_url'])
video_path = settings.get('VIDEOS_PATH')
os.makedirs(video_path, exist_ok=True)
video_file_name = os.path.join(
video_path, '{}_%Y-%m-%d_%H-%M-%S.mp4'.format(camera_info['device_code']))
if camera_info['device_code'] not in self.running and rtmp_url:
self.create_thread(camera_info['device_code'], rtmp_url, video_file_name)
return sn_list
def record_check(self):
sn_list = []
kill_proc_sn = []
check = False
now = datetime.now()
if not self.last_check_time \
or (now - self.last_check_time > timedelta(minutes=5)):
self.last_check_time = now
log.info('检查是否有新摄像头...')
sn_list = self.run_record()
check = True
t1 = time.time()
for camera_sn, record_thread in self.running.items():
if check and camera_sn not in sn_list:
kill_proc_sn.append(camera_sn)
continue
if record_thread.is_alive():
log.info('%s的录制进程还在录制,初始分辨率为%s',
camera_sn, record_thread.video_resolution)
# 创建进程判定分辨率是否发生变化
# if record_thread.video_stream_type == 'rtmp':
# try:
# ffprobe_key = 'video:camera_sn:{}:stream_url:{}:resolution'.format(
# camera_sn, record_thread.stream_url)
# with redis_utils.redis_connect() as pipe:
# res = pipe.set(ffprobe_key, '', nx=True, ex=120)
# if res:
# log.info('设置%s的查询分辨率任务', camera_sn)
# probe_thread = FfprobeThread(camera_sn, record_thread.stream_url,
# record_thread, ffprobe_key)
# probe_thread.daemon = True
# probe_thread.start()
# except Exception as e:
# log.exception(e)
# send_error_message('record', e)
continue
if now - record_thread.create_time > timedelta(seconds=6):
log.warning('%s录制进程退出,现在进行重启!',
record_thread.name)
self.create_thread(record_thread.name, record_thread.stream_url,
record_thread.out_file_path, record_thread.video_stream_type)
# self.retry_count(record_thread.name, now.strftime('%Y-%m-%d'))
log.info('--- \n当前存活摄像头%s路, 检查当前摄像头存活状态耗时%s',
len(self.running), round(time.time() - t1, 2))
for camera_sn in kill_proc_sn:
log.warning('摄像头%s已经不存在,将删除其录制进程', camera_sn)
if self.running[camera_sn].ffmpeg_proc:
# TODO 这里的kill并不能把录制ffmpeg进程杀掉,需要测试
self.running[camera_sn].kill()
self.running.pop(camera_sn)
def start(self):
try:
while True:
now = datetime.now(tz)
if now > self.stop_time:
break
time.sleep(1)
try:
# db.load_db(settings.get('INFLUXDB'))
self.record_check()
except Exception as e:
log.exception(e)
# send_error_message(service_name, e)
except KeyboardInterrupt:
log.info('Ctrl+C')
finally:
self.clean()
log.info('主进程退出')
def clean(self):
for camera_sn, record_thread in self.running.items():
if record_thread.is_alive():
record_thread.kill()
log.info('最后停顿5s,执行killall命令保证ffmpeg都能退出!')
time.sleep(5)
# apt-get install psmisc
# TODO 这里执行了killall如果event进程在运行可能会误杀!但是如果去掉不知道能否保证所有的ffmpeg进程都可以退掉
# os.system('killall -9 ffmpeg')
def runner():
def start_record():
video = VideoRecord()
video.start()
start_record()
scheduler = BlockingScheduler(
{'apscheduler.timezone': TIMEZONE}
)
scheduler.add_job(start_record, 'cron',
hour=settings.get('RECORD_START_HOUR', 6))
scheduler.start()
if __name__ == '__main__':
from intelab_python_sdk.logger import log_init
log_init(__file__, True, '/tmp/logs/video')
runner()
......@@ -12,7 +12,7 @@ from ils_common_video.db import rabbitmq_connect, mysql
from ils_common_video.db.mysql import get_camera_info, insert_video_info
from ils_common_video.utils.alarm_utils import send_alarm_to_developer
from ils_common_video.utils.api_helper import IntelabApiHelper, PlaybackUrlException
from ils_common_video.utils.pre_event import PreEvent
from ils_common_video.isc_video.pre_event import PreEvent
api_helper = IntelabApiHelper()
......
......@@ -90,4 +90,6 @@ if __name__ == '__main__':
# print(oss_download_file('https://test-qzwjtest.oss-cn-hangzhou.aliyuncs.com/test-2.mp4', 't.mp4'))
# print(oss_delete_file('https://test-qzwjtest.oss-cn-hangzhou.aliyuncs.com/test-2.mp4'))
# oss_download_file('D00268229_2020-10-23_14-07-13.mp4', '/tmp/v3/videos/D00268229_2020-10-23_14-07-13.mp4')
print(oss_delete_file('https://prod-jiandu-shanghai.oss-cn-shanghai.aliyuncs.com/isc_record/ISC_D86639983_20210509T143611_20210509T143859.mp4'))
# print(oss_delete_file('https://prod-jiandu-shanghai.oss-cn-shanghai.aliyuncs.com/isc_record/ISC_D86639983_20210509T143611_20210509T143859.mp4'))
oss_download_file('https://prod-isc.oss-cn-shanghai.aliyuncs.com/E82843971_2021-07-21_16-28-18.mp4', '/tmp/videos/E82843971_2021-07-21_16-28-18.mp4')
差异被折叠。
import shutil
import os
from datetime import timedelta
import dateutil.parser
from intelab_python_sdk.logger import log
from intelab_python_sdk.ffmpeg import ffmpeg_capture
from ils_common_video.utils.record_utils import get_video_duration, time_to_seconds
class VideoFile:
def __init__(self, full_path):
"""
full_path: 文件的全路径
"""
self._duration = None
self._is_opened = None
self._frame = None
self.full_path = full_path
self.dir_path, self.file_path = os.path.split(full_path)
self.file_name, self.postfix = self.file_path.rsplit('.', 1)
if len(self.file_name.split('_')) == 3:
# 文件名格式不正确
self.sn, self.date, self.time = self.file_name.split('_')
# HD文件名处理
self.sn = self.sn[3:] if 'HDV' in self.sn else self.sn
else:
self.sn = self.date = self.time = None
self.start_time = dateutil.parser.parse(
' '.join([self.date, self.time.replace('-', ':')]))
self._duration = self._bitrate = self._resolution = self._media_type = None
self._end_time = None
self._error_log = ''
self.load = False
self._picture_path = os.path.join(self.dir_path, self.file_name + '.jpg')
self._device_name = ''
self.rename = False
self.network_quality = 1.0 # 网络质量得分
self._network_quality_grade = ''
@property
def error_log(self):
if not self._error_log and not self.load:
self._get_video_info()
return self._error_log
@property
def duration(self):
if not self._duration:
self._get_video_info()
return self._duration
@property
def bitrate(self):
if not self._bitrate:
self._get_video_info()
return self._bitrate
@property
def resolution(self):
if not self._resolution:
self._get_video_info()
return self._resolution
@property
def media_type(self):
if not self._media_type:
self._get_video_info()
return self._media_type
@property
def end_time(self):
if not self._end_time:
self._get_video_info()
return self._end_time
def _get_video_info(self):
video_info, self._error_log = get_video_duration(self.full_path)
self._duration = time_to_seconds(video_info['duration'])
self._bitrate = video_info['bitrate']
self._resolution = video_info['resolution']
self._media_type = video_info['media_type']
self._end_time = self.start_time + timedelta(seconds=self._duration)
if self._duration == 7 \
and self._media_type == 'VideoHandler' \
and self._resolution == '512x288':
# 时长为7秒且无音频且分辨率为512x288的视频文件为萤石云无效视频
self._error_log = '萤石云无效视频分辨率为512x288'
self.load = True
@property
def network_quality_grade(self):
if self.network_quality > 0.95:
self._network_quality_grade = '优'
elif self.network_quality > 0.20:
self._network_quality_grade = '良'
else:
self._network_quality_grade = '差'
return self._network_quality_grade
def rename_by_start_time(self, start_time):
new_file_name = '{}_{}'.format(self.sn, start_time.strftime('%Y-%m-%d_%H-%M-%S'))
full_path = os.path.join(self.dir_path, '{}.{}'.format(new_file_name, self.postfix))
if os.path.isfile(self._picture_path):
shutil.move(self._picture_path, os.path.join(self.dir_path, '{}.{}'.format(new_file_name, 'jpg')))
return full_path
@property
def picture_path(self):
if not os.path.exists(self._picture_path):
# 如果文件所略图不存在,则打开视频文件,保存一帧
ffmpeg_capture.capture(self.full_path, self._picture_path)
return self._picture_path
@property
def device_name(self):
return self._device_name
@property
def size(self):
"""
获取文件大小(M: 兆) K = B / 1024, M = K / 1024, G = M / 1024
"""
file_path = self.get_exists_file()
size = 0
try:
if os.path.isfile(file_path):
size = os.path.getsize(file_path) / 1024.0
except Exception as e:
log.warning('获取文件%s大小失败!', self.file_name)
log.exception(e)
return size
def get_uploaded_name(self):
new_file_file_name = os.path.join(self.dir_path,
self.device_name + self.file_path.replace('-', '_'))
return new_file_file_name
def get_exists_file(self):
if os.path.exists(self.full_path):
file_path = self.full_path
else:
uploaded_name = self.get_uploaded_name()
if os.path.exists(uploaded_name):
file_path = uploaded_name
else:
file_path = ''
log.warning('未获取到文件%s的有效的文件名', self.file_name)
return file_path
@staticmethod
def gen_file_name(camera_code, start_time, end_time, prefix='ISC', part_num=None):
elements = [
prefix, camera_code,
start_time.strftime('%Y%m%dT%H%M%S'),
end_time.strftime('%Y%m%d%H%M%S')
]
if part_num is not None:
elements.append(str(part_num))
return '_'.join(elements) + '.mp4'
if __name__ == '__main__':
video_file = VideoFile('/tmp/videos/E82843165_2021-07-21_15-43-24.mp4')
# print(video_file.picture_path)
print(video_file.duration)
print(video_file.size)
print(video_file.resolution)
......@@ -4,7 +4,7 @@ from dynaconf import settings
from datetime import datetime
from ils_common_video.utils.isc_client import HikVisionClient
from ils_common_video.utils.pre_event import PreEvent
from ils_common_video.isc_video.pre_event import PreEvent
tz = pytz.timezone('Asia/Shanghai')
......
......@@ -7,7 +7,7 @@ from dynaconf import settings
from ils_common_video.utils.isc_client import HikVisionClient
from ils_common_video.utils.record_utils import record_thread, get_video_duration
from ils_common_video.utils.api_helper import PlaybackUrlException
from ils_common_video.utils.pre_event import PreEvent
from ils_common_video.isc_video.pre_event import PreEvent
tz = pytz.timezone('Asia/Shanghai')
......@@ -23,10 +23,10 @@ client = HikVisionClient(config.get('KEY'), config.get('SECRET'),
def main():
start_time = datetime(2021, 7, 5, 14, 33, 56).astimezone(tz)
start_time = datetime(2021, 7, 22, 10, 0, 0).astimezone(tz)
# start_time = datetime(2021, 5, 28, 9, 10, 59).astimezone(tz)
end_time = datetime(2021, 7, 5, 14, 34, 15).astimezone(tz)
camera_index = '8f50e406cad6489fac443e034d29a66f'
end_time = datetime(2021, 7, 22, 10, 5, 0).astimezone(tz)
camera_index = 'a3cafffd4114438eb197f48af1e19293'
results = []
try:
......@@ -51,6 +51,7 @@ def main():
print('ERROR:', e)
else:
print(res)
print(results)
for event in results:
cur_start_time = max(event['start_time'], start_time)
......@@ -75,7 +76,7 @@ def stream_record(stream, start_time, end_time):
video_path, 'rtmp_{}_{}_{}.mp4'.format('y', start_time, end_time))
# TODO 多进程处理
print(stream_url, start_time, end_time)
record_thread(stream_url, file_name, thread_name='y', protocol=stream['protocol'])
# record_thread(stream_url, file_name, thread_name='y', protocol=stream['protocol'])
return get_video_duration(file_name)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论