using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using Dpz.Core.Entity.Base;
using Dpz.Core.Infrastructure;
using Dpz.Core.Public.ViewModel.Response;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Dpz.Core.Service.ObjectStorage.Services.Impl;

public class VideoCloudService : IVideoCloudService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<VideoCloudService> _logger;

    private readonly UpyunOperator _upyunOperator;

    public VideoCloudService(
        HttpClient httpClient,
        IConfiguration configuration,
        ILogger<VideoCloudService> logger
    )
    {
        _httpClient = httpClient;
        _logger = logger;

        var upyunOperator = configuration.GetSection("upyun").Get<UpyunOperator>();
        _upyunOperator =
            upyunOperator
            ?? throw new BusinessException("configuration error,need upyun config node.");
    }

    public async Task<ResponseResult<string?>> VideoScreenshotAsync(
        string pathToFile,
        TimeSpan time
    )
    {
        var result = new ResponseResult<string?>();
        if (string.IsNullOrEmpty(pathToFile))
        {
            throw new ArgumentNullException(nameof(pathToFile));
        }

        var parentPath = pathToFile[..pathToFile.LastIndexOf('/')];

        var request = new HttpRequestMessage(HttpMethod.Post, $"{_upyunOperator.Bucket}/snapshot");
        await request.SignatureAsync(_upyunOperator);
        request.Content = JsonContent.Create(
            new
            {
                source = pathToFile,
                save_as = $"{parentPath}/video.webp",
                point = $"{time.TotalHours:00}:{time.Minutes:00}:{time.Seconds:00}",
                format = "webp",
            }
        );

        try
        {
            var response = await _httpClient.SendAsync(request);
            int? statusCode = (int)response.StatusCode;
            if (response.IsSuccessStatusCode)
            {
                var json = await response.Content.ReadAsStringAsync();
#if DEBUG
                Console.WriteLine(json);
#endif
                var root = JsonNode.Parse(json);
                statusCode = root?["status_code"]?.GetValue<int>();
                if (statusCode == 200)
                {
                    var savePath = root?["save_as"]?.GetValue<string>();
                    if (string.IsNullOrEmpty(savePath))
                    {
                        return result.FailResult("没有获取到缩略图地址");
                    }

                    return result.SuccessResult(_upyunOperator.Host + savePath);
                }
            }
            return result.FailResult($"设置缩略图失败,响应码:{statusCode}");
        }
        catch (Exception e)
        {
            _logger.LogError(e, "screenshot fail");
            return result.FailResult($"截图失败:{e.Message}");
        }
    }

    public async Task<ResponseResult<VideoMetaDataResponse?>> GetVideoMetaAsync(string pathToFile)
    {
        var result = new ResponseResult<VideoMetaDataResponse?>();

        if (string.IsNullOrEmpty(pathToFile))
        {
            throw new ArgumentNullException(nameof(pathToFile));
        }

        var request = new HttpRequestMessage(
            HttpMethod.Post,
            $"{_upyunOperator.Bucket}/m3u8er/get_meta"
        );
        await request.SignatureAsync(_upyunOperator);
        request.Content = JsonContent.Create(new { m3u8 = pathToFile });

        try
        {
            var response = await _httpClient.SendAsync(request);
            if (!response.IsSuccessStatusCode)
            {
                _logger.LogError(
                    "get video meta fail,response status code:{StatusCode}",
                    response.StatusCode
                );
                return result.FailResult($"获取视频元数据失败,响应状态码:{response.StatusCode}");
            }

            var json = await response.Content.ReadAsStringAsync();

            var root = JsonNode.Parse(json);
            var data = root?["meta"]?.Deserialize(
                typeof(VideoMetaDataResponse),
                new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }
            );
            if (data is not VideoMetaDataResponse meta)
            {
                return result.FailResult("获取视频元数据失败");
            }

            return result.SuccessResult(meta);
        }
        catch (Exception e)
        {
            _logger.LogError(e, "get video meta fail");
            return result.FailResult($"获取视频元数据失败:{e.Message}");
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这段代码是一个与又拍云(UpYun)对象存储/视频服务交互的服务实现类 VideoCloudService,负责调用又拍云的 API 来对存储在又拍云上的视频做截图和获取视频元数据。下面分部分解释其作用与流程。

总体结构与依赖

  • 命名空间:Dpz.Core.Service.ObjectStorage.Services.Impl,类实现接口 IVideoCloudService(接口定义未给出)。
  • 主要成员:
    • _httpClient:用于发送 HTTP 请求(通过依赖注入注入)。
    • _logger:用于记录日志。
    • _upyunOperator:从配置中读取的又拍云相关配置(类型 UpyunOperator),包含 Bucket、Host 等(具体字段根据 UpyunOperator 定义)。
  • 构造函数:从 IConfiguration 的 "upyun" 节点反序列化 UpyunOperator,若不存在则抛出 BusinessException,保证配置存在。

VideoScreenshotAsync(string pathToFile, TimeSpan time)

  • 作用:对给定路径的远程视频在指定时间点截取一帧并保存为 webp 缩略图,返回生成图片的访问 URL(带上又拍云的 Host 前缀)。
  • 校验:pathToFile 不能为空,否则抛 ArgumentNullException。
  • 逻辑:
    1. 计算 parentPath = pathToFile 去掉最后一个 '/' 之后的部分,用于生成保存路径 save_as(保存为 parentPath/video.webp)。
    2. 构造 POST 请求到 {_upyunOperator.Bucket}/snapshot(又拍云 snapshot API)。
    3. 调用 await request.SignatureAsync(_upyunOperator) 给请求签名(Sign 方法在代码中未展示,是扩展方法或工具方法,用于添加认证头)。
    4. 请求体为 JSON,包含 source(源视频路径)、save_as(保存路径)、point(时间点字符串,格式如 "HH:mm:ss")、format("webp")。
    5. 发送请求并读取响应:
      • 如果响应成功,解析响应 JSON,读取 status_code 字段;若为 200,读取 save_as 字段并返回成功的 URL(_upyunOperator.Host + save_as)。
      • 否则返回失败消息,包含响应码或错误信息。
  • 异常处理:捕获 Exception,记录日志并返回失败结果。
  • 返回值:ResponseResult<string?>,封装成功/失败信息与数据(成功时 data 为完整图像 URL)。

GetVideoMetaAsync(string pathToFile)

  • 作用:调用又拍云 m3u8 元数据接口来获取视频的元数据信息(返回 VideoMetaDataResponse 对象)。
  • 校验:pathToFile 不能为空,否则抛 ArgumentNullException。
  • 逻辑:
    1. 构造 POST 请求到 {_upyunOperator.Bucket}/m3u8er/get_meta。
    2. 给请求签名并设置 JSON body:{ m3u8 = pathToFile }。
    3. 发送请求,若 HTTP 状态非成功,记录日志并返回失败。
    4. 读取响应 JSON,解析 root["meta"] 并用 System.Text.Json 反序列化成 VideoMetaDataResponse(使用驼峰命名策略)。
    5. 如果反序列化成功则返回封装的成功结果,否则返回失败。
  • 异常处理:捕获 Exception,记录日志并返回失败结果。
  • 返回值:ResponseResult<VideoMetaDataResponse?>。

其它要点与注意事项

  • ResponseResult:一个自定义的响应封装类型(未给出实现),用于统一返回成功/失败状态和消息。
  • request.SignatureAsync(...):代码中调用该方法给请求签名,签名逻辑在本片段中未展示,是又拍云认证所必需的。
  • JSON 解析采用 System.Text.Json / JsonNode。
  • 日志:在失败或异常时通过 ILogger 记录错误。
  • 可能的边界/改进点:
    • parentPath 的计算依赖于 pathToFile 中包含 '/';若没有会抛出 IndexOf 相关异常,应考虑处理单一文件名或最后一个 '/' 未找到的情况。
    • 时间点格式使用 $"{time.TotalHours:00}:{time.Minutes:00}:{time.Seconds:00}",TotalHours 可能产生两位以上的小时数(例如 25 小时),根据 API 要求可能需要用 time.Hours 或自行格式化 TimeSpan 为 hh:mm:ss。
    • 对响应 JSON 的字段访问比较宽松(使用 JsonNode),若 API 返回结构变化会返回失败信息,已通过日志记录错误。
    • 未显示请求超时、重试等网络鲁棒性处理,这些可根据需求补充。

总结

  • 这个类封装了两个与又拍云视频相关的功能:截图(VideoScreenshotAsync)和获取视频元数据(GetVideoMetaAsync),通过 HttpClient 调用又拍云接口,基于配置进行签名与主机拼接,并使用 ResponseResult 统一返回结果与错误信息,同时有基本的日志与异常捕获。
评论加载中...