【剪映小助手源码精讲】20_视频添加服务

第二十章:视频添加服务

概述

视频添加服务是剪映小助手的核心功能模块,负责将外部视频文件批量添加到剪映草稿中。该服务通过统一的接口封装了复杂的视频处理流程,包括文件下载、格式验证、轨道创建、片段添加等关键步骤,为上层业务提供了简洁可靠的视频添加能力。

核心功能

1. 批量视频添加

视频添加服务支持批量添加多个视频文件到剪映草稿中,每个视频都可以独立配置各种参数。

核心实现
python 复制代码
def add_videos(
    draft_url: str, 
    video_infos: str,
    alpha: float = 1.0, 
    scale_x: float = 1.0, 
    scale_y: float = 1.0, 
    transform_x: int = 0, 
    transform_y: int = 0
) -> Tuple[str, str, List[str], List[str]]:
    """
    添加视频到剪映草稿的业务逻辑
    
    Args:
        draft_url: 草稿URL
        video_infos: 视频信息列表的JSON字符串
        alpha: 全局透明度[0, 1],默认值为1.0
        scale_x: X轴缩放比例,默认值为1.0
        scale_y: Y轴缩放比例,默认值为1.0
        transform_x: X轴位置偏移(像素),默认值为0
        transform_y: Y轴位置偏移(像素),默认值为0
    
    Returns:
        draft_url: 草稿URL
        track_id: 轨道ID
        video_ids: 视频ID列表
        segment_ids: 片段ID列表

    Raises:
        CustomException: 视频批量添加失败
    """
    logger.info(f"add_videos, draft_url: {draft_url}, video_infos: {video_infos}, alpha: {alpha}, scale_x: {scale_x}, scale_y: {scale_y}, transform_x: {transform_x}, transform_y: {transform_y}")

    # 1. 提取草稿ID
    draft_id = helper.get_url_param(draft_url, "draft_id")
    if (not draft_id) or (draft_id not in DRAFT_CACHE):
        raise CustomException(CustomError.INVALID_DRAFT_URL)

    # 2. 创建保存视频资源的目录
    draft_dir = os.path.join(config.DRAFT_DIR, draft_id)
    draft_video_dir = os.path.join(draft_dir, "assets", "videos")
    os.makedirs(name=draft_video_dir, exist_ok=True)

    # 3. 解析视频信息
    videos = parse_video_data(json_str=video_infos)
    if len(videos) == 0:
        logger.info(f"No video info, draft_id: {draft_id}")
        raise CustomException(CustomError.INVALID_VIDEO_INFO)

    # 4. 从缓存中获取草稿
    script: ScriptFile = DRAFT_CACHE[draft_id]

    # 5. 添加视频轨道
    track_name = f"video_track_{helper.gen_unique_id()}"
    script.add_track(track_type=draft.TrackType.video, track_name=track_name)

    # 6. 遍历视频信息,添加视频到草稿中的指定轨道,收集片段ID
    segment_ids = []
    for video in videos:
        segment_id = add_video_to_draft(script, track_name, draft_video_dir=draft_video_dir, video=video)
        segment_ids.append(segment_id)
    logger.info(f"segment_ids: {segment_ids}")

    # 7. 保存草稿
    script.save()

    # 获取当前视频轨道id
    track_id = ""
    for key in script.tracks.keys():
        if script.tracks[key].name == track_name:
            track_id = script.tracks[key].track_id
            break
    logger.info(f"draft_id: {draft_id}, track_id: {track_id}")

    # 获取当前所有视频资源ID(全局唯一ID)
    video_ids = [video.material_id for video in script.materials.videos if video.material_type == "video"]
    logger.info(f"draft_id: {draft_id}, video_ids: {video_ids}")

    return draft_url, track_id, video_ids, segment_ids
处理流程
  1. 参数验证:验证草稿URL和缓存状态
  2. 目录创建:创建视频资源存储目录
  3. 数据解析:解析和验证视频信息JSON
  4. 轨道创建:添加新的视频轨道
  5. 批量添加:遍历添加每个视频到轨道
  6. 草稿保存:持久化草稿更改
  7. 信息返回:返回轨道ID、视频ID和片段ID列表

2. 单个视频添加

add_video_to_draft函数负责将单个视频添加到指定的轨道中。

