本帖最后由 wz1st 于 2024-3-8 09:15 编辑
0x00 前提
论坛中看到了大佬的【立省100%】小米系可视门铃本地存储 ,遂在自己的环境上折腾,经过一天的和领导斗智斗勇(划掉)折腾,虽成功实现,但也遇见不少小问题:- 米家猫眼的m3u8文件使用AES加密,#35楼中的对m3u8进行切片再组装的方案无法获取原始视频数据;
- 保存成功的视频使用的是HEVC格式编码,部分设备播放黑屏或无法播放;
- 网络环境波动导致m3u8文件或ts文件下载失败;
so.沿用大佬的思路,用Python改造了一下,现在开始吧!!
0x01 准备
emmmm
把猫眼改为实时查看模式,不然HA经常性的猫眼掉线
0x02 配置
HACS中找pyscript,安装;/config/pyscript中新建video_doorbell.py,写入下面的代码
import re, requests, functools, urllib.parse
@service
def video_doorbell():
entity = camera.loock_v06_d9c1_video_doorbell #替换为你自己的实体
stream_address = urllib.parse.quote(getattr(entity, 'stream_address'))
motion_video_time = re.sub(r'[^0-9]', '', getattr(entity, 'motion_video_time'))
save_video(stream_address, motion_video_time)
async def save_video(stream_address, motion_video_time):
nas_addr = "http://192.168.0.250:5005" #替换为你自己的nas地址,端口为app.py中定义的端口,或者docker开放的端口
post_request = functools.partial(requests.post, f'{nas_addr}/save_video', data={"stream_address": stream_address, "motion_video_time": motion_video_time})
response = await hass.async_add_executor_job(post_request)
return response
在开发者工具->YAML配置->重载Pyscript Python scripting
新建一个自动化
alias: 猫眼视频保存
description: ""
trigger:
- platform: state
entity_id:
- camera.loock_v06_d9c1_video_doorbell
attribute: motion_video_time
condition: []
action:
- service: pyscript.video_doorbell
data: {}
mode: single
不一定非要用Nas,总结来说Docker、装有ffmpeg的Linux这两种类型设备,用HAOS、openwrt也行
1.Linux
安装ffmpeg和驱动
apt-get update && apt-get install -y ffmpeg libva-drm2 libva2 vainfo i965-va-driver && apt-get clean #centos和win自己解决
安装Python3和pip(自己解决),安装依赖库:pip3 install flask requests pycryptodome pycrypto m3u8
新建app.py文件,写入以下代码,然后运行:python3 app.py (后台运行:nohup python3 app.py > save2mp4.log 2>&1 &)
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from flask import Flask, request
import urllib.parse, os, requests, m3u8
from datetime import datetime
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
app = Flask(__name__)
@app.route('/save_video', methods=['POST'])
def save_video():
current_date = datetime.now().strftime("%Y%m%d")
data = request.form
stream_address = urllib.parse.unquote(data.get('stream_address'))
motion_video_time = data.get('motion_video_time')
save_path = '/nas' #视频保存位置
video_limit = '30' #保存天数上限
m3u8_file = f'{save_path}/tmp/{motion_video_time}.m3u8'
ts_tmp_dir = f'{save_path}/tmp/{motion_video_time}/'
try:
os.makedirs(f"{save_path}/{current_date}")
except:
pass
try:
os.makedirs(f"{save_path}/tmp")
except:
pass
try:
os.makedirs(ts_tmp_dir)
except:
pass
download_m3u8(stream_address, m3u8_file)
with open(m3u8_file, 'r') as f:
m3u8_data = f.read()
m3u8_decode(m3u8_data, ts_tmp_dir)
try:
os.system(f"rm -rf {save_path}/{current_date}/{motion_video_time}.mp4")
# 猫眼的视频是HEVC编码格式,有点占用较少,缺点不同的设备不一定能播放出画面
# os.system(f"ffmpeg -f concat -safe 0 -i {ts_tmp_dir}ts.list -c copy {save_path}/{current_date}/{motion_video_time}.mp4")
# 软解把HEVC转成H.264,占用较大,软解硬解都能放
os.system(f"ffmpeg -f concat -safe 0 -i {ts_tmp_dir}ts.list -c:v libx264 -preset slow -crf 22 -c:a copy -b:a 128k {save_path}/{current_date}/{motion_video_time}.mp4")
# 启用VAAPI硬编码
# os.system(f"ffmpeg -f concat -safe 0 -i {ts_tmp_dir}ts.list -vaapi_device /dev/dri/renderD128 -c:v h264_vaapi -global_quality 25 -vf 'format=nv12|vaapi,hwupload' -c:a copy {save_path}/{current_date}/{motion_video_time}.mp4")
os.system(f"rm -rf {save_path}/tmp/{motion_video_time}*")
os.system(f"find {save_path} -type f -mtime +{video_limit} -delete")
os.system(f"find {save_path} -depth -type d -empty -delete")
except:
pass
return "ok"
def download_m3u8(url, output_file):
for i in range(5):
try:
response = requests.get(url)
if response.status_code == 200:
with open(output_file, 'wb') as f:
f.write(response.content)
break
except:
pass
def m3u8_decode(m3u8_data, ts_tmp_dir):
m3u8_obj = m3u8.loads(m3u8_data)
key = download_key(m3u8_obj.keys[0].uri)
iv = bytes.fromhex(m3u8_obj.keys[0].iv.split('0x')[1])
os.system(f"rm -rf {ts_tmp_dir}ts.list")
with open(f"{ts_tmp_dir}ts.list", "a") as f:
for i in range(len(m3u8_obj.segments)):
for j in range(5):
try:
ts_data = requests.get(m3u8_obj.segments[i].uri).content
break
except:
pass
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(cipher.decrypt(ts_data), AES.block_size)
with open(f"{ts_tmp_dir}{i}.ts", "wb") as f1:
f1.write(decrypted_data)
f.write(f"file '{ts_tmp_dir}{i}.ts'\n")
return True
def download_key(key_url):
for i in range(5):
try:
response = requests.get(key_url)
return response.content
except:
pass
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5005)
2.Docker
Docker版用的是Dockerfile创建镜像,还没学会上传到dockerhub,具体文件可以看下我的gayhub:保存米家门铃视频
a.把gayhub仓库的nas文件夹放到你自己的nas中,cd命令切换到nas,执行命令docker build -t save2mp4 .创建镜像
b.运行docker run -it -p 5005:5005 -v $(pwd):/app -v /{替换为你的视频保存地址}:/nas --device=/dev/dri:/dev/dri save2mp4
0x03 结束
可以到门口溜一圈了,效果如下
效果
0x04 吐槽
莫得markdown...... 代码缩紧问题自行处理吧。。
gayhub点个星也是极好的
|