净空形变视频监测后端

This commit is contained in:
何德超 2025-08-12 17:20:09 +08:00
parent f7b877aef8
commit 2708a02588
11 changed files with 751 additions and 0 deletions

View File

@ -0,0 +1,24 @@
package com.dite.znpt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* @author hedechao
* @date 2025/8/11 11:02
* @Description: 形变配置线程
*/
@Configuration
public class TaskConfig {
@Bean("clearanceExecutor") // 方法返回的对象会被注册成 Bean名字叫 clearanceExecutor
public ThreadPoolTaskExecutor clearanceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 核心线程数
executor.setMaxPoolSize(4); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("clearance-"); // 线程名前缀
executor.initialize(); // 初始化
return executor;
}
}

View File

@ -0,0 +1,170 @@
package com.dite.znpt.domain.entity;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
* 视频监测信息
*/
@ApiModel(description="视频监测信息")
@Schema(description="视频监测信息")
@Data
@TableName(value = "video_monitor")
public class VideoMonitorEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 视频id
*/
@TableId(value = "video_id", type = IdType.ASSIGN_UUID)
@ApiModelProperty(value="视频id")
@Schema(description="视频id")
private String videoId;
/**
* 项目id
*/
@TableField(value = "project_id")
@ApiModelProperty(value="项目id")
@Schema(description="项目id")
private String projectId;
/**
* 机组id
*/
@TableField(value = "turbine_id")
@ApiModelProperty(value="机组id")
@Schema(description="机组id")
private String turbineId;
/**
* 视频名称
*/
@TableField(value = "video_name")
@ApiModelProperty(value="视频名称")
@Schema(description="视频名称")
private String videoName;
/**
* 视频路径
*/
@TableField(value = "video_path")
@ApiModelProperty(value="视频路径")
@Schema(description="视频路径")
private String videoPath;
/**
* 0 正常 1 已删除
*/
@TableField(value = "is_deleted")
@ApiModelProperty(value="0 正常 1 已删除")
@Schema(description="0 正常 1 已删除")
private Boolean isDeleted;
/**
* 0 待审核 1 已上线 2 下线
*/
@TableField(value = "`status`")
@ApiModelProperty(value="0 待审核 1 已上线 2 下线")
@Schema(description="0 待审核 1 已上线 2 下线")
private Byte status;
/**
* 预处理后的视频路径
*/
@TableField(value = "pre_image_path")
@ApiModelProperty(value="预处理后的视频路径")
@Schema(description="预处理后的视频路径")
private String preImagePath;
/**
* 是否处理默认0
*/
@TableField(value = "pre_treatment")
@ApiModelProperty(value="是否处理默认0")
@Schema(description="是否处理默认0")
private Boolean preTreatment;
/**
* 修改人
*/
@TableField(value = "update_by")
@ApiModelProperty(value="修改人")
@Schema(description="修改人")
private String updateBy;
/**
* 创建时间
*/
@TableField(value = "create_time",fill = FieldFill.INSERT)
@ApiModelProperty(value="创建时间")
@Schema(description="创建时间")
private Date createTime;
/**
* 创建人
*/
@TableField(value = "create_by")
@ApiModelProperty(value="创建人")
@Schema(description="创建人")
private String createBy;
/**
* 修改时间
*/
@TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
@ApiModelProperty(value="修改时间")
@Schema(description="修改时间")
private Date updateTime;
/**
* 风速
*/
@TableField(value = "wind_speed")
@ApiModelProperty(value="风速")
@Schema(description="风速")
private String windSpeed;
/**
* 转速
*/
@TableField(value = "rpm")
@ApiModelProperty(value="转速")
@Schema(description="转速")
private String rpm;
/**
* 检测类型
*/
@TableField(value = "`type`")
@ApiModelProperty(value="检测类型")
@Schema(description="检测类型")
private String type;
/**
* 业务扩展字段
*/
@TableField(value = "extra",typeHandler = JacksonTypeHandler.class)
@ApiModelProperty(value="业务扩展字段")
@Schema(description="业务扩展字段")
private JSONObject extra;
/**
* 上传时间
*/
@TableField(value = "upload_time")
@ApiModelProperty(value="上传时间")
@Schema(description="上传时间")
private Date uploadTime;
}

