大华网络摄像机
东北小麦客 2025-02-25 JavaScript大华网络摄像机
# 一. 基础
参考资料
# 调用案例
CONSTANST.ts
/**
* 照相机通道类型
*/
export enum channelEnum {
/**
* 正常 彩色通道
*/
normal = 1,
/**
* 温度 热成像通道
*/
therm = 2,
}
/**
* 码流类型
*/
export enum streamEnum {
/**
* 主码流
*/
main = 0,
/**
* 辅码流1
*/
auxiliary1 = 1,
/**
* ### 辅码流2 现在不可用!!!
*/
// auxiliary2 = 2,
}
/**
* 摄像机弹窗dom元素最大、最小宽度、高度
*/
export const cameraLiveDomStyle = {
/**
* 摄像机弹窗-最小宽度-px
*/
minW: 280,
/**
* 摄像机弹窗-最小高度-px
*/
minH: 200,
/**
* 摄像机弹窗-最大宽度-px
*/
maxW: Math.floor(window.innerWidth * 0.9),
/**
* 摄像机弹窗-最大高度-px
*/
maxH: Math.floor(window.innerHeight * 0.9),
/**
* 默认宽度-px 45vw or 625px(当45vw < 625px时; 取625px)
*/
defaultWidth: Math.floor(window.innerHeight * 0.45) < 625 ? 625 : Math.floor(window.innerHeight * 0.45),
/**
* 默认高度-px
*/
defaultHeight: 360,
/**
* 默认top-百分比%
*/
defaultTop: '25%',
/**
* 默认left-百分比%
*/
defaultLeft: '30%',
};
/**
* 缩小的 操作类型
*/
export enum zoomMinEnum {
/**
* 缩小至左下角
*/
moveLbMin = 1,
/**
* 缩小至右下角
*/
moveRbMin = 2,
}
/**
* 摄像头测试配置信息
*/
export const urlInfo = {
/**
* 连接协议
*/
cameraConnProtocol: (window.siteConfig as any).camera?.cameraConnProtocol || 'wss',
/**
* websocket 服务端口
* 80 - http
* 443 - https
*/
cameraConnWSPort: (window.siteConfig as any).camera?.cameraConnWSPort || 443,
/**
* RTSP 服务端口(默认为 554)
*/
cameraConnRTSPPort: (window.siteConfig as any).camera?.cameraConnRTSPPort || 554,
/**
* 主机地址
*/
cameraConnHost:
(window.siteConfig as any).camera?.cameraConnHost ||
import.meta.env.VITE_APP_CAMERA_CONNHOST ||
'opspre.imyunxia.com',
};
/**
* 摄像枪错误码枚举
* - 参考文档
* - (国际化键值对) https://open-icc.dahuatech.com/wsplayer/#/api
*/
export enum cameraErrorCodeEnum {
/**
* 网络链接状况不好, 延时大于8秒
*/
notGoodNet = 101,
/**
* 不支持当前音频格式
*/
unknownAudioFormat = 201,
/**
* webSocket 发生错误
*/
webSocketError = 202,
/**
* webSocket 已关闭
*/
webSocketClosed = 207,
/**
* 找不到流-RTSP 地址没找到
*/
notFoundStream = 404,
/**
* SETUP 服务不可用
*/
notUseSETUPService = 503,
}
useDomResize.ts
import { onBeforeUnmount } from 'vue';
import { cameraLiveDomStyle } from './CONSTANST';
/**
* ### 左下角/右下角缩放(未兼容移动端)
* @param pDomSelector 父元素的 id/class选择器
*/
export function DomZoomScale(pDomSelector = '#cameraLiveWrapper') {
let dom: HTMLDivElement;
let resizeHandleDomR: HTMLDivElement;
let isResizing = false;
let startX;
let startY;
let startWidth;
let startHeight;
function register() {
dom = document.querySelector(pDomSelector) as HTMLDivElement;
if (!dom) return;
resizeHandleDomR = document.createElement('div');
resizeHandleDomR.style.cssText =
'position: absolute; z-index: 5; width: 20px; height: 20px; right: 0; bottom: 0; cursor: se-resize;';
dom.append(resizeHandleDomR);
resizeHandleDomR.onmousedown = handleMouseDown;
}
function handleMouseDown(e: MouseEvent) {
e.preventDefault();
isResizing = true;
// 获取初始位置
startX = e.clientX;
startY = e.clientY;
startWidth = dom.offsetWidth;
startHeight = dom.offsetHeight;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}
// 鼠标移动时
function onMouseMove(e) {
if (isResizing) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newWidth = startWidth + dx;
let newHeight = startHeight + dy;
const newMaxW = Math.ceil(
((100 - parseFloat(dom.style.left || cameraLiveDomStyle.defaultLeft)) / 100) * window.innerWidth,
);
const newMaxH = Math.ceil(
((100 - parseFloat(dom.style.top || cameraLiveDomStyle.defaultTop)) / 100) * window.innerHeight,
);
newWidth = newWidth >= newMaxW ? newMaxW : newWidth;
newHeight = newHeight >= newMaxH ? newMaxH : newHeight;
if (newWidth > cameraLiveDomStyle.minW && newHeight > cameraLiveDomStyle.minH) {
dom.style.width = `${newWidth}px`;
dom.style.height = `${newHeight}px`;
}
}
}
// 鼠标释放时停止缩放
function onMouseUp() {
isResizing = false;
resizeHandleDomR = null;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
onBeforeUnmount(() => {
onMouseUp();
dom = null;
});
return {
register,
unregister: onMouseUp,
};
}
/**
* ### 兼容移动端-dom元素缩放
* @param pDomSelector 父元素的 id/class选择器
*/
export function TouchScale(pDomSelector = '#cameraLiveWrapper') {
const store = {
isScaling: false,
distance: 0,
};
function touchstart(event: TouchEvent) {
const { touches } = event;
if (touches.length === 2) {
const [touch1, touch2] = Array.from(touches);
store.isScaling = true;
store.distance = Math.sqrt(
Math.ceil(touch2.clientX - touch1.clientX) ** 2 + Math.ceil(touch2.clientY - touch1.clientY) ** 2,
);
}
}
function touchmove(event: TouchEvent) {
event.preventDefault();
if (!store.isScaling) return;
const { touches } = event;
if (touches.length === 2) {
touchMove(event);
}
}
function touchend() {
store.isScaling = false;
}
function touchcancel() {
store.isScaling = false;
}
function touchMove(event: TouchEvent) {
const { touches, currentTarget } = event;
const target = currentTarget as HTMLDivElement;
const [touch1, touch2] = Array.from(touches);
const newDistance = Math.sqrt(
Math.ceil(touch2.clientX - touch1.clientX) ** 2 + Math.ceil(touch2.clientY - touch1.clientY) ** 2,
);
const scale = +(newDistance / store.distance).toFixed(3);
store.distance = newDistance;
updateStyle(target, scale);
}
function updateStyle(target: HTMLDivElement, scale: number) {
let newWidth = target.style.width ? parseFloat(target.style.width) : cameraLiveDomStyle.defaultWidth;
let newHeight = target.style.height ? parseFloat(target.style.height) : cameraLiveDomStyle.defaultHeight;
newWidth = Math.ceil(newWidth * scale);
newHeight = Math.ceil(newHeight * scale);
const newMaxW = Math.ceil(
((100 - parseFloat(target.style.left || cameraLiveDomStyle.defaultLeft)) / 100) * window.innerWidth,
);
const newMaxH = Math.ceil(
((100 - parseFloat(target.style.top || cameraLiveDomStyle.defaultTop)) / 100) * window.innerHeight,
);
newWidth = newWidth >= newMaxW ? newMaxW : newWidth;
newHeight = newHeight >= newMaxH ? newMaxH : newHeight;
if (newWidth > cameraLiveDomStyle.minW && newHeight > cameraLiveDomStyle.minH) {
target.style.width = `${newWidth}px`;
target.style.height = `${newHeight}px`;
}
}
function register() {
const dom = document.querySelector(pDomSelector) as HTMLDivElement;
if (!dom) return;
dom.addEventListener('touchstart', touchstart);
dom.addEventListener('touchmove', touchmove);
dom.addEventListener('touchend', touchend);
dom.addEventListener('touchcancel', touchcancel);
}
function unregister() {
const dom = document.querySelector(pDomSelector) as HTMLDivElement;
if (!dom) return;
dom.removeEventListener('touchstart', touchstart);
dom.removeEventListener('touchmove', touchmove);
dom.removeEventListener('touchend', touchend);
dom.removeEventListener('touchcancel', touchcancel);
}
onBeforeUnmount(() => {
unregister();
});
return {
register,
unregister,
};
}
useDomMove.ts
import { onBeforeUnmount } from 'vue';
import { cameraLiveDomStyle } from './CONSTANST';
/**
* ### dom元素移动(未兼容移动端)
* @param pDomSelector 父元素的 id/class选择器
*/
export function DomZoomMove(pDomSelector = '#cameraLiveWrapper') {
/**
* 触发移动的dom元素
*/
let moveDom: HTMLDivElement;
let isDraging = false;
let offsetY = 0;
let offsetX = 0;
/**
*
* @param selector id/class 选择器
*/
function register(selector: string) {
moveDom = document.querySelector(selector) as HTMLDivElement;
if (!moveDom) return;
moveDom.onmousedown = handleMouseDown;
// moveDom.onmouseleave = handleMouseUp;
}
function handleMouseDown() {
document.addEventListener('mousemove', handleMousemove);
document.addEventListener('mouseup', handleMouseUp);
}
function handleMouseUp() {
isDraging = false;
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMousemove);
}
function handleMousemove(e: MouseEvent) {
if (!isDraging) {
offsetY = e.offsetY;
offsetX = e.offsetX;
isDraging = true;
return;
}
const dom = document.querySelector(pDomSelector) as HTMLDivElement;
if (dom) {
const curWidth = dom.clientWidth;
const curHeight = dom.clientHeight;
let tPx = e.clientY - offsetY;
let lPx = e.clientX - offsetX;
tPx = tPx > 0 ? tPx : 0;
lPx = lPx > 0 ? lPx : 0;
let top = +((tPx / window.innerHeight) * 100).toFixed(2);
let left = +((lPx / window.innerWidth) * 100).toFixed(2);
top = top >= 80 ? 80 : top;
left = left >= 90 ? 90 : left;
dom.style.top = `${top}%`;
dom.style.left = `${left}%`;
dom.style.width = `${curWidth}px`;
dom.style.height = `${curHeight}px`;
}
}
function unregister() {
if (moveDom) {
isDraging = false;
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMousemove);
moveDom.onmousedown = null;
// moveDom.onmouseleave = null;
}
}
onBeforeUnmount(() => {
unregister();
});
return {
register,
unregister,
};
}
/**
* ### 兼容移动端-dom元素移动
* TODO pointer Event https://blog.csdn.net/qq_45472813/article/details/131484480
* @param pDomSelector 父元素的 id/class选择器
*/
export function TouchMove(pDomSelector = '#cameraLiveWrapper') {
const store = {
isTouched: false,
offsetX: 0,
offsetY: 0,
};
function touchstart(event: TouchEvent) {
const { touches } = event;
if (touches.length === 1) {
const [touch] = Array.from(touches);
store.isTouched = true;
store.offsetX = touch.pageX;
store.offsetY = touch.pageY;
}
}
function touchmove(event: TouchEvent) {
if (!store.isTouched) return;
const { touches } = event;
if (touches.length === 1) {
event.preventDefault();
touchMove(event);
}
}
function touchend() {
store.isTouched = false;
}
function touchcancel() {
store.isTouched = false;
}
function touchMove(event: TouchEvent) {
const { touches, currentTarget } = event;
const [touch] = Array.from(touches);
const target = currentTarget as HTMLDivElement;
const offsetX = touch.pageX;
const offsetY = touch.pageY;
updateStyle(target, offsetX, offsetY);
}
function updateStyle(target: HTMLDivElement, offsetX: number, offsetY: number) {
const diffX = offsetX - store.offsetX;
const diffY = offsetY - store.offsetY;
store.offsetX = offsetX;
store.offsetY = offsetY;
let leftPix = (parseFloat(target.style.left || cameraLiveDomStyle.defaultLeft) / 100) * window.innerWidth;
let topPix = (parseFloat(target.style.top || cameraLiveDomStyle.defaultTop) / 100) * window.innerHeight;
leftPix = Math.ceil(leftPix + diffX);
topPix = Math.ceil(topPix + diffY);
let left = +((leftPix / window.innerWidth) * 100).toFixed(2);
let top = +((topPix / window.innerHeight) * 100).toFixed(2);
const maxTop = Math.ceil(((window.innerHeight - parseFloat(target.style.height)) / window.innerHeight) * 100);
const maxLeft = Math.ceil(((window.innerWidth - parseFloat(target.style.width) - 8) / window.innerWidth) * 100); // 8 边界值
if (top > 0) {
top = top >= maxTop ? maxTop : top;
} else {
top = 0;
}
if (left > 0) {
left = left >= maxLeft ? maxLeft : left;
} else {
left = 0;
}
// console.log(top, left);
target.style.top = `${top}%`;
target.style.left = `${left}%`;
}
function register() {
const dom = document.querySelector(pDomSelector) as HTMLDivElement;
if (!dom) return;
dom.addEventListener('touchstart', touchstart);
dom.addEventListener('touchmove', touchmove);
dom.addEventListener('touchend', touchend);
dom.addEventListener('touchcancel', touchcancel);
}
function unregister() {
const dom = document.querySelector(pDomSelector) as HTMLDivElement;
if (!dom) return;
dom.removeEventListener('touchstart', touchstart);
dom.removeEventListener('touchmove', touchmove);
dom.removeEventListener('touchend', touchend);
dom.removeEventListener('touchcancel', touchcancel);
}
onBeforeUnmount(() => {
unregister();
});
return {
register,
unregister,
};
}
Camera.vue
<template>
<div class="video-wrap">
<canvas ref="canvasRef" class="item"></canvas>
<video ref="videoRef" class="item"></video>
<t-loading v-if="loading" size="small" class="loading" text="视频加载中……"></t-loading>
</div>
</template>
<script setup lang="ts">
import { shallowRef, onBeforeUnmount, ref } from 'vue';
import { createElement } from '@cloudbase/utils';
import { urlInfo, channelEnum, streamEnum } from './CONSTANST';
const options = {
wsURL: `${urlInfo.cameraConnProtocol}://${urlInfo.cameraConnHost}:${urlInfo.cameraConnWSPort}/rtspoverwebsocket`,
/**
* subtype 流的枚举 0 主码流,1 辅码流1,(2辅码流2 不可用)
*/
rtspURL: `rtsp://${urlInfo.cameraConnHost}:${urlInfo.cameraConnRTSPPort}/cam/realmonitor?channel=1&subtype=0&proto=Private3`,
username: 'username', // TODO
password: 'password, // TODO
/**
* h264 video 解码
* h265 canvas 解码
*/
h265AccelerationEnabled: true,
};
const canvasRef = shallowRef(null);
const videoRef = shallowRef(null);
const loading = ref(false);
let player = null;
const defaultCameraConf = { channel: channelEnum.normal, subtype: streamEnum.main };
async function init(cameraOpt = defaultCameraConf) {
loading.value = true;
await loadCameraPlayerControlPlugin();
const canvas = canvasRef.value;
const video = videoRef.value;
// 拼接摄像机拉流地址信息
const rtspURL = `rtsp://${urlInfo.cameraConnHost}:${urlInfo.cameraConnRTSPPort}/cam/realmonitor?channel=${
cameraOpt.channel || channelEnum.normal
}&subtype=${cameraOpt.subtype || streamEnum.main}&proto=Private3`;
options.rtspURL = rtspURL;
player = new (window as any).PlayerControl(options);
player.on('WorkerReady', () => {
player.connect(); // 启动ws推流链接
});
player.on('DecodeStart', (rs) => {
// console.log('start decode', rs);
if (rs.decodeMode === 'video') {
video.style.zIndex = '2';
canvas.style.zIndex = '1';
} else {
video.style.zIndex = '1';
canvas.style.zIndex = '2';
}
});
player.on('PlayStart', () => {
setTimeout(() => {
loading.value = false;
}, 150);
});
player.on('Error', (err) => {
loading.value = false;
console.log('camer live err:', err);
});
player.init(canvas, video);
}
onBeforeUnmount(() => {
if (player && player.close) {
player.close(); // 关闭ws推流链接
player = null;
}
});
defineExpose({
init,
});
</script>
<script lang="ts">
async function loadCameraPlayerControlPlugin() {
const cameraPlayerControl = document.querySelector('#cameraPlayerControl');
if (!cameraPlayerControl) {
await createElement(document.body, 'script', { src: '/module/PlayerControl.js', id: 'cameraPlayerControl' });
}
}
export {
/**
* 加载 照相机播放插件
*/
loadCameraPlayerControlPlugin,
};
</script>
<style lang="less" scoped>
.video-wrap {
position: relative;
video {
object-fit: fill;
}
.item {
position: absolute;
width: 100%;
height: 100%;
border-radius: 5px;
}
.loading {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(255, 255, 255, 0.35);
z-index: 2;
display: flex;
flex-direction: column;
:deep(.t-loading__text) {
font-size: 0.75rem;
}
}
}
</style>
index.vue
<template>
<Transition>
<div
v-if="visible"
:id="cameraLiveDomID"
class="camera-live-wrapper"
:style="{
left: cameraLiveDomStyle.defaultLeft,
top: cameraLiveDomStyle.defaultTop,
width: `${cameraLiveDomStyle.defaultWidth}px`,
height: `${cameraLiveDomStyle.defaultHeight}px`,
}"
>
<t-close-icon name="close-icon" class="close-icon" @click="close" />
<div class="cus-dialog-header">
<div id="cameraDomMoveIcon" class="item move"><t-move-icon />移动</div>
<div class="item" @click="zoomMin(zoomMinEnum.moveLbMin)">缩小至左下角</div>
<div class="item" @click="zoomMin(zoomMinEnum.moveRbMin)">缩小至右下角</div>
<t-button
variant="text"
class="item"
theme="primary"
size="small"
:loading="capturing"
@click="handleCapturePicture"
>
抓拍
</t-button>
</div>
<div class="cus-dialog-container">
<camera ref="cameraRef" />
<camera ref="cameraThermRef" />
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, shallowRef, nextTick } from 'vue';
import { MessagePlugin } from 'tdesign-vue-next';
import { CloseIcon as TCloseIcon, MoveIcon as TMoveIcon } from 'tdesign-icons-vue-next';
import { Robot, RobotCameraAlarmMsg } from '@cloudbase/api/dcos';
import Camera, { loadCameraPlayerControlPlugin } from './Camera.vue';
import { channelEnum, streamEnum, zoomMinEnum, cameraLiveDomStyle } from './CONSTANST';
import { DomZoomScale, TouchScale } from './useDomResize';
import { DomZoomMove, TouchMove } from './useDomMove';
/**
* 标识是否为触摸屏设备
*/
const isTouchDevice = 'ontouchstart' in window;
let domMove;
let domScale;
const cameraLiveDomID = 'cameraLiveWrapper';
if (isTouchDevice) {
domMove = TouchMove(`#${cameraLiveDomID}`);
domScale = TouchScale(`#${cameraLiveDomID}`); // 移动端两指缩放
} else {
domMove = DomZoomMove(`#${cameraLiveDomID}`); // pc端 鼠标左键按住 #cameraDomMoveIcon 元素进行移动
domScale = DomZoomScale(`#${cameraLiveDomID}`);
}
let robotOverviewInfo: Robot.TsRobotViewVO = {} as any;
const visible = ref(false);
function showDialog(info: Robot.TsRobotViewVO) {
robotOverviewInfo = info;
visible.value = true;
initCamera();
nextTick(() => {
domMove.register('#cameraDomMoveIcon');
domScale.register();
});
}
function close() {
visible.value = false;
const event = new CustomEvent('changeCameraLiveStatus', {
detail: { cameraLiveStatus: false },
});
window.dispatchEvent(event);
}
const cameraRef = shallowRef(null);
const cameraThermRef = shallowRef(null);
async function initCamera() {
await loadCameraPlayerControlPlugin();
cameraRef.value?.init({ channel: channelEnum.normal, subtype: streamEnum.auxiliary1 });
cameraThermRef.value?.init({ channel: channelEnum.therm, subtype: streamEnum.auxiliary1 });
}
function zoomMin(type: zoomMinEnum) {
const dom = document.getElementById('cameraLiveWrapper');
if (dom) {
dom.style.minWidth = `${cameraLiveDomStyle.minW}px`;
dom.style.width = `${cameraLiveDomStyle.minW}px`;
dom.style.height = `${cameraLiveDomStyle.minH}px`;
const top = +(((window.innerHeight - cameraLiveDomStyle.minH) / window.innerHeight) * 100).toFixed(2);
dom.style.top = `${top}%`;
if (type === zoomMinEnum.moveLbMin) {
dom.style.left = '0';
} else {
const left = +(((window.innerWidth - cameraLiveDomStyle.minW) / window.innerWidth) * 100).toFixed(2);
dom.style.left = `${left}%`;
}
}
}
const capturing = ref(false);
function handleCapturePicture() {
capturing.value = true;
RobotCameraAlarmMsg.requestCapturePicture({ robotId: robotOverviewInfo.id }).then(() => {
capturing.value = false;
MessagePlugin.success('抓拍成功');
});
}
defineExpose({
showDialog,
});
</script>
<style scoped lang="less">
.camera-live-wrapper {
transform: translateZ(0);
position: absolute;
z-index: 999;
padding: 5px;
padding-top: 25px;
min-width: 280px;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 10px;
filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.3));
.cus-dialog-header {
position: absolute;
z-index: 5;
top: 0;
left: 0;
display: flex;
align-items: center;
.item {
cursor: pointer;
padding: 5px;
user-select: none;
font-size: 12px;
}
:deep(.t-button--variant-text) {
&:hover,
&:focus-visible {
background: none;
border-color: transparent;
}
}
.move {
padding-left: 4px;
cursor: move;
display: flex;
align-items: center;
:deep(.icon) {
width: 18px;
height: 18px;
}
}
}
.cus-dialog-container {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
column-gap: 5px;
:deep(.video-wrap) {
width: 50%;
height: 100%;
flex-grow: 1;
}
}
.close-icon {
position: absolute;
z-index: 9;
top: 5px;
right: 5px;
width: 20px;
height: 20px;
padding: 4px;
background-color: #fff;
border-radius: 50%;
box-shadow: 2px 2px 16px -10px #000;
cursor: pointer;
&:hover {
transform: scale(1.25);
transition: all 0.35;
}
}
}
.v-enter-active,
.v-leave-active {
transition: all 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
transform: translateY(-30px);
}
</style>
useCamera.ts
函数的方式调用(可全局挂载)
import { ref, createApp, h } from 'vue';
import CameraLive from './index.vue';
interface RobotViewVO {
}
/**
* 摄像机组件实例
*/
let cameraLiveIns;
let app;
const showCameraLiveDialog = ref(false);
export function useCreateCameraLive() {
function openCameraLive(info: RobotViewVO) {
if (!document.getElementById('cameraLiveBox')) {
const dom = document.createElement('div');
dom.id = 'cameraLiveBox';
document.body.append(dom);
}
app = createApp({
render() {
cameraLiveIns = h(CameraLive);
return cameraLiveIns;
},
});
app.mount('#cameraLiveBox');
// console.log(cameraLiveIns);
if (cameraLiveIns.component?.exposed?.showDialog) {
cameraLiveIns.component.exposed.showDialog(info);
window.addEventListener('changeCameraLiveStatus', closeCameraLive);
}
}
function closeCameraLive() {
showCameraLiveDialog.value = false;
window.removeEventListener('changeCameraLiveStatus', closeCameraLive);
const dom = document.getElementById('cameraLiveBox');
if (dom) {
dom.remove();
}
cameraLiveIns = null;
if (app) {
app.unmount();
app = null;
}
}
return {
openCameraLive,
closeCameraLive,
showCameraLiveDialog,
};
}