diff --git a/src/main/kotlin/com/hisense/dahua_video/controller/DeviceChannelController.kt b/src/main/kotlin/com/hisense/dahua_video/controller/DeviceChannelController.kt new file mode 100644 index 0000000..faa352f --- /dev/null +++ b/src/main/kotlin/com/hisense/dahua_video/controller/DeviceChannelController.kt @@ -0,0 +1,177 @@ +package com.hisense.dahua_video.controller + +import com.hisense.dahua_video.consts.EventBusAddress +import com.hisense.dahua_video.consts.MonitorScheme +import com.hisense.dahua_video.modules.device.Device +import com.hisense.dahua_video.modules.device.DeviceChannel +import com.hisense.dahua_video.modules.device.Organization +import com.hisense.dahua_video.modules.monitor.MonitorUser +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.eventbus.EventBus +import io.vertx.core.impl.logging.Logger +import io.vertx.core.impl.logging.LoggerFactory +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import io.vertx.ext.web.RoutingContext +import io.vertx.ext.web.templ.thymeleaf.ThymeleafTemplateEngine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.LocalDateTime + + +/** + * 通道控制器 + */ +class DeviceChannelController( + vertx: Vertx, + templateEngine: ThymeleafTemplateEngine? +) { + private val logger: Logger = LoggerFactory.getLogger(DeviceChannelController::class.java) + var templateEngine: ThymeleafTemplateEngine? = null + private var event: EventBus? = null + private var vertx: Vertx + + private val deviceChannel = DeviceChannel() + private val device = Device() + private val organization = Organization() + private val monitorUser = MonitorUser() + + init { + this.vertx = vertx + this.event = this.vertx.eventBus() + this.templateEngine = templateEngine + } + + + /** + * (设备通道)首页 + */ + fun index(requestHandler: RoutingContext) { + val userId = requestHandler.get("userId") + val data = JsonObject() + templateEngine!!.render( + data, "templates/device_channel/index.html" + ).onSuccess { success: Buffer? -> + requestHandler.response().putHeader("Content-Type", "text/html").end(success) + }.onFailure { fail: Throwable -> + requestHandler.fail(fail) + } + } + + + /** + * flv预览页面 + */ + fun flv(requestHandler: RoutingContext) { + val userId = requestHandler.get("userId") // 用户id + val queryParams = requestHandler.queryParams() + val channelId = queryParams.get("channelId") + val subType = queryParams.get("subType") + val scheme = queryParams.get("scheme") + event!!.request(EventBusAddress.SYS_MONITOR_USER_ALLMONITORUSER_TOKEN.address, JsonObject()) { + if (it.succeeded()) { + val token = it.result().body().stream().filter { index -> + index as JsonObject + index.getInteger("id") == userId + }.findFirst() + token.ifPresent { tokenInfo -> + tokenInfo as JsonObject + tokenInfo.put("channelId", channelId) + tokenInfo.put("subType", subType) + tokenInfo.put("scheme", MonitorScheme.values()[scheme.toInt()].scheme) + event!!.request(EventBusAddress.SYS_DEVICE_CHANNEL_PREVIEW_URL.address, tokenInfo) { previewUrl -> + if (previewUrl.succeeded()) { + requestHandler + .response() + .putHeader("content-type", "application/json") + .end(previewUrl.result().body().encode()) + } else { + requestHandler + .response() + .putHeader("content-type", "application/json") + .end(JsonObject().encode()) + } + } + } + } + } + } + + + /** + * hls预览页面 + */ + fun hls(requestHandler: RoutingContext) { + val queryParams = requestHandler.queryParams() + } + + + /** + * 设备通道分页 + */ + fun deviceChannelPage(requestHandler: RoutingContext) { + val body = requestHandler.body().asJsonObject() + val size = body.getInteger("size", 10) + val page = body.getLong("page", 1) + val key = body.getString("key", "") + val userId = requestHandler.get("userId") + runBlocking(Dispatchers.IO) { + transaction { + addLogger(StdOutSqlLogger) + val query = if (key.isNullOrEmpty()) { + deviceChannel + .join(device, JoinType.LEFT, additionalConstraint = { deviceChannel.deviceId eq device.id }) + .join(organization, JoinType.LEFT, additionalConstraint = { organization.id eq device.organizationId }) + .join(monitorUser, JoinType.LEFT, additionalConstraint = { monitorUser.id eq organization.userId }) + .slice( + deviceChannel.id, + deviceChannel.channelId, + deviceChannel.name, + deviceChannel.subType, + deviceChannel.online, + deviceChannel.sort + ).select { monitorUser.id eq userId } + .orderBy(deviceChannel.sort to SortOrder.DESC) + } else { + deviceChannel + .join(device, JoinType.LEFT, additionalConstraint = { deviceChannel.deviceId eq device.id }) + .join(organization, JoinType.LEFT, additionalConstraint = { organization.id eq device.organizationId }) + .join(monitorUser, JoinType.LEFT, additionalConstraint = { monitorUser.id eq organization.userId }) + .slice( + deviceChannel.id, + deviceChannel.channelId, + deviceChannel.name, + deviceChannel.subType, + deviceChannel.online, + deviceChannel.sort + ).select { monitorUser.id eq userId } + .andWhere { deviceChannel.name like "%${key}%" } + .orderBy(deviceChannel.sort to SortOrder.DESC) + } + + fun mapToJson(it: ResultRow): JsonObject? { + return JsonObject() + .put("id", it[deviceChannel.id].value) + .put("channelId", it[deviceChannel.channelId]) + .put("name", it[deviceChannel.name]) + .put("subType", it[deviceChannel.subType]) + .put("online", it[deviceChannel.online]) + .put("sort", it[deviceChannel.sort]) + } + + val count = query.count() + val pageList = query.limit(size, (page - 1) * size).map { mapToJson(it) } + val result = JsonObject() + .put("code", 0) + .put("msg", LocalDateTime.now().toString()) + .put("count", count) + .put("data", pageList) + requestHandler.response().putHeader("content-type", "application/json") + .end(result.encode()) + } + } + } +} diff --git a/src/main/kotlin/com/hisense/dahua_video/modules/device/DeviceChannel.kt b/src/main/kotlin/com/hisense/dahua_video/modules/device/DeviceChannel.kt index e04edde..566cecf 100644 --- a/src/main/kotlin/com/hisense/dahua_video/modules/device/DeviceChannel.kt +++ b/src/main/kotlin/com/hisense/dahua_video/modules/device/DeviceChannel.kt @@ -10,7 +10,7 @@ class DeviceChannel : IntIdTable("sys_device_channel") { val deviceId = integer("device_id").references(device.id) // 所属设备(外键)。 val channelNo = text("channel_no").uniqueIndex() // 通道编码 val channelId = text("channel_id").uniqueIndex() // 通道编码 同channelNo - val name = text("name").nullable() // 通道名称 + val name = text("name").nullable().index() // 通道名称 val orgCode = text("org_code").nullable() // 组织编码,根组织为"" val orgType = text("org_type").nullable() // 组织类型,"1"为基本组织 val online = text("online").nullable() // 通道是否在线。"1":在线;"0":离线 diff --git a/src/main/kotlin/com/hisense/dahua_video/util/ScreenshotUtil.kt b/src/main/kotlin/com/hisense/dahua_video/util/ScreenshotUtil.kt index c8eb2f4..767d811 100644 --- a/src/main/kotlin/com/hisense/dahua_video/util/ScreenshotUtil.kt +++ b/src/main/kotlin/com/hisense/dahua_video/util/ScreenshotUtil.kt @@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.bytedeco.ffmpeg.avcodec.AVPacket import org.bytedeco.javacv.FFmpegFrameGrabber import org.bytedeco.javacv.FFmpegFrameRecorder import org.bytedeco.javacv.Java2DFrameConverter @@ -145,7 +144,8 @@ class ScreenshotUtil(vertx: Vertx) { logger.info("图片保存成功:$fileName") event!!.publish( EventBusAddress.SYS_DEVICE_CHANNEL_SCREENSHOT_PICTURE_SAVE.address, - JsonObject().put("fileSize", File(fileName).length()) + JsonObject() + .put("fileSize", File(fileName).length()) .put("channelNo", screenshot.getString("channelId")) .put("filePath", fileName) ) @@ -193,7 +193,7 @@ class ScreenshotUtil(vertx: Vertx) { replay.reply(screenshotAction(result)) break } else { - logger.info("截图队列已满,尝试重试中:${result.encode()}") + logger.info("ffmpeg队列已满,尝试重试中:${result.encode()}") val random = (3000..10000).random().toLong() delay(random) } @@ -224,12 +224,27 @@ class ScreenshotUtil(vertx: Vertx) { logger.info("抓取url:$url") val result = JsonObject() result.put("startTime", LocalDateTime.now().toString()) - GlobalScope.launch(Dispatchers.IO) { - val grabber = FFmpegFrameGrabber(url)// 使用协程 + GlobalScope.launch(Dispatchers.IO) {// 使用协程 + val grabber = FFmpegFrameGrabber(url) val converter = Java2DFrameConverter() try { - if (screenshot.getString("scheme") == MonitorScheme.RTSP.scheme) { - grabber.setOption("rtsp_transport", "tcp") + when (screenshot.getString("scheme")) { + MonitorScheme.RTSP.scheme -> { + grabber.setOption("rtsp_transport", "tcp") + grabber.format = MonitorScheme.RTSP.scheme + } + + MonitorScheme.FLV_HTTP.scheme -> { + grabber.format = "flv" + } + + MonitorScheme.HLS.scheme -> { + + } + + MonitorScheme.RTMP.scheme -> { + grabber.format = MonitorScheme.RTMP.scheme + } } grabber.setOption("stimeout", "2000000") grabber.start() @@ -237,6 +252,11 @@ class ScreenshotUtil(vertx: Vertx) { val videoWidth = grabber.imageWidth val videoHeight = grabber.imageHeight val audioChannels = grabber.audioChannels + val videoCodecName = grabber.videoCodecName + val videoCodec = grabber.videoCodec + val formatContext = grabber.formatContext + logger.info("获取到预览流信息--> videoWidth:$videoWidth videoHeight:$videoHeight audioChannels:$audioChannels videoCodecName:$videoCodecName videoCodec:$videoCodec") + logger.info("formatContext--> $formatContext") val fileName = record + File.separator + screenshot.getString("channelId") + File.separator + screenshot.getString("channelId") + "_" + DateTimeFormatter.ofPattern( @@ -244,13 +264,15 @@ class ScreenshotUtil(vertx: Vertx) { ) .format(LocalDateTime.now()) + ".mp4" val recorder = FFmpegFrameRecorder(fileName, videoWidth, videoHeight, audioChannels) - recorder.videoBitrate = 4096 + recorder.videoBitrate = grabber.videoBitrate recorder.gopSize = 2 recorder.frameRate = 25.0 recorder.videoCodecName = "copy" recorder.format = "mp4" + try { recorder.start(grabber.formatContext) + logger.info("recorder 启动成功,开始进行录制") val startTime = System.currentTimeMillis() // 开始录制时间 while (true) { // 不断循环录制 val packet = grabber.grabPacket() @@ -258,7 +280,7 @@ class ScreenshotUtil(vertx: Vertx) { recorder.recordPacket(packet) } var durationMS = System.currentTimeMillis() - startTime - if (durationMS > (duration * 1000)) { // 超过预设录制时长 + if (durationMS >= (duration * 1000)) { // 超过预设录制时长 break } } @@ -269,6 +291,24 @@ class ScreenshotUtil(vertx: Vertx) { recorder.stop() recorder.close() recorder.release() + delay(100) // 延迟100ms + /** + * 尝试获取视频时长 + */ + val grabberFile = FFmpegFrameGrabber(fileName) + grabberFile.start() + val fileDuration = grabberFile.lengthInTime / 1000000 // 获取视频时长 秒 + grabberFile.stop() + grabberFile.close() + grabberFile.release() + event!!.publish( + EventBusAddress.SYS_DEVICE_CHANNEL_SCREENSHOT_VIDEO_SAVE.address, + JsonObject() + .put("fileSize", File(fileName).length()) + .put("channelNo", screenshot.getString("channelId")) + .put("filePath", fileName) + .put("duration", fileDuration) + ) } } catch (e: Exception) { logger.error("抓取录制短视频·失败·", e) @@ -313,7 +353,7 @@ class ScreenshotUtil(vertx: Vertx) { replay.reply(recordAction(result)) break } else { - logger.info("截图队列已满,尝试重试中:${result.encode()}") + logger.info("ffmpeg队列已满,尝试重试中:${result.encode()}") val random = (3000..10000).random().toLong() delay(random) } diff --git a/src/main/kotlin/com/hisense/dahua_video/verticle/WebAPIVerticle.kt b/src/main/kotlin/com/hisense/dahua_video/verticle/WebAPIVerticle.kt index 8990be9..0a42cc3 100644 --- a/src/main/kotlin/com/hisense/dahua_video/verticle/WebAPIVerticle.kt +++ b/src/main/kotlin/com/hisense/dahua_video/verticle/WebAPIVerticle.kt @@ -63,6 +63,7 @@ class WebAPIVerticle : CoroutineVerticle() { val monitorUserController = MonitorUserController(this.vertx, templateEngine) val monitorController = MonitorController(this.vertx, templateEngine) val organizationController = OrganizationController(this.vertx, templateEngine) + val deviceChannelController = DeviceChannelController(this.vertx, templateEngine) router.route().method(HttpMethod.POST).handler(BodyHandler.create()) router.route("/*").handler(StaticHandler.create()) //静态资源 router.route().handler( @@ -80,7 +81,7 @@ class WebAPIVerticle : CoroutineVerticle() { router.route().method(HttpMethod.GET).handler(commonController::getCros) logger.info("开放管理员后台") - router.route("/admin/*").order(0).handler(commonController::checkLogin) // 管理首页 + router.route("/admin/*").order(0).handler(commonController::checkLogin) // 管理首页(会先期校验登录情况) router.route(HttpMethod.GET, "/admin/").handler(indexControl::index) // 首页 router.route(HttpMethod.POST, "/admin/check").handler(indexControl::check) // 注册校验 router.route(HttpMethod.POST, "/admin/sign_up").handler(indexControl::sign_up) // 注册表单 @@ -90,5 +91,8 @@ class WebAPIVerticle : CoroutineVerticle() { router.route(HttpMethod.GET, "/admin/realmonitorUrl").handler(monitorController::realmonitorUrl) // 获取预览地址 router.route(HttpMethod.GET, "/admin/screenshot").handler(monitorController::screenshot) // 截图 router.route(HttpMethod.GET, "/admin/record").handler(monitorController::recordVideo) // 截取短视频 + + router.route(HttpMethod.GET, "/admin/channel/index").handler(deviceChannelController::index) // 通道首页 + router.route(HttpMethod.POST, "/admin/channel/page").handler(deviceChannelController::deviceChannelPage) // 通道分页 } } diff --git a/src/main/resources/templates/device_channel/flv.html b/src/main/resources/templates/device_channel/flv.html new file mode 100644 index 0000000..04e75bc --- /dev/null +++ b/src/main/resources/templates/device_channel/flv.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + diff --git a/src/main/resources/templates/device_channel/hls.html b/src/main/resources/templates/device_channel/hls.html new file mode 100644 index 0000000..04e75bc --- /dev/null +++ b/src/main/resources/templates/device_channel/hls.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + diff --git a/src/main/resources/templates/device_channel/index.html b/src/main/resources/templates/device_channel/index.html new file mode 100644 index 0000000..8bdefb5 --- /dev/null +++ b/src/main/resources/templates/device_channel/index.html @@ -0,0 +1,145 @@ + + + + + + + + 设备通道页面 + + + +
+
+ 搜索通道名称: +
+ +
+ +
+
+
+ + + + + +