通道界面与预览 初始化

This commit is contained in:
wangliwen 2022-11-29 17:33:02 +08:00
parent e765a67fae
commit 64c9f6ca69
7 changed files with 398 additions and 12 deletions

View File

@ -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<Int>("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<Int>("userId") // 用户id
val queryParams = requestHandler.queryParams()
val channelId = queryParams.get("channelId")
val subType = queryParams.get("subType")
val scheme = queryParams.get("scheme")
event!!.request<JsonArray>(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<JsonObject>(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<Int>("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())
}
}
}
}

View File

@ -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":离线

View File

@ -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) {
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)
}

View File

@ -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) // 通道分页
}
}

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>设备通道页面</title>
<link rel="stylesheet" th:href="@{/layui/css/layui.css}">
</head>
<body>
<div id="content" style="margin-top: 15px;margin-left: 15px;">
<div class="container">
搜索通道名称:
<div class="layui-inline">
<input class="layui-input" name="id" id="demoReload" autocomplete="off">
</div>
<button class="layui-btn" data-type="reload">搜索</button>
<table lay-filter="channel_table" id="channel_table"></table>
</div>
</div>
<script type="text/html" id="barDemo">
<a href="#" title="通道信息" style="font-size: 18px"><i class="layui-icon layui-icon-template-1"
style="font-size: 18px; color: #5FB878;"
lay-event="info"></i></a>
<a href="#" title="预览flv" style="font-size: 18px"><i class="layui-icon layui-icon-play"
style="font-size: 18px; color: #5FB878;"
lay-event="flv"></i></a>
<a href="#" title="预览hls" style="font-size: 18px"><i class="layui-icon layui-icon-play"
style="font-size: 18px; color: #5FB878;"
lay-event="hls"></i></a>
<a href="#" title="截图" style="font-size: 18px"><i class="layui-icon layui-icon-picture"
style="font-size: 18px; color: #FF5722;" lay-event="download"></i></a>
<a href="#" title="录制短视频" style="font-size: 18px"><i class="layui-icon layui-icon-video"
style="font-size: 18px; color: #66CCFF;"
lay-event="record"></i></a>
</script>
<script type="text/html" id="onlineTpl">
{{# if(d.online == 0){ }}
<span style="color: #FF5722;">离线</span>
{{# } else { }}
<span style="color: #5FB878;">在线</span>
{{# } }}
</script>
<script th:src="@{/layui/layui.js}"></script>
<script language='javascript' th:inline="javascript">
layui.use('table', function(){
var table = layui.table;
var layer = layui.layer;
var col = [
{
field: 'channelId',
title: '通道编号',
width: 280
},
{
field: 'name',
title: '通道名称',
width: 280
},
{
field: 'subType',
title: '设备子类',
width: 100
},
{
field: 'online',
title: '是否在线',
width: 150,
templet: '#onlineTpl'
},
{
fixed: 'right',
width: 140,
title: '操作',
align: 'center',
fixed: 'right',
toolbar: '#barDemo'
}
]
var tableIns = table.render({
elem: '#channel_table'
, height: 500
, width: 930
, page: true
, limit: 10
, limits: [10, 20]
, title: '设备通道列表'
, loading: true
, cols: [
col
],
url: '/admin/channel/page',
method: 'post',
request: {
pageName: 'page',
limitName: 'size'
},
contentType: 'application/json',
id: 'channel_table'
});
table.on('tool(channel_table)', function (obj) {
var data = obj.data;
var layEvent = obj.event;
var tr = obj.tr;
if (layEvent === 'flv') { // flv 预览
layer.open({
type: 2,
title: false,
area: ['1280px', '720px'],
shade: 0.8,
closeBtn: 0,
scrollbar: false,
shadeClose: true,
content: '/vod?stream=' + data.stream + '&clientId=' + data.clientId + '&liveApp=' + data.liveApp
});
}
})
var $ = layui.$, active = {
reload: function(){
var demoReload = $('#demoReload');
//执行重载
table.reload('channel_table', {
page: {
curr: 1 //重新从第 1 页开始
}
,where: {
key: demoReload.val()
}
});
}
};
$('.container .layui-btn').on('click', function(){
var type = $(this).data('type');
active[type] ? active[type].call(this) : '';
});
})
</script>
</body>
</html>