核心实现
python 复制代码
def add_video_to_draft(
    script: ScriptFile,
    track_name: str,
    draft_video_dir: str,
    video: dict, 
    alpha: float = 1.0, 
    scale_x: float = 1.0, 
    scale_y: float = 1.0, 
    transform_x: int = 0, 
    transform_y: int = 0
) -> str:
    """
    向剪映草稿中添加视频
    
    Args:
        script: 草稿脚本对象
        track_name: 轨道名称
        draft_video_dir: 视频资源目录
        video: 视频信息字典
        alpha: 视频透明度
        scale_x: 横向缩放
        scale_y: 纵向缩放
        transform_x: X轴位置偏移(像素)
        transform_y: Y轴位置偏移(像素)       
    
    Returns:
        material_id: 素材ID
    
    Raises:
        CustomException: 视频添加失败
    """
    try:
        # 0. 下载视频
        video_path = helper.download(url=video['video_url'], save_dir=draft_video_dir)

        # 1. 创建视频素材并添加到草稿
        duration = video['end'] - video['start']
        video_segment = draft.VideoSegment(
            material=video_path, 
            target_timerange=trange(start=video['start'], duration=duration),
            volume=video['volume']
        )
        logger.info(f"material_id: {video_segment.material_instance.material_id}")
        logger.info(f"video_path: {video_path}, start: {video['start']}, duration: {duration}, volume: {video['volume']}")

        # 2. 向指定轨道添加片段
        script.add_segment(video_segment, track_name)

        return video_segment.material_instance.material_id
    except CustomException:
        logger.info(f"Add video to draft failed, draft_video_dir: {draft_video_dir}, video: {video}")
        raise
    except Exception as e:
        logger.error(f"Add video to draft failed, error: {str(e)}")
        raise CustomException(err=CustomError.VIDEO_ADD_FAILED)
关键步骤
  1. 文件下载:从URL下载视频文件到本地目录
  2. 素材创建:创建VideoSegment对象,设置时间范围和音量
  3. 片段添加:将视频片段添加到指定轨道
  4. ID返回:返回素材的唯一标识符

3. 视频数据解析

parse_video_data函数负责解析和验证客户端提交的视频信息JSON字符串。

核心实现
python 复制代码
def parse_video_data(json_str: str) -> List[Dict[str, Any]]:
    """
    解析视频数据的JSON字符串,处理可选字段的默认值
    
    Args:
        json_str: 包含视频数据的JSON字符串,格式如下:
        [ 
            {
                "video_url": "https://example.com/video1.mp4", // [必选] 视频文件的URL地址
                "width": 1920, // [必选] 视频宽度 
                "height": 1080, // [必选] 视频高度 
                "start": 0.0, // [必选] 视频在时间轴上的开始时间 
                "end": 12000000.0, // [必选] 视频在时间轴上的结束时间 
                "duration": 12000000.0, // [必选] 视频总时长(微秒)
                "mask": "", // 遮罩类型[可选],默认值为None
                "transition": "", // 转场效果名称[可选],默认值为None
                "transition_duration": 500000.0, // 转场持续时间(微秒)[可选],默认值为500000
                "volume": 1.0, // 音量大小[0, 1][可选],默认值为1.0
            } 
        ]
        
    Returns:
        包含视频对象的数组,每个对象都处理了默认值
        
    Raises:
        json.JSONDecodeError: 当JSON格式错误时抛出
        KeyError: 当缺少必选字段时抛出
        CustomException: 自定义异常
    """
    try:
        # 解析JSON字符串
        data = json.loads(json_str)
    except json.JSONDecodeError as e:
        raise CustomException(CustomError.INVALID_VIDEO_INFO, f"JSON parse error: {e.msg}")
    
    # 确保输入是列表
    if not isinstance(data, list):
        raise CustomException(CustomError.INVALID_VIDEO_INFO, "video_infos should be a list")
    
    result = []
    
    for i, item in enumerate(data):
        if not isinstance(item, dict):
            raise CustomException(CustomError.INVALID_VIDEO_INFO, f"the {i}th item should be a dict")
        
        # 检查必选字段
        required_fields = ["video_url", "width", "height", "start", "end", "duration"]
        missing_fields = [field for field in required_fields if field not in item]
        
        if missing_fields:
            raise CustomException(CustomError.INVALID_VIDEO_INFO, f"the {i}th item is missing required fields: {', '.join(missing_fields)}")
        
        # 创建处理后的对象,设置默认值
        processed_item = {
            "video_url": item["video_url"],
            "width": item["width"],
            "height": item["height"],
            "start": item["start"],
            "end": item["end"],
            "duration": item["duration"],
            "mask": item.get("mask", None),  # 默认值 None
            "transition": item.get("transition", None),  # 默认值 None
            "transition_duration": item.get("transition_duration", 500000),  # 默认值 500000
            "volume": item.get("volume", 1.0)  # 默认值 1.0
        }
        
        # 验证数值范围
        if processed_item["volume"] < 0 or processed_item["volume"] > 1:
            # 音量值必须在[0, 1]范围内,给默认值
            processed_item["volume"] = 1.0
        
        if processed_item["transition_duration"] < 0:
            # 转场持续时间必须为非负数,给默认值
            processed_item["transition_duration"] = 500000
        
        result.append(processed_item)
    
    return result
