优化文件

This commit is contained in:
BBIT-Kai
2025-06-18 10:21:32 +08:00
parent 617cc3162e
commit f02173c667
6 changed files with 219 additions and 249 deletions
@@ -9,7 +9,6 @@ import ink.snowflake.server.route.ImageAnalytics
import ink.snowflake.server.route.RemoteDebug
import ink.snowflake.server.route.VideoAnalytics
import ink.snowflake.server.route.VideoAnalyticsJetson
import ink.snowflake.server.route.mainFunc
import ink.snowflake.server.utils.AppConfig
import io.ktor.server.application.*
import io.ktor.server.tomcat.jakarta.*
@@ -54,8 +53,6 @@ fun Application.module() {
// 设置-WebSocket
configureSockets()
// 业务-首页导航
mainFunc()
// 业务-用户信息相关操作
User(appConfig)
// 业务-聊天
@@ -32,7 +32,6 @@ object ImageDao {
}
}
fun getImageList(name: String): List<ImageAnalyticsRequest> {
return transaction {
ImageTable.selectAll()
@@ -8,6 +8,7 @@ import io.ktor.server.auth.*
import io.ktor.server.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import kotlin.text.isNullOrEmpty
fun Application.ImageAnalytics() {
routing {
@@ -18,16 +19,6 @@ fun Application.ImageAnalytics() {
call.respond(BaseResponse(data = ImageDao.insertImageAnalyticsData(request)))
}
authenticate {
// 拍照保存为图片 并且调用Python程序进行分析
get("/takePhoto") {
val camera = call.request.queryParameters["cameraId"]
if (camera.isNullOrEmpty()) {
call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null))
return@get
}
stopHLSStream(camera)
call.respond(BaseResponse(message = "摄像头流已停止", data = null))
}
// 获取已分析图片列表
get("/getImageList") {
val name = call.parameters["name"]
@@ -1,30 +0,0 @@
package ink.snowflake.server.route
import com.google.gson.Gson
import ink.snowflake.server.model.response.BaseResponse
import ink.snowflake.server.model.response.DeviceItem
import ink.snowflake.server.utils.runCommand
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.BufferedReader
import java.io.InputStreamReader
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.auth.*
import io.ktor.server.http.content.*
import java.io.File
fun Application.mainFunc() {
routing {
get("/") {
// call.respondFile(File("src/main/resources/page/html/login.html"))
// call.respondRedirect("html/login.html")
}
}
}
@@ -34,244 +34,211 @@ import java.time.format.DateTimeFormatter
import java.util.*
val clients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
var aiState = "等待分析任务中"
fun Application.VideoAnalytics() {
routing {
// 实时发送AI状态
webSocket("/handleState") { // WebSocket 路由
clients.add(this) // 添加当前连接的客户端
send(aiState) // 向客户端发送连接成功消息
try {
incoming.consumeEach { frame -> // 持续接收消息
when (frame) {
is Frame.Text -> {
aiState = frame.readText() // 更新状态
broadcastMessage(aiState) // 使用封装的方法广播消息
}
is Frame.Close -> {
println("Closed")
clients.remove(this)
close() // 确保关闭 WebSocket 连接
return@consumeEach
}
// 其他消息类型的处理
is Frame.Binary -> TODO() // 处理二进制消息
is Frame.Ping -> TODO() // 处理 Ping 消息
is Frame.Pong -> TODO() // 处理 Pong 消息
}
}
} catch (e: Exception) {
// 处理接收消息时的异常
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
e.printStackTrace()
} finally {
clients.remove(this) // 确保在连接关闭时移除客户端
}
}
route("/api/iva") {
// 上传分析结果
post("/saveVideoAnalyticsData") {
val request = call.receive<VideoAnalyticsRequest>()
// todo 上传这里未做测试
call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request)))
}
authenticate {
post("/createVideoTask") {
val multipart = call.receiveMultipart() //1G
// 确保 uploads 目录存在
val uploadDir = File(VIDEO_INPUT_PATH)
if (!uploadDir.exists()) {
if (!uploadDir.mkdirs()) {
println("无法创建目录")
throw IOException("Failed to create upload directory.")
}
// 上传分析结果
post("/saveVideoAnalyticsData") {
val request = call.receive<VideoAnalyticsRequest>()
// todo 上传这里未做测试
call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request)))
}
authenticate {
post("/createVideoTask") {
val multipart = call.receiveMultipart() //1G
// 确保 uploads 目录存在
val uploadDir = File(VIDEO_INPUT_PATH)
if (!uploadDir.exists()) {
if (!uploadDir.mkdirs()) {
println("无法创建目录")
throw IOException("Failed to create upload directory.")
}
var fileName = ""
var name = ""
var datetime = ""
broadcastMessage("正在上传数据")
}
var fileName = ""
var name = ""
var datetime = ""
broadcastMessage("正在上传数据")
withContext(Dispatchers.IO) {
multipart.forEachPart { part ->
when (part) {
is PartData.FileItem -> {
fileName = part.originalFileName ?: "unknown"
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径
//ktor3
withContext(Dispatchers.IO) {
multipart.forEachPart { part ->
when (part) {
is PartData.FileItem -> {
fileName = part.originalFileName ?: "unknown"
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径
//ktor3
// file.outputStream().use { outputStream ->
// val writableChannel = Channels.newChannel(outputStream)
// part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel
// }
//ktor2
part.streamProvider().use { inputStream ->
file.outputStream().buffered().use { outputStream ->
inputStream.copyTo(outputStream)
}
//ktor2
part.streamProvider().use { inputStream ->
file.outputStream().buffered().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
is PartData.FormItem -> {
when (part.name) {
"projectName" -> name = part.value
"projectDatetime" -> datetime = part.value
}
}
else -> part.dispose()
}
is PartData.FormItem -> {
when (part.name) {
"projectName" -> name = part.value
"projectDatetime" -> datetime = part.value
}
}
else -> part.dispose()
}
}
call.respond(BaseResponse(message = "上传成功", data = null))
broadcastMessage("上传完成,开始启动AI引擎")
val command = listOf(
"/usr/bin/python3",
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
"$VIDEO_INPUT_PATH$fileName",
datetime,
name
}
call.respond(BaseResponse(message = "上传成功", data = null))
broadcastMessage("上传完成,开始启动AI引擎")
val command = listOf(
"/usr/bin/python3",
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
"$VIDEO_INPUT_PATH$fileName",
datetime,
name
)
println("-----------------" + command.joinToString(" "))
runCommand(command) {
println(it)
}
}
// 获取已分析视频列表
get("/getVideoList") {
val name = call.parameters["name"]
val res = VideoDao.getVideoList(name)
call.respond(BaseResponse(data = res))
}
// 获取某视频分析详情
get("/getAnalyticsDetailByVideoId") {
// 获取 vId 参数
val vIdParam = call.parameters["vId"]
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
if (vId == null) {
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
return@get
}
// 查询视频信息
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
if (video == null) {
call.respond(BaseResponse(status = false, message = "不存在该视频", data = null))
} else {
// 查询相关的分析详情
val details = VideoDao.selectVideoDetailByVid(vId)
// 用于返回的数据
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
val areaData = mutableListOf<List<Area>>()
val detailList = mutableListOf<DetailItem>()
// 颜色映射
val colors = mapOf(
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
)
println("-----------------" + command.joinToString(" "))
runCommand(command) {
println(it)
}
}
// 获取已分析视频列表
get("/getVideoList") {
val name = call.parameters["name"]
val res = VideoDao.getVideoList(name)
call.respond(BaseResponse(data = res))
}
// 获取某视频分析详情
get("/getAnalyticsDetailByVideoId") {
// 获取 vId 参数
val vIdParam = call.parameters["vId"]
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
if (vId == null) {
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
return@get
}
// 查询视频信息
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
if (video == null) {
call.respond(BaseResponse(status = false, message = "不存在该视频", data = null))
} else {
// 查询相关的分析详情
val details = VideoDao.selectVideoDetailByVid(vId)
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
var lastAction: String? = null
var areaStartTime: String? = null
var currentColor: String? = null
// 用于返回的数据
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
val areaData = mutableListOf<List<Area>>()
val detailList = mutableListOf<DetailItem>()
// 颜色映射
val colors = mapOf(
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
)
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
var lastAction: String? = null
var areaStartTime: String? = null
var currentColor: String? = null
for (detail in details) {
// 获取视频开始时间
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
val timeStr = vStartDatetime.toJavaLocalDateTime()
.plusSeconds(detail.a_time_stamp.toLong())
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
for (detail in details) {
// 获取视频开始时间
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
val timeStr = vStartDatetime.toJavaLocalDateTime()
.plusSeconds(detail.a_time_stamp.toLong())
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
// 添加总人数和口罩佩戴人数
yTotalData.add(timeStr to detail.a_total_people)
yMaskedData.add(timeStr to detail.a_total_people_masked)
// 添加总人数和口罩佩戴人数
yTotalData.add(timeStr to detail.a_total_people)
yMaskedData.add(timeStr to detail.a_total_people_masked)
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
if (detail.a_action != "--") { // 只处理非 "--" 动作
if (lastAction == null || lastAction != detail.a_action) {
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
if (areaStartTime != null) {
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
)
)
}
// 添加到 detailList,记录动作开始的时刻
detailList.add(
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
)
// 更新为新的动作
lastAction = detail.a_action
areaStartTime = timeStr
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
}
} else {
// 如果当前动作为 "--",则结束当前区域,并重置状态
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
if (detail.a_action != "--") { // 只处理非 "--" 动作
if (lastAction == null || lastAction != detail.a_action) {
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
if (areaStartTime != null) {
// 添加到 detailList,记录动作开始的时刻
detailList.add(
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
)
// 结束当前区域
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
)
)
// 重置状态
lastAction = null
areaStartTime = null
currentColor = null
}
// 添加到 detailList,记录动作开始的时刻
detailList.add(
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
)
// 更新为新的动作
lastAction = detail.a_action
areaStartTime = timeStr
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
}
} else {
// 如果当前动作为 "--",则结束当前区域,并重置状态
if (areaStartTime != null) {
// 添加到 detailList,记录动作开始的时刻
detailList.add(
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
)
// 结束当前区域
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
)
)
// 重置状态
lastAction = null
areaStartTime = null
currentColor = null
}
}
// 处理最后一个区域的结束时间
if (areaStartTime != null && lastAction != null) {
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
)
)
}
val analyticsData = VideoAnalyticsData(
yTotalData = yTotalData,
yMaskedData = yMaskedData,
areaData = areaData
)
// 返回数据
call.respond(
BaseResponse(
data = VideoAnalyticsDetail(
v_id = vId,
v_name = video[VideoTable.vName],
v_video_play_path = "http://${SERVER_PATH_OSS}:9000/video/" + video[VideoTable.vObjectName],
v_file_name = video[VideoTable.vFileName],
v_duration = video[VideoTable.vDuration],
v_size = video[VideoTable.vSize],
v_start_datetime = video[vStartDateTime]?.toString() ?: "",
v_video_codec = video[VideoTable.vVideoCodec],
v_audio_codec = video[VideoTable.vAudioCodec],
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
v_resolution = video[VideoTable.vResolution],
v_a_time = video[VideoTable.vATime].toString(),
v_a_total_people = video[vATotalPeople], // 总人数
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
v_details_list = detailList
)
}
// 处理最后一个区域的结束时间
if (areaStartTime != null && lastAction != null) {
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
)
)
}
val analyticsData = VideoAnalyticsData(
yTotalData = yTotalData,
yMaskedData = yMaskedData,
areaData = areaData
)
// 返回数据
call.respond(
BaseResponse(
data = VideoAnalyticsDetail(
v_id = vId,
v_name = video[VideoTable.vName],
v_video_play_path = "http://${SERVER_PATH_OSS}:9000/video/" + video[VideoTable.vObjectName],
v_file_name = video[VideoTable.vFileName],
v_duration = video[VideoTable.vDuration],
v_size = video[VideoTable.vSize],
v_start_datetime = video[vStartDateTime]?.toString() ?: "",
v_video_codec = video[VideoTable.vVideoCodec],
v_audio_codec = video[VideoTable.vAudioCodec],
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
v_resolution = video[VideoTable.vResolution],
v_a_time = video[VideoTable.vATime].toString(),
v_a_total_people = video[vATotalPeople], // 总人数
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
v_details_list = detailList
)
)
)
}
}
}
}
}
@@ -0,0 +1,46 @@
var aiState = "等待分析任务中"
// 实时发送AI状态
webSocket("/handleState") { // WebSocket 路由
clients.add(this) // 添加当前连接的客户端
send(aiState) // 向客户端发送连接成功消息
try {
incoming.consumeEach { frame -> // 持续接收消息
when (frame) {
is Frame.Text -> {
aiState = frame.readText() // 更新状态
broadcastMessage(aiState) // 使用封装的方法广播消息
}
is Frame.Close -> {
println("Closed")
clients.remove(this)
close() // 确保关闭 WebSocket 连接
return@consumeEach
}
// 其他消息类型的处理
is Frame.Binary -> TODO() // 处理二进制消息
is Frame.Ping -> TODO() // 处理 Ping 消息
is Frame.Pong -> TODO() // 处理 Pong 消息
}
}
} catch (e: Exception) {
// 处理接收消息时的异常
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
e.printStackTrace()
} finally {
clients.remove(this) // 确保在连接关闭时移除客户端
}
}
// 拍照保存为图片 并且调用Python程序进行分析
//get("/takePhoto") {
// val camera = call.request.queryParameters["cameraId"]
// if (camera.isNullOrEmpty()) {
// call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null))
// return@get
// }
// stopHLSStream(camera)
// call.respond(BaseResponse(message = "摄像头流已停止", data = null))
//}