using System.Drawing;
using Dpz.Core.Shard.Service;
using Dpz.Core.Web.Models.Video;
namespace Dpz.Core.Web.Controllers;
public class VideoController(
IBarrageService barrageService,
ILogger<VideoController> logger,
IVideoShardService videoShardService,
IConfiguration configuration
) : Controller
{
private readonly Lazy<string> _lazyCdnHost = new(() =>
configuration["S3:CdnHost"]
?? throw new InvalidConfigurationException("S3:CdnHost is empty or null")
);
public async Task<IActionResult> Index()
{
this.SetTitle("视频");
var videos = await videoShardService.GetVideosAsync();
return View(videos);
}
[Route("video/watch/{id}.html")]
public async Task<IActionResult> Watch(string id)
{
var video = await videoShardService.GetVideoAsync(id);
if (video == null)
{
return NotFound();
}
var title = string.IsNullOrWhiteSpace(video.VideoTitle)
? video.Title ?? "未命名视频"
: video.VideoTitle;
var subtitle = string.IsNullOrWhiteSpace(video.SubTitle) ? video.Name : video.SubTitle;
var description = video.Description?.Trim();
var coverUrl = string.IsNullOrWhiteSpace(video.Cover)
? $"{_lazyCdnHost.Value}/Video/{video.Title}/video.webp"
: video.Cover;
var videoUrl = string.IsNullOrWhiteSpace(video.M3u8)
? $"{_lazyCdnHost.Value}/Video/{video.Title}/1080p.m3u8"
: video.M3u8;
var tags = video.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
var model = new VideoWatchModel(
subtitle ?? "",
description ?? "",
video.PlayCount,
video.DanmakuCount,
video.CommentCount,
tags,
new VideoConfig(video.Id ?? "", videoUrl, coverUrl, title)
);
this.SetTitle(video.VideoTitle ?? "");
return View(model);
}
[HttpPatch("play/{id}")]
public async Task<IActionResult> Play(string id)
{
await videoShardService.AddPlayAsync(id);
return NoContent();
}
[Route("history/chat/v3/"), HttpGet]
public async Task<IActionResult> HistoryChat(string id)
{
var dbDanmaku = await barrageService.GetGroupBarragesAsync(id);
var allDanmaku = dbDanmaku.Select(x =>
{
var result = int.TryParse(x.Color, out var color);
return new object[]
{
x.Time,
x.Position,
result ? color : x.Color ?? "#FFF",
"阿胖",
x.Text ?? "",
};
});
return Json(new { code = 0, data = allDanmaku });
}
[Route("history/danmaku/{id}"), HttpGet]
public async Task<IActionResult> HistoryDanmaku(string id)
{
var dbDanmaku = await barrageService.GetGroupBarragesAsync(id);
var allDanmaku = dbDanmaku
.OrderBy(x => x.Time)
.Select(x =>
{
var result = int.TryParse(x.Color, out var color);
var colorRgb = Color.FromArgb(color);
return new
{
color = result ? $"rgb({colorRgb.R},{colorRgb.G},{colorRgb.B})" : x.Color,
text = x.Text,
time = x.Time,
type = x.Position switch
{
0 => "scroll",
1 => "top",
2 => "bottom",
_ => "",
},
};
})
.ToList();
return Json(allDanmaku);
}
[Route("history/danmaku"), HttpPost]
public async Task<IActionResult> HistoryDanmaku([FromBody] VideoDanmaku2 danmaku)
{
if (string.IsNullOrWhiteSpace(danmaku.Text))
{
return Json(new ResultInfo(true));
}
var color = ColorTranslator.FromHtml("#FF0000");
var vmBarrage = new VmBarrage
{
Color = color.ToArgb().ToString(),
Group = danmaku.Id,
Position = danmaku.Type switch
{
"scroll" => 0,
"top" => 1,
"bottom" => 2,
_ => 0,
},
SendTime = DateTime.Now,
Size = 0,
Text = danmaku.Text,
Time = danmaku.Time,
};
await barrageService.AddBarrageAsync(vmBarrage);
return Json(new ResultInfo(true));
}
[Route("history/chat/v3/"), HttpPost]
public async Task<IActionResult> HistoryChat([FromBody] VideoDanmaku danmaku)
{
if (string.IsNullOrWhiteSpace(danmaku.Text))
{
return Json(new { code = 0, data = danmaku });
}
var vmBarrage = new VmBarrage
{
Color = danmaku.Color.ToString(),
Group = danmaku.Id,
Position = (int)danmaku.Type,
SendTime = DateTime.Now,
Size = 0,
Text = danmaku.Text,
Time = danmaku.Time,
};
await barrageService.AddBarrageAsync(vmBarrage);
return Json(new { code = 0, data = danmaku });
}
[Route("history/danmaku/v2/{id}"), HttpGet]
public async Task<IActionResult> HistoryDanmakuV2(string id)
{
var dbDanmaku = await barrageService.GetGroupBarragesAsync(id);
var data = dbDanmaku
.Select(x =>
{
var result = int.TryParse(x.Color, out var color);
var colorRgb = Color.FromArgb(color);
return new
{
text = x.Text,
time = x.Time,
color = result ? $"rgb({colorRgb.R},{colorRgb.G},{colorRgb.B})" : x.Color,
border = false,
mode = x.Position switch
{
0 => 0,
1 => 1,
2 => 1,
_ => 0,
},
};
})
.ToList();
return Json(data);
}
[Route("send/danmaku/v2"), HttpPost]
public async Task<IActionResult> SendDanmaku([FromBody] SendDanmaku danmaku)
{
if (!ModelState.IsValid)
{
return Json(new ResultInfo(false, ModelState));
}
Color color;
try
{
color = ColorTranslator.FromHtml(danmaku.Color);
}
catch (Exception e)
{
color = ColorTranslator.FromHtml("#FFFFFF");
logger.LogError(e, "color translator fail,in color:{Color}", danmaku.Color);
}
var vmBarrage = new VmBarrage
{
Color = color.ToArgb().ToString(),
Group = danmaku.Id,
Position = danmaku.Mode,
SendTime = DateTime.Now,
Size = 0,
Text = danmaku.Text,
Time = danmaku.Time,
};
await barrageService.AddBarrageAsync(vmBarrage);
return Json(new ResultInfo(true));
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
VideoController.cs 解析报告
总体定位
VideoController.cs 是「叫我阿胖」网站中负责视频模块的 ASP.NET Core MVC 控制器。它同时承担两类职责:
- 页面渲染:视频列表页、视频播放页(MVC View)。
- 弹幕(danmaku/barrage)数据接口:为页面上的播放器提供历史弹幕拉取与发送的多种版本 JSON API(兼容不同前端/播放器实现)。
控制器使用 C# 12 的主构造函数注入依赖,继承 Controller,整体没有类级路由前缀,各 Action 通过特性自定义路由。
依赖与字段
通过主构造函数注入(来自 using 与外部命名空间,均已通过工具确认):
| 依赖 | 命名空间/文件 | 用途 |
|---|---|---|
IBarrageService | IBarrageService.cs | 弹幕的读取(GetGroupBarragesAsync)与写入(AddBarrageAsync) |
ILogger<VideoController> | 框架 | 颜色解析异常时记日志 |
IVideoShardService | IVideoShardService.cs | 视频元数据读取(列表/单个/播放数自增) |
IConfiguration | 框架 | 读取 CDN 配置 |
成员字段 _lazyCdnHost 是 Lazy<string>,首次访问时才读取配置键 S3:CdnHost;缺失或为空时抛出 InvalidConfigurationException.cs(该类由我通过搜索确认存在,具体行为以文件为准)。CDN 主机用于拼接视频默认封面、m3u8 播放地址。
各 Action 职责
页面类
Index():调用videoShardService.GetVideosAsync()取视频列表,经扩展方法SetTitle(定义在 WebToolsExtensions.cs,向ViewData["Title"]写入"{标题} - 叫我阿胖")设置标题「视频」,返回列表 View。Watch(string id)(路由video/watch/{id}.html):- 按 id 取视频,不存在返回 404。
- 对字段做降级处理:主标题取
VideoTitle→Title→「未命名视频」;副标题取SubTitle→Name;封面缺省时拼接{CDN}/Video/{视频目录名}/video.webp;m3u8 缺省时拼接.../1080p.m3u8(说明默认存储目录约定为视频Title下的固定文件名,且视频存放在 S3 兼容的对象存储上,播放经 CDN 分发)。 - 过滤掉空白标签后,组装成
record VideoWatchModel(见 VideoWatchModel.cs,内含VideoConfig,并提供序列化好的ConfigJson供页面 JS 直接使用),返回播放页 View。
Play(string id)(HttpPatch play/{id}):videoShardService.AddPlayAsync(id)自增播放数,返回 204。
弹幕读取接口(GET)
三条 GET 接口都从 barrageService.GetGroupBarragesAsync(id) 拉取同一组弹幕,但序列化格式完全不同,以兼容不同版本的播放器协议:
HistoryChat(history/chat/v3/):返回{code:0, data:[[time, position, color, "阿胖", text]…]}。其中每行是一个匿名 object 数组(DPlayer 聊天弹幕风格);发送者固定为「阿胖」;颜色若为可解析整数则原样给出 ARGB 整数值,否则用原始十六进制字符串兜底。HistoryDanmaku(history/danmaku/{id}):按Time升序,颜色统一转成rgb(r,g,b)字符串;Position(0=滚动/1=顶部/2=底部,见 VmBarrage.cs 注释)映射为scroll/top/bottom。HistoryDanmakuV2(history/danmaku/v2/{id}):颜色同样转rgb(),新增border:false字段,并把位置归一成mode:0→滚动,1/2→1(顶部、底部都按静止处理,符合 SendDanmaku.cs 中「mode 0-滚动,1-静止」的定义)。
三处都用 System.Drawing.Color.FromArgb(color) 把数据库里存的 ARGB 整数还原成颜色再取 R/G/B。
弹幕发送接口(POST)
HistoryDanmaku(history/danmaku,请求体 VideoDanmaku2.cs):文本为空直接返回成功占位。注意一个可疑点:虽然模型携带Color字段,但实现并未使用它,而是硬编码ColorTranslator.FromHtml("#FF0000")(红色)再转 ARGB 存储;Type字符串scroll/top/bottom被映射为Position0/1/2。HistoryChat(history/chat/v3/,请求体 VideoDanmaku.cs):Color是int,直接.ToString()存入;Position取(int)danmaku.Type(对应枚举 DanmakuType.cs:Rolling=0/Top=1/Bottom=2)。SendDanmaku(send/danmaku/v2,请求体 SendDanmaku.cs):最完整的入口——先做ModelState校验(含MaxLength),失败返回 ResultInfo.cs 包装的错误;再用ColorTranslator.FromHtml解析前端传来的十六进制颜色,解析异常时降级为白色#FFFFFF并LogError;Position = danmaku.Mode。
所有 POST 最终统一构建 VmBarrage(Color 为 ARGB 整数串、Group=视频 id、SendTime=当前时间、Size=0、Time=播放器时间点)交给 barrageService.AddBarrageAsync 持久化。
与相关文件的协作关系
- 视频元数据模型 VmVideo.cs:
Watch中读取的Title/Name/VideoTitle/SubTitle/Description/Tags/PlayCount/DanmakuCount/CommentCount/Cover/M3u8都来自它。 - 弹幕实体 VmBarrage.cs:三条发送接口统一写入、三条 GET 接口统一读取的对象。
- 请求/响应模型:
VideoDanmaku、VideoDanmaku2、SendDanmaku、DanmakuType位于 Models/Video;统一响应包装ResultInfo;页面模型VideoWatchModel。 - 扩展方法
SetTitle:位于 WebToolsExtensions.cs,供所有页面 Action 设置浏览器标题(拼上站点名「叫我阿胖」)。
关键流程示意
flowchart TD
subgraph 页面类
A1[GET /] --> A2[GetVideosAsync 列表] --> A3[SetTitle 视频] --> A4[View 列表页]
B1[GET video/watch/{id}.html] --> B2[GetVideoAsync]
B2 -- null --> B3[404]
B2 -- 有数据 --> B4[字段降级: 标题/封面/m3u8 兜底拼接 CDN] --> B5[构造 VideoWatchModel] --> B6[View 播放页]
C1[PATCH play/{id}] --> C2[AddPlayAsync 播放数+1] --> C3[204]
end
subgraph 弹幕 GET(读取同组 VmBarrage)
D1[GET history/chat/v3/] --> D2[DPlayer chat 数组格式 code:0]
E1[GET history/danmaku/{id}] --> E2[排序+rgb颜色+scroll/top/bottom]
F1[GET history/danmaku/v2/{id}] --> F2[rgb+border:false+mode 0/1]
end
subgraph 弹幕 POST(写 VmBarrage)
G1[POST history/danmaku] --> G2[颜色硬编码红/ Type串映射 Position]
H1[POST history/chat/v3/] --> H2[Color int 直接存/ Type枚举转 int]
I1[POST send/danmaku/v2] --> I2[ModelState校验] --> I3[FromHtml 解析颜色 异常兜底白并记日志] --> I4[Position=Mode]
G2 & H2 & I4 --> J[barrageService.AddBarrageAsync]
end
D2 & E2 & F2 --> K[barrageService.GetGroupBarragesAsync]
值得注意的实现细节
- 多套弹幕协议并存(chat v3、danmaku v1、danmaku v2),读取与写入各对应一套,说明站点需要兼容多种播放器/历史数据,这是"以兼容为目的"的遗留式设计。
POST history/danmaku忽略请求体中的 Color 字段、固定存红色,与其它入口不一致,疑似历史遗留或前端固定红色弹幕;前端VideoDanmaku2的 Color 在此完全未使用。- 数据库里
VmBarrage.Color实际存的是 ARGB 整数的字符串(ToArgb().ToString()),但读取端同时兼容「整数串→Color.FromArgb→rgb()」和「原始十六进制字符串」两种形态,说明库中存在新老两种颜色数据。 HistoryChat返回数组中出现固定作者「阿胖」以及Position、颜色整数值直接透传,属于面向特定播放器(DPlayer 风格 chat 弹幕)的协议格式。- 所有弹幕接口都没有做身份校验/发言频率限制(仅
SendDanmaku做了ModelState校验),权限控制不在本控制器职责范围内。
(说明:InvalidConfigurationException 与 SetTitle 行为已经由源码确认;VideoShardService 的内部实现细节本次未读取,视频元数据的具体持久化来源不展开。)
AI 正在分析代码…
评论加载中...