数据验证
  • JSON格式验证:确保输入是有效的JSON字符串
  • 数据结构验证:验证输入必须是列表类型
  • 字段完整性检查:检查所有必选字段是否存在
  • 数据类型验证:确保每个视频项都是字典类型
  • 数值范围验证:验证音量和转场持续时间的有效范围
  • 默认值处理:为可选字段提供合理的默认值

视频参数配置

基础参数

参数名 类型 必选 默认值 说明
video_url string - 视频文件的URL地址
width int - 视频宽度(像素)
height int - 视频高度(像素)
start float - 视频在时间轴上的开始时间(微秒)
end float - 视频在时间轴上的结束时间(微秒)
duration float - 视频总时长(微秒)

高级参数

参数名 类型 必选 默认值 说明
mask string None 遮罩类型
transition string None 转场效果名称
transition_duration float 500000 转场持续时间(微秒)
volume float 1.0 音量大小[0, 1]

变换参数

参数名 类型 必选 默认值 说明
alpha float 1.0 全局透明度[0, 1]
scale_x float 1.0 X轴缩放比例
scale_y float 1.0 Y轴缩放比例
transform_x int 0 X轴位置偏移(像素)
transform_y int 0 Y轴位置偏移(像素)

缓存集成

视频添加服务深度集成了草稿缓存机制,通过DRAFT_CACHE实现高效的草稿数据管理。

缓存使用

python 复制代码
# 验证草稿缓存状态
if (not draft_id) or (draft_id not in DRAFT_CACHE):
    raise CustomException(CustomError.INVALID_DRAFT_URL)

# 从缓存获取草稿对象
script: ScriptFile = DRAFT_CACHE[draft_id]

# 操作完成后自动更新缓存(无需手动更新)
script.save()

缓存优势

  • 性能提升:避免重复的文件系统操作
  • 数据一致性:确保草稿数据的实时同步
  • 内存管理:通过LRU策略防止内存溢出

错误处理

视频添加服务采用统一的异常处理机制,通过CustomExceptionCustomError定义标准化的错误类型。

异常类型

异常类型 错误码 说明
INVALID_DRAFT_URL 1001 无效的草稿URL
INVALID_VIDEO_INFO 1002 无效的视频信息
VIDEO_ADD_FAILED 1003 视频添加失败
DOWNLOAD_FAILED 1004 文件下载失败

异常处理策略

python 复制代码
try:
    # 核心业务逻辑
    video_path = helper.download(url=video['video_url'], save_dir=draft_video_dir)
except CustomException:
    # 自定义异常直接抛出
    logger.info(f"Add video to draft failed, draft_video_dir: {draft_video_dir}, video: {video}")
    raise
except Exception as e:
    # 未知异常转换为业务异常
    logger.error(f"Add video to draft failed, error: {str(e)}")
    raise CustomException(err=CustomError.VIDEO_ADD_FAILED)

日志记录

视频添加服务通过结构化的日志记录机制,详细记录每个关键步骤的执行情况。

日志级别

  • INFO:记录关键业务操作信息
  • ERROR:记录异常和错误信息
  • DEBUG:记录详细的调试信息(开发模式)

日志内容

python 复制代码
# 请求参数日志
logger.info(f"add_videos, draft_url: {draft_url}, video_infos: {video_infos}, alpha: {alpha}")

# 关键步骤日志
logger.info(f"segment_ids: {segment_ids}")
logger.info(f"draft_id: {draft_id}, track_id: {track_id}")
logger.info(f"draft_id: {draft_id}, video_ids: {video_ids}")

# 错误日志
logger.info(f"No video info, draft_id: {draft_id}")
logger.error(f"Add video to draft failed, error: {str(e)}")

性能优化

