万码皆空的博客
Published on

监控辅助工具的实现过程(三)

Authors
  • avatar
    Name
    万码皆空
    Twitter

问题的关键是session的自动获取。不知道当时哪里来的懒劲儿,就是不想写代码,一心想找一个自动化工具(苹果的Automator和微软的Power Automate)来实现,最后发现这并不会比写点代码简单,只好放弃。

时间一长,又开始嫌麻烦了。刚巧不知在哪里看到了一个python包,叫ddddocr,用来识别验证码。拿来测试了一下效果很好。那不能浪费,用一下吧。

获取session

获取session需要三步:

  1. 使用手机号、密码、验证码登陆获取token
  2. 使用token获取可用摄像头播放地址
  3. 从任意播放地址中提取session

因为全家只有我一个账号能访问,所以手机信息我直接写死就可以了,代码如下:

import ddddocr
import requests
import time

# 获取验证码,同时需要从cookie中获取一个code_key
def get_verify_code():
    t = time.time()
    timestamp = int(round(t * 1000))

    try:
        response = requests.get(
                "https://target.domain/verifyCode?t={}".format(timestamp))

        code_key = response.cookies['CODE_KEY']
        ocr = ddddocr.DdddOcr()
        code = ocr.classification(response.content)
        
        return (code.lower(), code_key)
    except Exception as err:
        return ('','')

# 获取token,使用
def get_token():
    code, code_key = get_verify_code()

    headers = {
        "accept": "*/*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "Cookie": "CODE_KEY={}".format(code_key),
        "content-type": "application/json;charset=UTF-8"
    }

    body = '{{"username":"139xxxxxxxx","password":"12345678","verifyCode":"{}"}}'.format(code)

    try:
        response = requests.post("https://target.domain/user/login", headers=headers, data=body)
        res = response.json()
        success_code = res['code']
        data = res['data']

        if success_code == '000': 
            return data['token']
        else:
            raise Exception('获取token失败,错误代码:{}'.format(success_code))
    except Exception as err:
        return ''

# 获取session
def get_session():
    token = get_token()

    if len(token) == 0:
        return ''

    headers = {
        "accept": "*/*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/json;charset=UTF-8"
    }

    body = '{{"token":"{}","camera_id":"camera_id001","childId":00000001}}'.format(token)

    try:
        response = requests.post("https://target.domain/camera/info", headers=headers, data=body)
        res = response.json()
        success_code = res['code']

        if success_code == '000': 
            data = res['data']
            return data['rtmpUrl'][-43:]
        else:
            raise Exception('获取session失败,错误代码:{}'.format(success_code))

    except Exception as err:
        return ''

以上代码隐藏了真实url和用户信息,调用get_session 就可以拿到session了。

代码在本地正常,可是放到服务器上,却一直出错。排查发现是对方服务器对ip做了限制,如果登陆时换了ip是需要短信验证码验证的。

那只好再处理下。增加了发送验证码和验证的功能。

import sys
import requests
from sessionmgr import get_token

# 获取验证码
def send_sms(token):
    headers = {
        "accept": "*/*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/json;charset=UTF-8"
    }

    body = '{{"token":"{}","mobile":139xxxxxxxx}}'.format(token)

    try:
        response = requests.post("https://target.domain/user/sms", headers=headers, data=body)
        res = response.json()
    except Exception as err:
        print(str(err))

# 提交收到的验证码
def verify_sms(token,code):
    headers = {
        "accept": "*/*",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/json;charset=UTF-8"
    }

    body = '{{"token":"{}","mobile":139xxxxxxxx,"verifyCode":"{}"}}'.format(token, code)

    try:
        response = requests.post("https://target.domain/user/verify", headers=headers, data=body)
        res = response.json()
    except Exception as err:
        print(str(err))

# 获取命令行参数执行命令
if sys.argv[1] == 'token':
    print(get_token())
elif sys.argv[1] == 'send':
    send_sms(sys.argv[2])
elif sys.argv[1] == 'verify':
    verify_sms(sys.argv[2], sys.argv[3])
else:
    pass

将代码保存到sms_verify.py,拷贝到服务器端执行如下命令:

# 获取token,用来发送验证码
python sms_verify.py token
# 使用上一步拿到的token发送验证码
python sms_verify.py send token_str
# 收集收到验证码后,提交
python sms_verify.py verify verify_code_str

