文件预览

osd_config.py

查看 jf-open-pro-device-osd 技能包中的文件内容。

文件内容

scripts/osd_config.py

#!/usr/bin/env python3
"""
杰峰设备 OSD 水印设置技能(开发版)

支持功能:
- 获取/设置单行 OSD 配置
- 获取/设置多行 OSD 配置
- 通道标题设置
- 时间标题设置
- 隐私区域设置
"""

import os
import sys
import argparse
import requests
from typing import Optional, Dict, Any, List

# 导入加密工具(复用)
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
    from crypto import get_time_millis, generate_signature
except ImportError:
    print("❌ 错误:找不到 crypto.py 模块")
    print("   请确保 scripts/crypto.py 存在")
    sys.exit(1)

# API 基础地址(可通过环境变量切换)
JF_ENDPOINT = os.getenv("JF_ENDPOINT", "api-cn.jftechws.com")
JF_BASE_URL = f"https://{JF_ENDPOINT}/gwp/v3"


def get_headers(uuid: str, app_key: str, app_secret: str, move_card: int) -> Dict[str, str]:
    """生成请求头(包含签名和时间戳)"""
    time_millis = get_time_millis()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, move_card)
    
    return {
        "Content-Type": "application/json; charset=UTF-8",
        "uuid": uuid,
        "appKey": app_key,
        "timeMillis": time_millis,
        "signature": signature,
        "X-Request-Id": os.urandom(16).hex()
    }


def get_osd_config(device_token: str, uuid: str, app_key: str,
                   app_secret: str, move_card: int,
                   config_type: str = "single",
                   channel: int = 0) -> Dict[str, Any]:
    """
    获取 OSD 配置
    
    API: POST /gwp/v3/rtc/device/getconfig/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/getconfig/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)
    
    name_map = {
        "single": "AVEnc.VideoWidget",
        "multi": "AVEnc.VideoOSD"
    }
    
    body = {
        "Name": name_map.get(config_type, "AVEnc.VideoWidget")
    }
    
    if channel is not None:
        body["Channel"] = str(channel)
    
    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()
    
    if result.get("code") != 2000:
        raise RuntimeError(f"获取 OSD 配置失败:{result.get('msg', '未知错误')}")
    
    return result.get("data", {})


def set_osd_config(device_token: str, uuid: str, app_key: str,
                   app_secret: str, move_card: int,
                   config: Dict[str, Any],
                   config_type: str = "single",
                   channel: int = 0) -> Dict[str, Any]:
    """
    设置 OSD 配置
    
    API: POST /gwp/v3/rtc/device/setconfig/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/setconfig/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)
    
    name_map = {
        "single": "AVEnc.VideoWidget",
        "multi": "AVEnc.VideoOSD"
    }
    
    body = {
        "Name": name_map.get(config_type, "AVEnc.VideoWidget")
    }
    
    if config_type == "single":
        body["AVEnc.VideoWidget"] = config
    else:
        body["AVEnc.VideoOSD"] = config
    
    if channel is not None:
        body["Channel"] = str(channel)
    
    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()
    
    if result.get("code") != 2000:
        raise RuntimeError(f"设置 OSD 配置失败:{result.get('msg', '未知错误')}")
    
    return result.get("data", {})


def pixel_to_osd_coord(pixel: int, total_pixels: int = 1920) -> int:
    """像素坐标转换为 OSD 坐标(0-8192 范围)"""
    return int((pixel / total_pixels) * 8192)


def rgba_to_hex(r: int, g: int, b: int, a: int = 255) -> str:
    """RGBA 转换为十六进制颜色字符串"""
    return f"0x{r:02X}{g:02X}{b:02X}{a:02X}"


# ============== 动作处理函数 ==============

def get_single_action(args: argparse.Namespace) -> int:
    """执行获取单行 OSD 配置操作"""
    try:
        print(f"正在获取设备 {args.device_sn} 的单行 OSD 配置...")
        
        result = get_osd_config(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            config_type="single",
            channel=args.channel
        )
        
        print()
        print("✅ 获取成功")
        print()
        print("📋 OSD 配置详情:")
        print("-" * 60)
        
        # 打印配置详情
        print_config_details(result, "single")
        
        return 0
        
    except RuntimeError as e:
        print(f"❌ 错误:{e}", file=sys.stderr)
        return 1


def get_multi_action(args: argparse.Namespace) -> int:
    """执行获取多行 OSD 配置操作"""
    try:
        print(f"正在获取设备 {args.device_sn} 的多行 OSD 配置...")
        
        result = get_osd_config(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            config_type="multi",
            channel=args.channel
        )
        
        print()
        print("✅ 获取成功")
        print()
        print("📋 OSD 配置详情:")
        print("-" * 60)
        
        # 打印配置详情
        print_config_details(result, "multi")
        
        return 0
        
    except RuntimeError as e:
        print(f"❌ 错误:{e}", file=sys.stderr)
        return 1