批量处理优化

  • 批量验证:一次性验证所有视频数据的有效性
  • 批量添加:顺序添加视频片段,确保时间轴正确性
  • 单次保存:所有操作完成后统一保存草稿

文件管理优化

  • 目录复用:重复使用已存在的资源目录
  • 文件名优化:使用唯一标识符避免文件名冲突
  • 下载优化:支持断点续传和并发下载(底层实现)

内存管理优化

  • 流式处理:避免一次性加载大文件到内存
  • 缓存控制:通过LRU策略控制缓存大小
  • 资源清理:及时释放不再使用的资源

安全性考虑

输入验证

  • URL验证:验证视频URL的合法性和可访问性
  • 参数验证:严格验证所有输入参数的类型和范围
  • 路径检查:防止路径穿越和目录遍历攻击

数据保护

  • 异常脱敏:避免在异常信息中暴露敏感数据
  • 日志脱敏:在日志中脱敏处理URL和文件路径
  • 访问控制:通过权限控制防止未授权访问

扩展性设计

参数扩展

视频参数设计支持灵活的扩展,可以轻松添加新的视频属性:

python 复制代码
# 向后兼容的参数设计
processed_item = {
    # 基础参数(必须)
    "video_url": item["video_url"],
    "width": item["width"],
    "height": item["height"],
    # 可选参数(有默认值)
    "mask": item.get("mask", None),
    "transition": item.get("transition", None),
    # 新参数(向后兼容)
    "new_feature": item.get("new_feature", default_value),
}

功能扩展

  • 特效支持:支持添加视频特效和滤镜
  • 转场支持:支持视频间的转场效果
  • 动画支持:支持视频动画和关键帧

最佳实践

使用示例

python 复制代码
# 准备视频信息
video_infos = [
    {
        "video_url": "https://example.com/video1.mp4",
        "width": 1920,
        "height": 1080,
        "start": 0.0,
        "end": 12000000.0,
        "duration": 12000000.0,
        "volume": 1.0
    },
    {
        "video_url": "https://example.com/video2.mp4",
        "width": 1920,
        "height": 1080,
        "start": 12000000.0,
        "end": 24000000.0,
        "duration": 12000000.0,
        "transition": "fade",
        "transition_duration": 1000000.0
    }
]

# 调用视频添加服务
draft_url, track_id, video_ids, segment_ids = add_videos(
    draft_url="http://localhost:8080/draft?draft_id=123",
    video_infos=json.dumps(video_infos),
    alpha=1.0,
    scale_x=1.0,
    scale_y=1.0
)

注意事项

  1. 时间轴规划:确保视频的时间范围不重叠或按预期重叠
  2. 格式兼容性:使用剪映支持的视频格式(MP4、MOV、AVI等)
  3. 性能考虑:批量添加大量视频时考虑分批处理
  4. 错误处理:妥善处理网络异常和文件下载失败
  5. 缓存管理:合理使用缓存,避免内存溢出

总结

视频添加服务通过统一的接口封装了复杂的视频处理流程,提供了批量添加、参数验证、错误处理、日志记录等核心功能。服务深度集成了缓存机制和异常处理,确保了数据的一致性、可靠性和可追溯性。通过合理的性能优化、安全性考虑和扩展性设计,视频添加服务为剪映小助手提供了稳定可靠的视频添加能力。


相关资源

相关推荐
IT·小灰灰35 分钟前
探索即梦生图AI与AI Ping平台的创新融合:技术实践与代码实现
人工智能·python
deephub38 分钟前
CALM自编码器:用连续向量替代离散token,生成效率提升4倍
人工智能·python·大语言模型
94621931zyn63 小时前
关于应用 - Cordova 与 OpenHarmony 混合开发实战
笔记·python
知远同学8 小时前
Anaconda的安装使用(为python管理虚拟环境)
开发语言·python
Blossom.1188 小时前
AI编译器实战:从零手写算子融合与自动调度系统
人工智能·python·深度学习·机器学习·flask·transformer·tornado
热爱专研AI的学妹9 小时前
数眼搜索API与博查技术特性深度对比:实时性与数据完整性的核心差异
大数据·开发语言·数据库·人工智能·python
Mr_Chenph9 小时前
Miniconda3在Windows11上和本地Python共生
开发语言·python·miniconda3
智航GIS11 小时前
5.1 if语句基础
开发语言·python
华研前沿标杆游学11 小时前
2026年湖南省工业旅游线路
python
APIshop12 小时前
深入解析京东API接口:如何高效获取商品详情与SKU信息
python