验证成功后,服务器端就可以直接获取session了。

使用http服务

然后我们做一个获取session的http服务,这样就可以随时随地访问获取session了。

from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
import time

from sessionmgr import get_session

SESSION_EXPIRED_TIME = 30 * 60

class ResquestHandler(BaseHTTPRequestHandler):
    __session = ''
    __session_time = 0

    def __get_session(self):
        if time.time() - ResquestHandler.__session_time > SESSION_EXPIRED_TIME:
            ResquestHandler.__session, ResquestHandler.__session_time = (
                get_session(), time.time())

    def do_GET(self):
        try:
            self.__get_session()
			self.send_response(200)
		    self.send_header("Content-type", "text/html")
		    self.end_headers()
		    self.wfile.write(ResquestHandler.__session.encode('utf-8'))
        except Exception as err:
            self.__send_response(500, err)

def main():
    server = HTTPServer(('', 8889), ResquestHandler)
    server.serve_forever()

main()

启动后,我们就可以通过访问 https://my.domain:8889/ 获取session了。

优化转发服务

因为获取session有时间段限制,我们的转发服务还是不能停,这样在超出合法时间后我们也能查看被转发的摄像头。

我们先改一下nginx配置,去掉execstatic指令,这样nginx就不需要重启了。

# rtmp服务配置
rtmp {
        server {
                listen 8888;
                chunk_size 4000;

                application live1 {
                        live on;
                        record off;
                }

                application live2 {
                        live on;
                        record off;
                }

                application live3 {
                        live on;
                        record off;
                }
        }
}

然后我们使用python的subprocess模块自己管理ffmpeg进程,当一个ffmpeg子进程出错时,就重新获取session,拼凑出播放地址,然后启动新的ffmpeg进程推送到我们的转发服务上,代码就不写了,推送命令如下:

ffmpeg -rw_timeout 30000000 -probesize 102400 -i "rtmp://xxxx.xxx/camera_id001?session=xxxxxxxxxxxx" -c copy -f flv rtmp://localhost:8888/live1

编写客户端脚本

接下来,我们编写客户端播放脚本:

import subprocess
import shlex
import sys
import requests

from help import print_help

CMD_TEMPLATE = 'ffplay rtmp://xxxx.xxx/{}?session={} -analyzeduration 500'
CMD_LIVE = 'ffplay rtmp://{}:8888/{} -analyzeduration 500'

def main():
	# 打印帮助信息
    if sys.argv[1] == 'help':
        print_help()
	# 如果启动参数以live开头,则请求转发服务
    elif sys.argv[1].find('live') == 0:
        cmd = shlex.split(CMD_LIVE.format(HOST, sys.argv[1]))
	# 否则,直连对方直播服务
    else:
        resposne = requests.get('http://my.domain:8888/')
        session = resposne.text
        camera_id = 'camera_id{:0>3}'.format(sys.argv[1])

        cmd = shlex.split(CMD_TEMPLATE.format(camera_id, session))

    subprocess.run(cmd)

查看直播命令如下:

# 使用转发服务
python play.py live1
# 直连对方直播服务
python play.py 1

记录脚本与play类似,替换成ffmpeg命令即可。这样客户端就算制作完成了。

android

android系统自带的播放器组件不能解码这个视频格式,我们还需要使用ffmpeg。不过自己编译、集成非常麻烦,干脆直接修改ijkplayer的example吧。修改FileExplorerActivity.java 直接展示一个摄像头id列表,点击某个item时获取session拼凑出播放地址,然后播放。代码大同小异,就不贴了,直接上几个图吧。

移动端列表
移动端播放中

ios

也是修改example。

总结

目前基本解决了我的需求,除了完全自动录制播放。其实自动获取session还不能根本性解决问题,因为过了关闭监控时间,就没办法获取session了。有的时候,孩子上兴趣班会到别的教室,这时候除了转发的三个监控,其他监控因为无法获得session已经没办法查看。要想进一步突破限制的话,要不把监控全部设置到转发服务器,要不想办法突破获取session的限制。

对方的session还是有一定规律的,通过摸索,心里大概有一个设想,可是我不想太深入去做,因为已经够用了,如果还有忍不住的一天那就再说吧。