View File

@ -0,0 +1,29 @@
package com.dite.znpt.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author hedechao
* @date 2025/8/8 09:10
* @Description:
*/
@Data
@ApiModel("视频列表查询实体")
public class VideoReq implements Serializable {
@Serial
private static final long serialVersionUID = 771014582625089979L;
@ApiModelProperty("项目id")
private String projectId;
@ApiModelProperty("视频类型")
private String[] type;
@ApiModelProperty("机组id")
private String turbineId;
@ApiModelProperty("是否已审核0未审核1已审核")
private Boolean State;
}

View File

@ -0,0 +1,45 @@
package com.dite.znpt.enums;
import cn.hutool.json.JSONObject;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@Getter
public enum VideoMonitorEnum {
CLEARANCE("clearance","净空监测"),
DEFORMATION("deformation","形变监测");
private final String code;
private final String desc;
VideoMonitorEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public static VideoMonitorEnum getByCode(String code) {
for (VideoMonitorEnum e : VideoMonitorEnum.values()) {
if (e.code.equals(code)) {
return e;
}
}
return null;
}
public static String getDescByCode(String code) {
VideoMonitorEnum e = getByCode(code);
return null == e ? null : e.desc;
}
public static List<JSONObject> listAll() {
List<JSONObject> list = new ArrayList<>(UserStatusEnum.values().length);
for (VideoMonitorEnum e : VideoMonitorEnum.values()) {
JSONObject jsonObject = new JSONObject();
jsonObject.set(e.code, e.desc);
list.add(jsonObject);
}
return list;
}
}

View File

@ -0,0 +1,12 @@
package com.dite.znpt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dite.znpt.domain.entity.VideoMonitorEntity;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface VideoMonitorEntityMapper extends BaseMapper<VideoMonitorEntity> {
List<VideoMonitorEntity> selectAllByProjectIdAndPartId(@Param("projectId") String projectId, @Param("partId") String partId);
int batchInsert(@Param("list") List<VideoMonitorEntity> list);
}

View File

@ -0,0 +1,60 @@
package com.dite.znpt.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.dite.znpt.domain.PageResult;
import com.dite.znpt.domain.entity.VideoMonitorEntity;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* <p>
* 视频信息 服务类
* </p>
*
* @author hdc
* @since 2025-08-07
*/
public interface VideoMonitorService extends IService<VideoMonitorEntity> {
/**
* 批量上传视频
*/
List<VideoMonitorEntity> uploadBatch(String projectId,
String partId,
String type,
MultipartFile[] files) throws IOException;
/**
* 单文件上传
*/
VideoMonitorEntity upload(String projectId,
String partId,
String type,
MultipartFile file) throws IOException;
/**
* 分页列表
*/
PageResult<VideoMonitorEntity> page(Integer pageNo,
Integer pageSize,
String projectId,
String partId);
/**
* 列表
*/
List<VideoMonitorEntity> list(String projectId, String partId);
/**
* 删除
*/
void delete(String videoId);
/**
* 下载/播放
*/
void download(String videoId, HttpServletResponse response) throws IOException;
}

View File

@ -0,0 +1,51 @@
package com.dite.znpt.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.dite.znpt.domain.entity.VideoMonitorEntity;
import com.dite.znpt.enums.FilePathEnum;
import com.dite.znpt.util.PythonUtil;
import lombok.RequiredArgsConstructor;
import java.io.File;
import java.nio.charset.StandardCharsets;
@RequiredArgsConstructor
public class ClearanceTask implements Runnable {
private final String videoAbsolutePath; // 上传后的完整磁盘路径
private final String outputDir; // 结果目录
private final String videoId; // 数据库主键用于更新状态
private final VideoMonitorServiceImpl service;
@Override
public void run() {
try {
// 1. 调用 Python阻塞但跑在子线程
PythonUtil.runClearance(videoAbsolutePath,outputDir);
// 2. 更新数据库status = 已完成 / 预处理成功
VideoMonitorEntity update = new VideoMonitorEntity();
update.setVideoId(videoId);
update.setPreTreatment(true); // 或自定义状态字段
update.setPreImagePath(FilePathEnum.VIDEO.getFileDownPath(outputDir));
File resultFile = new File(outputDir, "results.json");
if (!resultFile.exists()) {
throw new IllegalStateException("results.json 不存在");
}
String jsonStr = FileUtil.readString(resultFile, StandardCharsets.UTF_8);
// 3. 转成 hutool JSONObject对应 MySQL JSON 字段
JSONObject jsonObj = JSONUtil.parseObj(jsonStr);
update.setExtra(jsonObj);
service.updateById(update);
} catch (Exception e) {
// 失败时可将 status 置为失败
VideoMonitorEntity update = new VideoMonitorEntity();
update.setVideoId(videoId);
update.setStatus((byte) -1); // 自定义失败码
service.updateById(update);
}
}
}

