通道界面与预览 初始化
This commit is contained in:
parent
e765a67fae
commit
64c9f6ca69
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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":离线
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) // 通道分页
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -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>
|
Loading…
Reference in New Issue