def print_config_details(result: Dict[str, Any], config_type: str):
    """打印配置详情"""
    if config_type == "single":
        widget = result.get("AVEnc.VideoWidget", [])
        if widget:
            w = widget[0]
            
            # 通道标题
            channel_title = w.get("ChannelTitle", {})
            print(f"通道标题:{channel_title.get('Name', '-')}")
            
            # 通道标题属性
            title_attr = w.get("ChannelTitleAttribute", {})
            if title_attr:
                encode = "✅" if title_attr.get("EncodeBlend") else "❌"
                preview = "✅" if title_attr.get("PreviewBlend") else "❌"
                pos = title_attr.get("RelativePos", [])
                print(f"  编码叠加:{encode}  预览叠加:{preview}")
                if pos:
                    print(f"  位置:[{pos[0]}, {pos[1]}]")
            
            # 时间标题属性
            time_attr = w.get("TimeTitleAttribute", {})
            if time_attr:
                encode = "✅" if time_attr.get("EncodeBlend") else "❌"
                preview = "✅" if time_attr.get("PreviewBlend") else "❌"
                pos = time_attr.get("RelativePos", [])
                print(f"时间标题:编码叠加:{encode}  预览叠加:{preview}")
                if pos:
                    print(f"  位置:[{pos[0]}, {pos[1]}]")
            
            # 隐私区域
            covers = w.get("Covers", [])
            covers_num = w.get("CoversNum", 0)
            print(f"隐私区域:{covers_num} 个")
            for i, cover in enumerate(covers[:3], 1):
                pos = cover.get("RelativePos", [])
                if pos:
                    print(f"  区域{i}: [{pos[0]}, {pos[1]}]")
    else:
        # 多行配置
        print("多行 OSD 配置(详细配置请参考返回数据)")
        print(result)


def set_channel_title_action(args: argparse.Namespace) -> int:
    """执行设置通道标题操作"""
    try:
        enable = args.enable and not args.disable
        
        print(f"正在{'启用' if enable else '禁用'}通道标题...")
        print(f"   标题:{args.title}")
        
        # 先获取当前配置
        result = get_osd_config(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            config_type="single",
            channel=args.channel
        )
        
        widget = result.get("AVEnc.VideoWidget", [])
        if not widget:
            widget = [{}]
        
        # 更新通道标题
        if "ChannelTitle" not in widget[0]:
            widget[0]["ChannelTitle"] = {}
        
        widget[0]["ChannelTitle"]["Name"] = args.title
        widget[0]["ChannelTitle"]["SerialNo"] = args.device_sn
        
        # 更新属性
        if "ChannelTitleAttribute" not in widget[0]:
            widget[0]["ChannelTitleAttribute"] = {
                "BackColor": "0x00000000",
                "FrontColor": "0xFFFFFFFF",
                "EncodeBlend": enable,
                "PreviewBlend": enable,
                "RelativePos": [100, 50, 0, 0]
            }
        else:
            widget[0]["ChannelTitleAttribute"]["EncodeBlend"] = enable
            widget[0]["ChannelTitleAttribute"]["PreviewBlend"] = enable
        
        # 设置配置
        set_result = set_osd_config(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            config=widget,
            config_type="single",
            channel=args.channel
        )
        
        print()
        print("✅ 通道标题设置成功")
        print(f"   状态:{'启用' if enable else '禁用'}")
        
        return 0
        
    except RuntimeError as e:
        print(f"❌ 错误:{e}", file=sys.stderr)
        return 1


def main():
    """主函数"""
    parser = argparse.ArgumentParser(description="杰峰设备 OSD 水印设置技能")
    
    # 全局参数
    parser.add_argument("--action", required=True,
                        choices=["get-single", "get-multi", "set-channel-title"],
                        help="操作类型")
    parser.add_argument("--uuid", default=os.getenv("JF_UUID"),
                        help="开放平台用户 uuid")
    parser.add_argument("--app-key", default=os.getenv("JF_APP_KEY"),
                        help="应用 appKey")
    parser.add_argument("--app-secret", default=os.getenv("JF_APP_SECRET"),
                        help="应用密钥")
    parser.add_argument("--move-card", type=int, default=os.getenv("JF_MOVE_CARD", "2"),
                        help="移动卡标识")
    parser.add_argument("--device-sn", default=os.getenv("JF_DEVICE_SN"),
                        help="设备序列号")
    parser.add_argument("--device-token", default=os.getenv("JF_DEVICE_TOKEN"),
                        help="设备接口访问令牌")
    parser.add_argument("--channel", type=int, default=0,
                        help="通道号(默认 0)")
    
    # set-channel-title 参数
    parser.add_argument("--title",
                        help="通道标题文本")
    parser.add_argument("--enable", action="store_true",
                        help="启用通道标题")
    parser.add_argument("--disable", action="store_true",
                        help="禁用通道标题")
    
    args = parser.parse_args()
    
    # 验证必需参数
    if not args.uuid:
        print("❌ 错误:缺少 --uuid 或 JF_UUID 环境变量", file=sys.stderr)
        return 1
    if not args.app_key:
        print("❌ 错误:缺少 --app-key 或 JF_APP_KEY 环境变量", file=sys.stderr)
        return 1
    if not args.app_secret:
        print("❌ 错误:缺少 --app-secret 或 JF_APP_SECRET 环境变量", file=sys.stderr)
        return 1
    if not args.device_token:
        print("❌ 错误:缺少 --device-token 或 JF_DEVICE_TOKEN 环境变量", file=sys.stderr)
        return 1
    
    # 执行对应操作
    if args.action == "get-single":
        return get_single_action(args)
    elif args.action == "get-multi":
        return get_multi_action(args)
    elif args.action == "set-channel-title":
        return set_channel_title_action(args)
    else:
        print(f"❌ 未知操作:{args.action}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())