View File

@ -0,0 +1,181 @@
package com.dite.znpt.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dite.znpt.constant.Message;
import com.dite.znpt.domain.PageResult;
import com.dite.znpt.domain.entity.VideoMonitorEntity;
import com.dite.znpt.enums.FilePathEnum;
import com.dite.znpt.enums.VideoMonitorEnum;
import com.dite.znpt.exception.ServiceException;
import com.dite.znpt.mapper.VideoMonitorEntityMapper;
import com.dite.znpt.service.ProjectService;
import com.dite.znpt.service.TurbineService;
import com.dite.znpt.service.VideoMonitorService;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.rmi.ServerException;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* <p>
* 视频信息 服务实现类
* </p>
*
* @author hdc
* @since 2025-08-07
*/
@Service
public class VideoMonitorServiceImpl extends ServiceImpl<VideoMonitorEntityMapper, VideoMonitorEntity> implements VideoMonitorService {
@Resource
private TurbineService turbineService;
@Autowired
private ProjectService projectService;
@Resource(name = "clearanceExecutor")
private ThreadPoolTaskExecutor clearanceExecutor;
@Override
@Transactional(rollbackFor = Exception.class)
public List<VideoMonitorEntity> uploadBatch(String projectId,
String turbineId,
String type,
MultipartFile[] files) throws IOException {
if (files == null || files.length == 0) {
throw new ServiceException("上传文件为空");
}
if (Objects.isNull(projectService.detail(projectId))) {
throw new ServiceException(Message.PROJECT_ID_IS_NOT_EXIST);
}
if (StrUtil.isNotBlank(turbineId) && turbineService.getById(turbineId) == null) {
throw new ServiceException(Message.PART_ID_IS_NOT_EXIST);
}
String userId = StpUtil.getLoginIdAsString();
String dateStr = DateUtil.today();
String storeDir = FilePathEnum.VIDEO.getFileAbsolutePathPrefix()
+ projectId;
if (!turbineId.isEmpty() )
storeDir+=File.separator+turbineId+ File.separator + dateStr;
else
storeDir+=File.separator + dateStr;
FileUtil.mkdir(storeDir);
List<VideoMonitorEntity> list = Lists.newArrayList();
for (MultipartFile file : files) {
String original = file.getOriginalFilename();
String suffix = FileUtil.extName(original);
if (suffix != null && !suffix.equals("mp4")) throw new ServerException("非视频文件");
String uuid = IdUtil.simpleUUID();
String fileName = uuid + StrUtil.DOT + suffix;
String absolutePath = storeDir + File.separator + fileName;
File dest = new File(absolutePath);
file.transferTo(dest);
VideoMonitorEntity entity = new VideoMonitorEntity();
entity.setVideoId(uuid);
entity.setProjectId(projectId);
entity.setTurbineId(turbineId);
entity.setVideoName(original);
entity.setVideoPath(FilePathEnum.VIDEO.getFileDownPath(absolutePath));
entity.setType(type);
entity.setStatus((byte) 0); // 待审核
entity.setPreTreatment(false);
entity.setIsDeleted(false);
entity.setCreateTime(new Date());
entity.setUploadTime(new Date());
entity.setUpdateBy(userId);
entity.setCreateBy(userId);
list.add(entity);
if (type.equals(VideoMonitorEnum.CLEARANCE.getCode()))
clearanceExecutor.execute(
new ClearanceTask(absolutePath, storeDir+File.separator+ type+File.separator+uuid, uuid, this)
);
else if (type.equals(VideoMonitorEnum.DEFORMATION.getCode())) {
//TODO
}
}
saveBatch(list);
return list;
}
@Override
public VideoMonitorEntity upload(String projectId,
String partId,
String type,
MultipartFile file) throws IOException {
return uploadBatch(projectId, partId, type, new MultipartFile[]{file}).get(0);
}
@Override
public PageResult<VideoMonitorEntity> page(Integer pageNo,
Integer pageSize,
String projectId,
String partId) {
LambdaQueryWrapper<VideoMonitorEntity> wrapper = Wrappers.lambdaQuery();
wrapper.eq(StrUtil.isNotBlank(projectId),VideoMonitorEntity::getProjectId, projectId)
.eq(StrUtil.isNotBlank(partId), VideoMonitorEntity::getTurbineId, partId)
.orderByDesc(VideoMonitorEntity::getCreateTime);
Page<VideoMonitorEntity> page = page(Page.of(pageNo, pageSize), wrapper);
return PageResult.ok(page.getRecords(), page.getTotal());
}
@Override
public List<VideoMonitorEntity> list(String projectId, String partId) {
return lambdaQuery()
.eq(StrUtil.isNotBlank(projectId),VideoMonitorEntity::getProjectId, projectId)
.eq(StrUtil.isNotBlank(partId), VideoMonitorEntity::getTurbineId, partId)
.list();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(String videoId) {
VideoMonitorEntity entity = getById(videoId);
if (Objects.isNull(entity)) {
throw new ServiceException("视频不存在");
}
entity.setIsDeleted(true);
entity.setUpdateBy(StpUtil.getLoginIdAsString());
updateById(entity);
// 物理删除文件
FileUtil.del(FilePathEnum.VIDEO.getFileAbsolutePath(entity.getVideoPath()));
}
@Override
public void download(String videoId, HttpServletResponse response) throws IOException {
VideoMonitorEntity entity = getById(videoId);
if (entity == null || Boolean.TRUE.equals(entity.getIsDeleted())) {
throw new ServiceException("视频不存在或已删除");
}
File file = new File(FilePathEnum.VIDEO.getFileAbsolutePath(entity.getVideoPath()));
if (!file.exists()) {
throw new ServiceException("视频文件不存在");
}
response.setContentType("video/mp4");
response.setHeader("Content-Disposition",
"inline; filename=" + URLEncoder.encode(entity.getVideoName(), StandardCharsets.UTF_8));
FileUtil.writeToStream(file, response.getOutputStream());
}
}

View File

@ -0,0 +1,49 @@
package com.dite.znpt.util;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.core.env.Environment;
import java.io.IOException;
/**
* @author hedechao
* @Date 2025/8/11 09:04
* @Description: 形变python脚本执行工具
*/
public class PythonUtil {
/**
* 调用叶片净空计算脚本
*
* @param videoPath 待检测视频路径
* @throws IOException 如果启动进程失败
* @throws InterruptedException 如果等待进程完成被中断
*/
public static void runClearance(
String videoPath,
String outputPath
) throws IOException, InterruptedException {
String pyScriptPath=SpringUtil.getBean(Environment.class).getProperty("pyScript.clearance");
String modelPath=SpringUtil.getBean(Environment.class).getProperty("model.tip-hub");
// 1. 构造命令
ProcessBuilder pb = new ProcessBuilder(
"python", // 也可以是 python3 / 绝对路径
pyScriptPath,
"--video_path=" + videoPath,
"--model_path=" + modelPath,
"--output_dir=" + outputPath
);
// 2. 把子进程的标准输出 / 错误流重定向到 Java 控制台可选
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
// 3. 启动进程并等待完成
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("脚本返回非 0 状态码: " + exitCode);
}
System.out.println("净空计算完成,结果已保存到: " + outputPath);
}
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dite.znpt.mapper.VideoMonitorEntityMapper">
<resultMap id="BaseResultMap" type="com.dite.znpt.domain.entity.VideoMonitorEntity">
<!--@mbg.generated-->
<!--@Table video_monitor-->
<id column="video_id" property="videoId" />
<result column="project_id" property="projectId" />
<result column="turbine_id" property="turbineId" />
<result column="video_name" property="videoName" />
<result column="video_path" property="videoPath" />
<result column="is_deleted" property="isDeleted" />
<result column="status" property="status" />
<result column="pre_image_path" property="preImagePath" />
<result column="pre_treatment" property="preTreatment" />
<result column="update_by" property="updateBy" />
<result column="create_time" property="createTime" />
<result column="create_by" property="createBy" />
<result column="update_time" property="updateTime" />
<result column="wind_speed" property="windSpeed" />
<result column="rpm" property="rpm" />
<result column="type" property="type" />
<result column="extra" property="extra" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
<result column="upload_time" property="uploadTime" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
video_id, project_id, part_id, video_name, video_path, is_deleted, `status`, pre_image_path,
pre_treatment, update_by, create_time, create_by, update_time, wind_speed, rpm, `type`,
extra, upload_time
</sql>
<select id="selectAllByProjectIdAndPartId" resultMap="BaseResultMap">
<!--@mbg.generated-->
select
<include refid="Base_Column_List"/>
from video_monitor
where project_id=#{projectId,jdbcType=VARCHAR} and part_id=#{partId,jdbcType=VARCHAR}
</select>
<insert id="batchInsert" keyColumn="video_id" keyProperty="videoId" parameterType="map" useGeneratedKeys="true">
<!--@mbg.generated-->
insert into video_monitor
(project_id, part_id, video_name, video_path, is_deleted, `status`, pre_image_path,
pre_treatment, update_by, create_time, create_by, update_time, wind_speed, rpm,
`type`, extra, upload_time)
values
<foreach collection="list" item="item" separator=",">
(#{item.projectId}, #{item.partId}, #{item.videoName}, #{item.videoPath}, #{item.isDeleted},
#{item.status}, #{item.preImagePath}, #{item.preTreatment}, #{item.updateBy}, #{item.createTime},
#{item.createBy}, #{item.updateTime}, #{item.windSpeed}, #{item.rpm}, #{item.type},
#{item.extra}, #{item.uploadTime})
</foreach>
</insert>
</mapper>

View File

@ -0,0 +1,77 @@
package com.dite.znpt.web.controller;
import com.dite.znpt.domain.PageResult;
import com.dite.znpt.domain.Result;
import com.dite.znpt.domain.entity.VideoMonitorEntity;
import com.dite.znpt.service.VideoMonitorService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* @author hedechao
* @date 2025/8/8 17:15
* @Description:
*/
@Api(tags = "视频监测")
@RestController
@RequestMapping("/video-monitor")
@RequiredArgsConstructor
public class VideoMonitorController {
@Resource
VideoMonitorService videoService;
@ApiOperation("分页查询")
@GetMapping("/page")
public PageResult<VideoMonitorEntity> page(@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam String projectId,
@RequestParam(required = false) String turbineId) {
return videoService.page(pageNo, pageSize, projectId, turbineId);
}
@ApiOperation("列表查询")
@GetMapping("/list")
public Result<List<VideoMonitorEntity>> list(@RequestParam(required = false) String projectId,
@RequestParam(required = false) String turbineId) {
return Result.ok(videoService.list(projectId, turbineId));
}
@ApiOperation("批量上传")
@PostMapping("/{projectId}/upload-batch")
public Result<List<VideoMonitorEntity>> uploadBatch(@PathVariable String projectId,
@RequestParam(required = false) String turbineId,
@RequestParam String type,
@RequestParam("files") MultipartFile[] files) throws IOException {
return Result.ok(videoService.uploadBatch(projectId, turbineId, type, files));
}
@ApiOperation("单文件上传")
@PostMapping("/{projectId}/upload")
public Result<VideoMonitorEntity> upload(@PathVariable String projectId,
@RequestParam(required = false) String turbineId,
@RequestParam String type,
@RequestParam("file") MultipartFile file) throws IOException {
return Result.ok(videoService.upload(projectId, turbineId, type, file));
}
@ApiOperation("删除")
@DeleteMapping("/{videoId}")
public Result<?> delete(@PathVariable String videoId) {
videoService.delete(videoId);
return Result.ok();
}
@ApiOperation("下载/播放")
@GetMapping("/download/{videoId}")
public void download(@PathVariable String videoId, HttpServletResponse response) throws IOException {
videoService.download(videoId, response);
}
}