2026-08-30 22:22:11 +08:00
|
|
|
|
import os
|
|
|
|
|
|
import time
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import platform
|
|
|
|
|
|
|
|
|
|
|
|
from django.db import transaction
|
|
|
|
|
|
from app.views.ViewsBase import *
|
|
|
|
|
|
from app.utils.LanguageUtils import GSettingsLangDefault
|
|
|
|
|
|
from app.utils.Utils import GB28181CodeUtils
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
内部服务调用的接口
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 按code的细粒度锁(不同code完全并发,同一code串行化,防止并发插入重复编号)
|
|
|
|
|
|
_code_locks = {}
|
|
|
|
|
|
_code_locks_guard = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
def _get_code_lock(code):
|
|
|
|
|
|
"""获取指定code的锁对象"""
|
|
|
|
|
|
with _code_locks_guard:
|
|
|
|
|
|
if code not in _code_locks:
|
|
|
|
|
|
_code_locks[code] = threading.Lock()
|
|
|
|
|
|
return _code_locks[code]
|
|
|
|
|
|
|
|
|
|
|
|
# (内部调用,无需国际化)被monitor调用,用于修改流(主要是国标等协议)
|
|
|
|
|
|
def api_on_media_update_stream(request):
|
|
|
|
|
|
ret = False
|
|
|
|
|
|
msg = "unknown error"
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
|
params = f_parsePostParams(request)
|
|
|
|
|
|
lang = f_parseRequestLang(request)
|
|
|
|
|
|
try:
|
|
|
|
|
|
forward_state = int(params.get("forwardState", 0))
|
|
|
|
|
|
app = params.get("app") # 流app
|
|
|
|
|
|
name = params.get("name") # 流name(视频通道channelId)
|
|
|
|
|
|
ip = params.get("ip") # 设备IP
|
|
|
|
|
|
port = int(params.get("port", 0)) # 设备SIP通信端口
|
|
|
|
|
|
clientId = params.get("clientId") # gb28181注册的client_id
|
|
|
|
|
|
parentID = params.get("parentID", "") # 设备连接的sipServer-sipId
|
|
|
|
|
|
rtpServerPort = int(params.get("rtpServerPort", 0))
|
|
|
|
|
|
rtpPort = int(params.get("rtpPort", 0))
|
|
|
|
|
|
|
|
|
|
|
|
pullStreamType = int(params.get("pullStreamType", 0))
|
|
|
|
|
|
pullStreamUrl = params.get("pullStreamUrl")
|
|
|
|
|
|
|
|
|
|
|
|
cameraSumNum = int(params.get("cameraSumNum", 1))
|
|
|
|
|
|
cameraName = params.get("cameraName", "")
|
|
|
|
|
|
cameraManufacturer = params.get("cameraManufacturer", "")
|
|
|
|
|
|
cameraModel = params.get("cameraModel", "")
|
|
|
|
|
|
cameraOwner = params.get("cameraOwner", "")
|
|
|
|
|
|
cameraCivilCode = params.get("cameraCivilCode", "")
|
|
|
|
|
|
|
|
|
|
|
|
lastKeepaliveTime = int(params.get("lastKeepaliveTime", 0))
|
|
|
|
|
|
lastRegisterTime = int(params.get("lastRegisterTime", 0))
|
|
|
|
|
|
rtpTransferMode = int(params.get("rtpTransferMode", 0))
|
|
|
|
|
|
rtpTransferAudioType = int(params.get("rtpTransferAudioType", 0))
|
|
|
|
|
|
|
|
|
|
|
|
if app is None:
|
|
|
|
|
|
raise Exception("The parameter app is invalid")
|
|
|
|
|
|
else:
|
|
|
|
|
|
app = str(app)
|
|
|
|
|
|
if name is None:
|
|
|
|
|
|
raise Exception("The parameter name is invalid")
|
|
|
|
|
|
else:
|
|
|
|
|
|
name = str(name)
|
|
|
|
|
|
if ip is None:
|
|
|
|
|
|
raise Exception("The parameter ip is invalid")
|
|
|
|
|
|
else:
|
|
|
|
|
|
ip = str(ip)
|
|
|
|
|
|
|
|
|
|
|
|
if clientId is None:
|
|
|
|
|
|
raise Exception("The parameter clientId is invalid")
|
|
|
|
|
|
else:
|
|
|
|
|
|
clientId = str(clientId)
|
|
|
|
|
|
|
|
|
|
|
|
if pullStreamUrl is None:
|
|
|
|
|
|
raise Exception("The parameter pullStreamUrl is invalid")
|
|
|
|
|
|
if cameraName is None:
|
|
|
|
|
|
raise Exception("The parameter cameraName is invalid")
|
|
|
|
|
|
|
|
|
|
|
|
now_date = datetime.now()
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
# 【关键修复 v2】GB28181 多设备共享同一 channel code 时,
|
|
|
|
|
|
# 以 (code + camera_device_id) 复合键查找/锁:
|
|
|
|
|
|
# - 不同 device_id 的同名通道各自独立创建/更新记录
|
|
|
|
|
|
# - 非 GB28181 场景 camera_device_id 为空字符串,退化为纯 code 匹配
|
|
|
|
|
|
# 注意:SQLite写锁是数据库级的,不能用transaction.atomic(),否则不同复合键会互相阻塞
|
|
|
|
|
|
compound_key = f"{name}::{clientId}"
|
|
|
|
|
|
code_lock = _get_code_lock(compound_key)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
with code_lock:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
stream = StreamModel.objects.filter(code=name, camera_device_id=clientId).first()
|
2026-08-30 22:22:11 +08:00
|
|
|
|
if stream:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
# 编辑:不修改 nickname(用户自定义)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
pass
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 新增
|
|
|
|
|
|
stream = StreamModel()
|
|
|
|
|
|
stream.user_id = 0
|
|
|
|
|
|
stream.sort = 0
|
|
|
|
|
|
stream.code = name
|
|
|
|
|
|
stream.app = app
|
|
|
|
|
|
stream.name = name
|
|
|
|
|
|
stream.create_time = now_date
|
2026-09-04 18:16:14 +08:00
|
|
|
|
stream.add_type = 21 if pullStreamType == 21 else 0 # 21=GB28181 自动添加标记
|
2026-08-30 22:22:11 +08:00
|
|
|
|
stream.state = 0
|
|
|
|
|
|
stream.nickname = cameraName
|
|
|
|
|
|
stream.remark = ""
|
|
|
|
|
|
|
|
|
|
|
|
stream.forward_state = forward_state
|
|
|
|
|
|
stream.last_update_time = now_date
|
|
|
|
|
|
stream.pull_stream_type = pullStreamType
|
|
|
|
|
|
stream.pull_stream_url = pullStreamUrl
|
|
|
|
|
|
stream.pull_stream_ip = ip
|
|
|
|
|
|
stream.pull_stream_port = port
|
|
|
|
|
|
|
|
|
|
|
|
stream.camera_sum_num = cameraSumNum
|
|
|
|
|
|
stream.camera_name = cameraName
|
|
|
|
|
|
stream.camera_manufacturer = cameraManufacturer
|
|
|
|
|
|
stream.camera_owner = cameraOwner
|
|
|
|
|
|
stream.camera_model = cameraModel
|
|
|
|
|
|
stream.camera_civilcode = cameraCivilCode
|
|
|
|
|
|
stream.camera_device_id = clientId
|
|
|
|
|
|
stream.camera_parent_id = parentID
|
|
|
|
|
|
|
|
|
|
|
|
if lastKeepaliveTime > 0:
|
|
|
|
|
|
lastKeepaliveTime_date = datetime.fromtimestamp(int(lastKeepaliveTime / 1000))
|
|
|
|
|
|
stream.camera_last_keepalive_time = lastKeepaliveTime_date
|
|
|
|
|
|
|
|
|
|
|
|
if lastRegisterTime > 0:
|
|
|
|
|
|
lastRegisterTime_date = datetime.fromtimestamp(int(lastRegisterTime / 1000))
|
|
|
|
|
|
stream.camera_last_register_time = lastRegisterTime_date
|
|
|
|
|
|
|
|
|
|
|
|
stream.pull_stream_transfer_mode = rtpTransferMode
|
|
|
|
|
|
stream.is_audio = rtpTransferAudioType
|
|
|
|
|
|
|
|
|
|
|
|
stream.save()
|
|
|
|
|
|
|
|
|
|
|
|
ret = True
|
|
|
|
|
|
msg = LANG_VIEWS_T(request, "msg_success")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
msg = str(e)
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = LANG_VIEWS_T(request, "msg_method_not_supported")
|
|
|
|
|
|
|
|
|
|
|
|
res = {
|
|
|
|
|
|
"code": 1000 if ret else 0,
|
|
|
|
|
|
"msg": msg
|
|
|
|
|
|
}
|
|
|
|
|
|
if not ret:
|
|
|
|
|
|
g_logger.warning("InnerView.api_on_media_update_stream() res=%s" % str(res))
|
|
|
|
|
|
return f_responseJson(res)
|
|
|
|
|
|
|
|
|
|
|
|
# (内部调用,无需国际化)被GB28181SipServer调用,用于删除回退通道的数据库记录
|
|
|
|
|
|
def api_on_media_delete_stream(request):
|
|
|
|
|
|
ret = False
|
|
|
|
|
|
msg = "unknown error"
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
|
try:
|
|
|
|
|
|
params = f_parsePostParams(request)
|
|
|
|
|
|
code = params.get("code")
|
|
|
|
|
|
if not code:
|
|
|
|
|
|
raise Exception("The parameter code is invalid")
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
# 支持可选 cameraDeviceId:GB28181 多设备共享 channel code 时
|
|
|
|
|
|
# 按 (code, camera_device_id) 复合键精准删除,避免误删同名不同设备的记录
|
|
|
|
|
|
camera_device_id = params.get("cameraDeviceId") or ""
|
|
|
|
|
|
if camera_device_id:
|
|
|
|
|
|
stream = StreamModel.objects.filter(code=code, camera_device_id=camera_device_id).first()
|
|
|
|
|
|
else:
|
|
|
|
|
|
stream = StreamModel.objects.filter(code=code).first()
|
|
|
|
|
|
|
2026-08-30 22:22:11 +08:00
|
|
|
|
if stream:
|
|
|
|
|
|
stream.delete()
|
2026-09-04 18:16:14 +08:00
|
|
|
|
g_gb28181SipServer.remove_channel(code, device_id=camera_device_id or None)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
ret = True
|
|
|
|
|
|
msg = "success"
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = "stream not found"
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
msg = str(e)
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = "method not supported"
|
|
|
|
|
|
|
|
|
|
|
|
res = {
|
|
|
|
|
|
"code": 1000 if ret else 0,
|
|
|
|
|
|
"msg": msg
|
|
|
|
|
|
}
|
|
|
|
|
|
if not ret:
|
|
|
|
|
|
g_logger.warning("InnerView.api_on_media_delete_stream() res=%s" % str(res))
|
|
|
|
|
|
return f_responseJson(res)
|
|
|
|
|
|
|
|
|
|
|
|
# (内部调用,无需国际化)被monitor_zlm调用,用于实时获得推流信息
|
|
|
|
|
|
def api_on_publish(request):
|
|
|
|
|
|
# ZLMediaKit Hook https://github.com/ZLMediaKit/ZLMediaKit/wiki/MediaServer%E6%94%AF%E6%8C%81%E7%9A%84HTTP-HOOK-API
|
|
|
|
|
|
|
|
|
|
|
|
ret = False
|
|
|
|
|
|
msg = "unknown error"
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
|
try:
|
|
|
|
|
|
params = f_parsePostParams(request)
|
|
|
|
|
|
lang = f_parseRequestLang(request)
|
|
|
|
|
|
|
|
|
|
|
|
_app = params.get("app", "").strip()
|
|
|
|
|
|
_stream = params.get("stream", "").strip()
|
|
|
|
|
|
_tcp_id = params.get("id", "").strip()
|
|
|
|
|
|
_schema = params.get("schema", "").strip()
|
|
|
|
|
|
_ip = params.get("ip", "").strip()
|
|
|
|
|
|
_port = int(params.get("port", 0))
|
|
|
|
|
|
|
|
|
|
|
|
if _app == APP_NAME_LIVE:
|
|
|
|
|
|
# 被动推流(RTSP/RTMP推流到ZLM的live应用)
|
|
|
|
|
|
stream_code = _stream
|
|
|
|
|
|
stream = StreamModel.objects.filter(code=stream_code).first()
|
|
|
|
|
|
now_date = datetime.now()
|
|
|
|
|
|
if stream:
|
|
|
|
|
|
if stream.pull_stream_type == 31 or stream.pull_stream_type == 32:
|
|
|
|
|
|
pass
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise Exception("This stream does not come from passive streaming")
|
|
|
|
|
|
else:
|
|
|
|
|
|
stream = StreamModel()
|
|
|
|
|
|
stream.user_id = 0
|
|
|
|
|
|
stream.sort = 0
|
|
|
|
|
|
stream.code = stream_code
|
|
|
|
|
|
stream.app = _app
|
|
|
|
|
|
stream.name = _stream
|
|
|
|
|
|
stream.create_time = now_date
|
|
|
|
|
|
stream.add_type = 0
|
|
|
|
|
|
stream.state = 0
|
|
|
|
|
|
stream.is_audio = 0 # 默认静音(与手动添加一致)
|
|
|
|
|
|
stream.pull_stream_transfer_mode = 0
|
|
|
|
|
|
stream.nickname = _stream
|
|
|
|
|
|
stream.remark = ""
|
|
|
|
|
|
|
|
|
|
|
|
stream.camera_name = _stream
|
|
|
|
|
|
stream.camera_manufacturer = _stream
|
|
|
|
|
|
stream.camera_device_id = "default" # 默认分组编号
|
|
|
|
|
|
|
|
|
|
|
|
if _schema == "rtsp":
|
|
|
|
|
|
pullStreamType = 31
|
|
|
|
|
|
elif _schema == "rtmp":
|
|
|
|
|
|
pullStreamType = 32
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise Exception("unsupported schema type, schema=%s" % _schema)
|
|
|
|
|
|
|
|
|
|
|
|
pullStreamUrl = "%s://%s:%d/%s" % (_schema, _ip, _port, _tcp_id)
|
|
|
|
|
|
|
|
|
|
|
|
stream.forward_state = 1
|
|
|
|
|
|
stream.last_update_time = now_date
|
|
|
|
|
|
stream.pull_stream_type = pullStreamType
|
|
|
|
|
|
stream.pull_stream_url = pullStreamUrl
|
|
|
|
|
|
stream.pull_stream_ip = _ip
|
|
|
|
|
|
stream.pull_stream_port = _port
|
|
|
|
|
|
|
|
|
|
|
|
stream.save()
|
|
|
|
|
|
|
|
|
|
|
|
ret = True
|
|
|
|
|
|
msg = LANG_VIEWS_T(request, "msg_success")
|
|
|
|
|
|
elif _app == APP_NAME_RTP:
|
|
|
|
|
|
# gb28181接入被动接收的推流,默认通过
|
|
|
|
|
|
ret = True
|
|
|
|
|
|
msg = LANG_VIEWS_T(request, "msg_success")
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise Exception("unsupported app, app=%s" % _app)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
msg = "error %s" % str(e)
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = LANG_VIEWS_T(request, "msg_method_not_supported")
|
|
|
|
|
|
|
|
|
|
|
|
res = {
|
|
|
|
|
|
"code": 0 if ret else -1, # 返回-1的请求,推流将会被取消
|
|
|
|
|
|
"msg": msg
|
|
|
|
|
|
}
|
|
|
|
|
|
if not ret:
|
|
|
|
|
|
g_logger.warning("InnerView.api_on_publish() res=%s" % str(res))
|
|
|
|
|
|
|
|
|
|
|
|
return f_responseJson(res)
|
|
|
|
|
|
|
|
|
|
|
|
# (内部调用,无需国际化)被monitor_zlm调用,用于实时获得not found的流信息
|
|
|
|
|
|
def api_on_stream_not_found(request):
|
|
|
|
|
|
# ZLMediaKit Hook https://github.com/ZLMediaKit/ZLMediaKit/wiki/MediaServer%E6%94%AF%E6%8C%81%E7%9A%84HTTP-HOOK-API
|
|
|
|
|
|
ret = False
|
|
|
|
|
|
msg = "unknown error"
|
|
|
|
|
|
params = None
|
|
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
|
params = f_parsePostParams(request)
|
|
|
|
|
|
|
|
|
|
|
|
_app = params.get("app", "").strip()
|
|
|
|
|
|
_stream = params.get("stream", "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if _app == APP_NAME_RTP:
|
|
|
|
|
|
# gb28181 流不存在,尝试重新 invite
|
|
|
|
|
|
stream = StreamModel.objects.filter(code=_stream).first()
|
|
|
|
|
|
if stream and stream.pull_stream_type == 21:
|
|
|
|
|
|
__ret, __msg = g_gb28181SipServer.request_invite(
|
|
|
|
|
|
client_id=stream.camera_device_id, channel_id=stream.name)
|
|
|
|
|
|
if __ret:
|
|
|
|
|
|
ret = True
|
|
|
|
|
|
msg = "success"
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = __msg
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = "the stream does not exist"
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = "unsupported app"
|
|
|
|
|
|
else:
|
|
|
|
|
|
msg = LANG_VIEWS_T(request, "msg_method_not_supported")
|
|
|
|
|
|
|
|
|
|
|
|
if not ret:
|
|
|
|
|
|
g_logger.warning("InnerView.api_on_stream_not_found() params=%s,msg=%s" % (str(params), msg))
|
|
|
|
|
|
|
|
|
|
|
|
res = {
|
|
|
|
|
|
"code": 0,
|
|
|
|
|
|
"msg": "success"
|
|
|
|
|
|
}
|
|
|
|
|
|
return f_responseJson(res)
|
|
|
|
|
|
|