<?php include("./link/session.php") ?><!doctype html><html lang="uft-8"><head><?php include("./public/head.inc") ?><link href="assets/plugins/nouislider/css/nouislider.min.css" rel="stylesheet"></head><body><?php include("./public/menu.inc") ?><div data-simplebar><main class="page-content decode" id="app" v-cloak><div class="row"><div class="col-lg-12 mx-auto"><div class="card"><div class="card-header bg-transparent"><div class="p-2 mb-0 d-flex align-items-end"><cn>全局配置</cn><en>Overall config</en></div></div><div class="card-body overflow-auto"><div class="row"><div class="col-lg-12" v-if="Object.keys(globalConf).length !== 0"><div class="row"><div class="col-2 text-center"></div><div class="col-2 text-center"><cn>帧率</cn><en>framerate</en></div><div class="col text-center"><cn>缓冲模式</cn><en>buffer mode</en></div><div class="col-2 text-center"><cn>缓冲时间</cn><en>buffer time</en></div><div class="col text-center"><cn>协议</cn><en>protocol</en></div><div class="col text-center"><cn>视频解码</cn><en>video decode</en></div><div class="col text-center"><cn>音频解码</cn><en>audio decode</en></div><div class="col text-center"></div></div><hr><div class="row mt-1"><div class="col-lg-12"><div class="row"><div class="col-2 text-center p-0 pt-2"><cn>拉流配置</cn><en>Pull config</en></div><div class="col-2"><input type="text" class="form-control"v-model.trim.lazy="globalConf.net.framerate"></div><div class="col"><select class="form-select" v-model="globalConf.net.bufferMode"><option value="0" cn="一般" en="Normal" v-language-option></option><option value="1" cn="实时" en="NoBuffer" v-language-option></option><option value="2" cn="缓冲" en="Buffer" v-language-option></option><option value="3" cn="帧同步" en="Sync" v-language-option></option></select></div><div class="col-2"><input type="text" class="form-control"v-model.trim.lazy="globalConf.net.minDelay"></div><div class="col"><select class="form-select" v-model="globalConf.net.protocol"><option value="udp">UDP</option><option value="tcp">TCP</option></select></div><div class="col lp-align-center"><bs-switch v-model="globalConf.net.decodeV"></bs-switch></div><div class="col lp-align-center"><bs-switch v-model="globalConf.net.decodeA"></bs-switch></div><div class="col lp-align-center"></div></div><hr></div></div><div class="row text-center mt-3"><div class="col-lg-12"><button type="button" class="btn border-3 btn-primary me-2"@click="saveGlobalConfByLocal"><cn>应用到全部</cn><en>Apply to all</en></button></div></div></div></div></div></div></div></div><div class="row"><div class="col-lg-12 mx-auto"><ul class="nav nav-tabs nav-primary" role="tablist"><li class="nav-item" role="presentation" @click="tabType = 'net'"><a class="nav-link active" data-bs-toggle="tab" href="#tab1" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-brands fa-internet-explorer me-1"></i></div><div class="tab-title"><cn>网络拉流</cn><en>Network stream</en></div></div></a></li><li v-if="Object.keys(hardwareConf).length > 0 && hardwareConf.function.carousel" class="nav-item"role="presentation" @click="tabType = 'file'"><a class="nav-link" data-bs-toggle="tab" href="#tab2" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-regular fa-file-audio me-1"></i></div><div class="tab-title"><cn>文件轮播</cn><en>File carousel</en></div></div></a></li><li v-if="Object.keys(hardwareConf).length > 0 && hardwareConf.function.carousel" class="nav-item"role="presentation" @click="tabType = 'image'"><a class="nav-link" data-bs-toggle="tab" href="#tab3" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-regular fa-file-audio me-1"></i></div><div class="tab-title"><cn>图片轮播</cn><en>Image carousel</en></div></div></a></li><li class="nav-item" role="presentation" @click="tabType = 'rtmp'"><a class="nav-link" data-bs-toggle="tab" href="#tab4" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-solid fa-arrow-down-short-wide me-1"></i></div><div class="tab-title"><cn>接收rtmp流</cn><en>Receive rtmp</en></div></div></a></li><li class="nav-item" role="presentation" @click="tabType = 'srt'"><a class="nav-link" data-bs-toggle="tab" href="#tab5" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-solid fa-arrow-down-short-wide me-1"></i></div><div class="tab-title"><cn>接收srt流</cn><en>Receive srt</en></div></div></a></li><li v-if="Object.keys(hardwareConf).length > 0 && hardwareConf.function.ndi" class="nav-item" role="presentation" @click="tabType = 'ndi'"><a class="nav-link" data-bs-toggle="tab" href="#tab6" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-brands fa-audible me-1"></i></div><div class="tab-title"><cn>NDI解码</cn><en>Decode ndi</en></div></div></a></li><li class="nav-item" role="presentation" @click="tabType = 'crop'"><a class="nav-link" data-bs-toggle="tab" href="#tab7" role="tab" aria-selected="false"><div class="d-flex align-items-center"><div class="tab-icon"><i class="fa-solid fa-crop-simple me-1"></i></div><div class="tab-title"><cn>旋转裁剪</cn><en>crop</en></div></div></a></li></ul><div class="tab-content py-3 pe-2 ps-2"><div class="tab-pane fade show active" id="tab1" role="tabpanel"><div class="row"><div class="col-2 text-center"><cn>频道名称</cn><en>channel name</en></div><div class="col-3 text-center"><cn>拉流地址</cn><en>stream url</en></div><div class="col text-center"><cn>帧率</cn><en>framerate</en></div><div class="col text-center"><cn>缓冲模式</cn><en>buffer mode</en></div><div class="col text-center"><cn>缓冲时间</cn><en>buffer time</en></div><div class="col text-center"><cn>协议</cn><en>protocol</en></div><div class="col text-center"><cn>视频解码</cn><en>video decode</en></div><div class="col text-center"><cn>音频解码</cn><en>audio decode</en></div><div class="col text-center"><cn>开关</cn><en>enable</en></div><div class="col text-center" v-if="Object.keys(hardwareConf).length > 0"><div v-if="hardwareConf.fac === 'VGA1'"><cn>VGA</cn><en>VGA</en></div><div v-else-if="hardwareConf.fac === 'ENCS1'"><cn>SDI</cn><en>SDI</en></div><div v-else-if="hardwareConf.fac === 'ENCD1'"><cn>DP</cn><en>DP</en></div><div v-else><cn>HDMI</cn><en>HDMI</en></div></div></div><hr><div class="row mt-1" v-for="(item,index) in handleNetConf" :key="item.id"><div class="col-lg-12"><div class="row"><div class="col-2 text-center"><input type="text" class="form-control" v-model.trim.lazy="item.name"></div><div class="col-3"><input type="text" class="form-control" v-model.trim.lazy="item.net.path"></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.net.framerate"></div><div class="col"><select class="form-select" v-model="item.net.bufferMode"><option value="0" cn="一般" en="Normal" v-language-option></option><option value="1" cn="实时" en="NoBuffer" v-language-option></option><option value="2" cn="缓冲" en="Buffer" v-language-option></option><option value="3" cn="帧同步" en="Sync" v-language-option></option></select></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.net.minDelay"></div><div class="col"><select class="form-select" v-model="item.net.protocol"><option value="udp">UDP</option><option value="tcp">TCP</option></select></div><div class="col lp-align-center"><bs-switch v-model="item.net.decodeV"></bs-switch></div><div class="col lp-align-center"><bs-switch v-model="item.net.decodeA"></bs-switch></div><div class="col lp-align-center"><bs-switch v-model="item.enable"></bs-switch></div><div class="col lp-align-center"><button type="button" class="btn btn-primary border-1 px-3"@click="onDisplayHdmi(item.id,item.enable)"><cn>输出</cn><en>display</en></button></div></div><hr></div></div></div><div class="tab-pane fade" id="tab2" role="tabpanel"><div class="row"><div class="col-2 text-center"><cn>频道名称</cn><en>channel name</en></div><div class="col text-center"><cn>视频解码</cn><en>video decode</en></div><div class="col text-center"><cn>音频解码</cn><en>audio decode</en></div><div class="col text-center"><cn>开关</cn><en>enable</en></div><div class="col text-center" v-if="Object.keys(hardwareConf).length > 0"><div v-if="hardwareConf.fac === 'VGA1'"><cn>VGA</cn><en>VGA</en></div><div v-else-if="hardwareConf.fac === 'ENCS1'"><cn>SDI</cn><en>SDI</en></div><div v-else-if="hardwareConf.fac === 'ENCD1'"><cn>DP</cn><en>DP</en></div><div v-else><cn>HDMI</cn><en>HDMI</en></div></div><div class="col-6"></div></div><hr><div class="row mt-1"><div class="col-lg-12"><div class="row" v-if="Object.keys(handleVideoFileConf).length > 0"><div class="col-2 text-center"><input type="text" class="form-control" v-model.trim.lazy="handleVideoFileConf.name"></div><div class="col lp-align-center"><bs-switch v-model="handleVideoFileConf.decodeV"></bs-switch></div><div class="col lp-align-center"><bs-switch v-model="handleVideoFileConf.decodeA"></bs-switch></div><div class="col lp-align-center"><bs-switch v-model="handleVideoFileConf.enable"></bs-switch></div><div class="col lp-align-center"><button type="button" class="btn btn-primary border-1 px-3"@click="onDisplayHdmi(handleVideoFileConf.id,handleVideoFileConf.enable)"><cn>输出</cn><en>display</en></button></div><div class="col-6"></div></div></div></div><div class="row mt-4"><div class="col-12"><div class="card" style="border-left: none;border-right: none"><div class="card-body"><div class="row"><div class="col text-center"><cn>序号</cn><en>Num.</en></div><div class="col-3 text-center"><cn>文件名称</cn><en>File name</en></div><div class="col text-center"><cn>时长</cn><en>Duration</en></div><div class="col-4 text-center"><cn>时间轴</cn><en>Timeline</en></div><div class="col-3 text-center"><cn>操作</cn><en>Option</en></div></div><hr><div class="row" v-for="(item,index) in handleVideoFileConf.file"><div class="col text-center pt-2">{{index+1}}</div><div class="col-3"><select class="form-select" v-model="handleVideoFileConf.file[index]"><option cn="无" en="none" value="" v-language-option></option><option v-for="(it,idx) in handleUsbVideoFile" :key="idx" :value="it.path">{{it.name}}</option></select></div><div class="col text-center pt-2">{{formatTime(onHandleFileDuration(item))}}</div><div class="col-4 pt-3"><noui-slider :func-value="onHandleFilePosition(item,'file')" :min="0":max="onHandleFileDuration(item)" :step="1000" :fix="2":format="'time'" :index="index"@slide-end="onTimelineSliderEnd"></noui-slider></div><div class="col-3 text-center"><button type="button" class="btn btn-primary px-3" @click="onMediaFileOption('up',index)"><i class="fa-solid fa-arrow-up"></i></button><button type="button" class="btn btn-primary px-3 ms-2"@click="onMediaFileOption('down',index)"><i class="fa-solid fa-arrow-down"></i></button><button type="button" class="btn btn-primary px-3 ms-2"@click="onMediaFileOption('play',index)"><i class="fa-solid fa-play"></i></button><button type="button" class="btn btn-primary px-3 ms-2"@click="onMediaFileOption('del',index)"><i class="fa-solid fa-trash-can"></i></button></div><hr></div></div></div></div></div></div><div class="tab-pane fade" id="tab3" role="tabpanel"><div class="row"><div class="col-2 text-center"><cn>频道名称</cn><en>channel name</en></div><div class="col text-center"><cn>开关</cn><en>enable</en></div><div class="col text-center" v-if="Object.keys(hardwareConf).length > 0"><div v-if="hardwareConf.fac === 'VGA1'"><cn>VGA</cn><en>VGA</en></div><div v-else-if="hardwareConf.fac === 'ENCS1'"><cn>SDI</cn><en>SDI</en></div><div v-else-if="hardwareConf.fac === 'ENCD1'"><cn>DP</cn><en>DP</en></div><div v-else><cn>HDMI</cn><en>HDMI</en></div></div><div class="col-8"></div></div><hr><div class="row mt-1"><div class="col-lg-12"><div class="row" v-if="Object.keys(handleImageFileConf).length > 0"><div class="col-2 text-center"><input type="text" class="form-control" v-model.trim.lazy="handleImageFileConf.name"></div><div class="col lp-align-center"><bs-switch v-model="handleImageFileConf.enable"></bs-switch></div><div class="col lp-align-center"><button type="button" class="btn btn-primary border-1 px-3" @click="onDisplayHdmi(handleImageFileConf.id,handleImageFileConf.enable)"><cn>输出</cn><en>display</en></button></div><div class="col-8"></div></div></div></div><div class="row mt-4"><div class="col-12"><div class="card" style="border-left: none;border-right: none"><div class="card-body"><div class="row"><div class="calc-24-2 text-center"><cn>序号</cn><en>Num.</en></div><div class="calc-24-4 text-center"><cn>图片名称</cn><en>File name</en></div><div class="calc-24-3 text-center"><cn>保持模式</cn><en>Hold mode</en></div><div class="calc-24-3 text-center"><cn>持续时间</cn><en>Hold time</en></div><div class="calc-24-1"></div><div class="calc-24-6 text-center"><cn>时间轴</cn><en>Timeline</en></div><div class="calc-24-5 text-center"><cn>操作</cn><en>Option</en></div></div><hr><div class="row mt-2" v-for="(item,index) in handleImageFileConf.image"><div class="calc-24-2 text-center pt-2">{{index+1}}</div><div class="calc-24-4"><select class="form-select" v-model="item.path"><option cn="无" en="none" value="" v-language-option></option><option v-for="(it,idx) in handleUsbImageFile" :key="idx" :value="it.path">{{it.name}}</option></select></div><div class="calc-24-3"><select class="form-select" v-model="item.hold"><option cn="固定时长" en="fixed duration" value="dur" v-language-option></option></select></div><div class="calc-24-3 text-center"><div class="input-group"><input type="text" class="form-control" v-model="item.duration"><span class="input-group-text input-group-addon"><cn>秒</cn><en>sec</en></span></div></div><div class="calc-24-1"></div><div class="calc-24-6 pt-3"><noui-slider :func-value="onHandleFilePosition(item.path,'image')" :min="0":max="item.duration*1000" :step="1000" :fix="2":format="'time'" :index="index"disabled></noui-slider></div><div class="calc-24-5 text-center"><button type="button" class="btn btn-primary px-3"@click="onMediaFileOption('up',index)"><i class="fa-solid fa-arrow-up"></i></button><button type="button" class="btn btn-primary px-3 ms-2"@click="onMediaFileOption('down',index)"><i class="fa-solid fa-arrow-down"></i></button><button type="button" class="btn btn-primary px-3 ms-2"@click="onMediaFileOption('play',index)"><i class="fa-solid fa-play"></i></button><button type="button" class="btn btn-primary px-3 ms-2"@click="onMediaFileOption('del',index)"><i class="fa-solid fa-trash-can"></i></button></div><hr></div></div></div></div></div></div><div class="tab-pane fade" id="tab4" role="tabpanel"><div class="row"><div class="col-2 text-center"><cn>描述</cn><en>description</en></div><div class="col-3 text-center"><cn>rtmp 地址</cn><en>rtmp address</en></div><div class="col-2 text-center"><cn>推流码</cn><en>stream key</en></div><div class="col text-center"><cn>解码频道</cn><en>decode channel</en></div><div class="col text-center"><cn>用户名</cn><en>username</en></div><div class="col text-center"><cn>密码</cn><en>password</en></div><div class="col text-center"><cn>启用认证</cn><en>auth</en></div><div class="col text-center"><cn>操作</cn><en>option</en></div></div><hr><div class="row mt-1" v-for="(item,index) in rxPushConf" :key="item.id"><div class="col-lg-12"><div class="row"><div class="col-2 text-center"><input type="text" class="form-control" v-model.trim.lazy="item.desc"></div><div class="col-3"><div class="input-group"><input class="form-control" v-model.trim.lazy="item.server" disabled readonly><button class="btn btn-primary input-group-text input-group-addon lp-cursor-pointer"@click="onCopyReceiveChnUrl(index,'server')"><i class="fa-regular fa-copy"></i></button></div></div><div class="col-2"><div class="input-group"><input class="form-control" v-model.trim.lazy="item.key" disabled readonly><button class="btn btn-primary input-group-text input-group-addon lp-cursor-pointer"@click="onResetRXPushChnUrl(index)"><i class="fa-solid fa-repeat"></i></button><button class="btn btn-primary input-group-text input-group-addon lp-cursor-pointer ms-1"@click="onCopyReceiveChnUrl(index,'key')"><i class="fa-regular fa-copy"></i></button></div></div><div class="col"><select class="form-select" v-model="item.bind"><option v-if="handleNetConf.length > 0" v-for="(it,index) in handleNetConf":key="index" :value="it.id">{{it.name}}</option><option value="-1" cn="关闭" en="Close" v-language-option></option></select></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.uname"></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.passwd"></div><div class="col lp-align-center"><bs-switch v-model="item.auth"></bs-switch></div><div class="col lp-align-center"><button type="button" class="btn btn-primary border-1 px-3"@click="onDelReceiveChn(index)"><cn>删除</cn><en>delete</en></button></div></div><hr></div></div><div class="row mt-3"><div class="col-lg-12 tips"><cn>1、设备作为流媒体服务器使用,可以接收其他设备推送的 RTMP 流。</cn><en>1. The device is used as a streaming media server and can receive RTMP streams pushed by other devices.</en></div><div class="col-lg-12 tips"><cn>2、设置解码频道后,保存时会自动把该频道的拉流地址替换为对应的解码地址。</cn><en>2. After the decoding channel is set, the stream address of the correspondingchannel is automatically replaced with the current decoding address when saving.</en></div><div class="col-lg-12 tips"><cn>3、设置解码频道并保存后,请返回【网络拉流】标签页,根据使用场景打开/关闭音视频解码。</cn><en>3. After setting the decoding channel and saving, please go back to the NetworkStreaming tab. From there, you can enable or disable audio and video decoding as needed.</en></div><div class="col-lg-12 tips"><cn>4、如需完整的推流地址,可以使用rtmp 地址 + 推流码拼接,如 rtmp://{{window.location.hostname}}/live/push1</cn><en>4. For the full stream URL, combine the RTMP address with the stream key, e.g. rtmp://{{window.location.hostname}}/live/push1.</en></div></div></div><div class="tab-pane fade" id="tab5" role="tabpanel"><div class="row"><div class="col-2 text-center"><cn>描述</cn><en>description</en></div><div class="col-5 text-center"><cn>推流地址</cn><en>recive url</en></div><div class="col text-center"><cn>解码频道</cn><en>decode channel</en></div><div class="col text-center"><cn>端口</cn><en>port</en></div><div class="col text-center"><cn>密码</cn><en>password</en></div><div class="col text-center"><cn>延时</cn><en>delay</en></div><div class="col text-center"><cn>Stream ID</cn><en>stream id</en></div><div class="col text-center"><cn>操作</cn><en>option</en></div></div><hr><div class="row mt-1" v-for="(item,index) in srtPushConf" :key="item.id"><div class="col-lg-12"><div class="row"><div class="col-2 text-center"><input type="text" class="form-control" v-model.trim.lazy="item.desc"></div><div class="col-5"><div class="input-group"><input class="form-control" v-model.trim.lazy="item.url" disabled readonly><button class="btn btn-primary input-group-text input-group-addon lp-cursor-pointer"@click="onCopyReceiveChnUrl(index)"><i class="fa-regular fa-copy"></i></button></div></div><div class="col"><select class="form-select" v-model="item.bind"><option v-if="handleNetConf.length > 0" v-for="(it,index) in handleNetConf":key="index" :value="it.id">{{it.name}}</option><option value="-1" cn="关闭" en="Close" v-language-option></option></select></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.port"></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.passphrase"></div><div class="col"><input type="text" class="form-control" v-model.trim.lazy="item.latency"></div><div class="col lp-align-center"><input type="text" class="form-control" v-model.trim.lazy="item.streamid"></div><div class="col lp-align-center"><button type="button" class="btn btn-primary border-1 px-3"@click="onDelReceiveChn(index)"><cn>删除</cn><en>delete</en></button></div></div><hr></div></div><div class="row mt-3"><div class="col-lg-12 tips"><cn>1、设备作为流媒体服务器使用,可以接收其他设备推送的 SRT 流。</cn><en>1. The device is used as a streaming media server and can receive SRT streams pushed by other devices.</en></div><div class="col-lg-12 tips"><cn>2、设置解码频道后,保存时会自动把该频道的拉流地址替换为对应的解码地址。</cn><en>2. After the decoding channel is set, the stream address of the correspondingchannel is automatically replaced with the current decoding address when saving.</en></div><div class="col-lg-12 tips"><cn>3、设置解码频道并保存后,请返回【网络拉流】标签页,根据使用场景打开/关闭音视频解码。</cn><en>3. After setting the decoding channel and saving, please go back to the NetworkStreaming tab. From there, you can enable or disable audio and video decoding as needed.</en></div></div></div><div class="tab-pane fade" id="tab6" role="tabpanel"><div class="row"><div class="col-2 text-center"><cn>描述</cn><en>description</en></div><div class="col-5 text-center"><cn>NDI源</cn><en>ndi source</en></div><div class="col-1 text-center"><cn>解码频道</cn><en>decode channel</en></div><div class="col-1 text-center"><cn>操作</cn><en>option</en></div></div><hr><div class="row mt-1" v-for="(item,index) in ndiReciveConf" :key="item.id"><div class="col-lg-12"><div class="row"><div class="col-2 text-center"><input type="text" class="form-control" v-model.trim.lazy="item.desc"></div><div class="col-5"><div class="input-group"><select class="form-select" v-model="item.url"><option v-for="it in ndiList" :value="it">{{it}}</option></select><button class="btn btn-primary input-group-text input-group-addon lp-cursor-pointer"@click="refreshNdiSourceList"><i class="fa-solid fa-repeat"></i></button></div></div><div class="col-1"><select class="form-select" v-model="item.bind"><option v-if="handleNetConf.length > 0" v-for="(it,index) in handleNetConf":key="index" :value="it.id">{{it.name}}</option><option value="-1" cn="关闭" en="Close" v-language-option></option></select></div><div class="col-1 lp-align-center"><button type="button" class="btn btn-primary border-1 px-3"@click="onDelReceiveChn(index)"><cn>删除</cn><en>delete</en></button></div></div><hr></div></div><div class="row mt-3"><div class="col-lg-12 tips"><cn>1、设备可以扫描并发现网络内的 NDI 流。</cn><en>1. The device can scan and discover NDI streams on the network.</en></div><div class="col-lg-12 tips"><cn>2、设置解码频道后,保存时会自动把该频道的拉流地址替换为对应的解码地址。</cn><en>2. After the decoding channel is set, the stream address of the correspondingchannel is automatically replaced with the current decoding address when saving.</en></div><div class="col-lg-12 tips"><cn>3、设置解码频道并保存后,请返回【网络拉流】标签页,根据使用场景打开/关闭音视频解码。</cn><en>3. After setting the decoding channel and saving, please go back to the NetworkStreaming tab. From there, you can enable or disable audio and video decoding as needed.</en></div></div></div><div class="tab-pane fade" id="tab7" role="tabpanel"><div class="row"><div class="calc-18-3 text-center"><cn>频道名称</cn><en>channel name</en></div><div class="calc-18-2 text-center"><cn>旋转</cn><en>rotate</en></div><div class="calc-18-2 text-center"><cn>左裁剪</cn><en>left crop</en></div><div class="calc-18-2 text-center"><cn>右裁剪</cn><en>right crop</en></div><div class="calc-18-2 text-center"><cn>上裁剪</cn><en>top crop</en></div><div class="calc-18-2 text-center"><cn>下裁剪</cn><en>bottom crop</en></div></div><hr ><div class="row mt-1" v-for="(item,index) in handleVdoConf" :key="item.id"><div class="col-lg-12"><div class="row"><div class="calc-18-3 text-center"><input type="text" class="form-control" v-model.trim.lazy="item.name"></div><div class="calc-18-2"><select class="form-select" v-model="item.cap.rotate"><option value="0">0</option><option value="90">90</option><option value="180">180</option><option value="270">270</option></select></div><div class="calc-18-2"><input type="text" class="form-control" v-model.trim.lazy="item.cap.crop.L"></div><div class="calc-18-2"><input type="text" class="form-control" v-model.trim.lazy="item.cap.crop.R"></div><div class="calc-18-2"><input type="text" class="form-control" v-model.trim.lazy="item.cap.crop.T"></div><div class="calc-18-2"><input type="text" class="form-control" v-model.trim.lazy="item.cap.crop.B"></div></div><hr ></div></div><div class="row mt-3"><div class="col-lg-12 tips"><cn>1、开启视频解码后,可以对原始时间视频数据进行旋转裁剪的操作</cn><en>1. After enabling video decoding, operations such as rotating and cropping can be performed on the original video data.</en></div><div class="col-lg-12 tips"><cn>2、旋转裁剪效果在接口输出和串流输出均生效。</cn><en>2.The rotation and cropping effects take effect in both the interface output and the streaming output.</en></div></div></div><div class="row mt-3"><div class="col-lg-12 text-center" v-if="tabType!=='rtmp' && tabType!=='srt' && tabType!=='ndi'"><button type="button" class="btn btn-primary border-3 px-5 me-2" v-if="tabType==='file' || tabType==='image'" @click="onAddMediaFile"><cn>添加</cn><en>Add</en></button><button type="button" class="btn btn-primary border-3 px-5" @click="saveDefaultConf"><cn>保存</cn><en>Save</en></button></div><div class="col-lg-12 text-center" v-if="tabType==='rtmp'"><button type="button" class="btn btn-primary border-3 px-5 me-2" @click="onAddReceiveChn"><cn>添加</cn><en>Add</en></button><button type="button" class="btn btn-primary border-3 px-5" @click="saveReceiveChnConf"><cn>保存</cn><en>Save</en></button></div><div class="col-lg-12 text-center" v-if="tabType==='srt'"><button type="button" class="btn btn-primary border-3 px-5 me-2" @click="onAddReceiveChn"><cn>添加</cn><en>Add</en></button><button type="button" class="btn btn-primary border-3 px-5" @click="saveReceiveChnConf"><cn>保存</cn><en>Save</en></button></div><div class="col-lg-12 text-center" v-if="tabType==='ndi'"><button type="button" class="btn btn-primary border-3 px-5 me-2" @click="onAddReceiveChn"><cn>添加</cn><en>Add</en></button><button type="button" class="btn btn-primary border-3 px-5" @click="saveReceiveChnConf"><cn>保存</cn><en>Save</en></button></div></div></div></div></div></main></div><?php include("./public/foot.inc") ?><script type="module">import {rpc, extend, deepCopy, swap, clearReactiveArray, clearReactiveObject, formatTime, alertMsg,isEmpty} from "./assets/js/lp.utils.js?hash=e82520f08";import {useDefaultConf, useUsbFilesConf, useHardwareConf, useReceiverConf} from "./assets/js/vue.hooks.js?hash=89523e901";import {ignoreCustomElementPlugin, filterKeywordPlugin, bootstrapSwitchComponent, multipleSelectComponent, nouiSliderComponent, languageOptionDirective} from "./assets/js/vue.helper.js?hash=68eefc36f"import {md5} from "./assets/plugins/md5/js.md5.esm.js";import vue from "./assets/js/vue.build.js?hash=879ea7dbc";const {createApp, ref, reactive, watch, computed, onMounted} = vue;const app = createApp({directives: {"language-option": languageOptionDirective},components: {"bs-switch": bootstrapSwitchComponent,"multiple-select": multipleSelectComponent,"noui-slider": nouiSliderComponent},setup(props, context) {const {defaultConf, updateDefaultConf} = useDefaultConf();const {hardwareConf} = useHardwareConf();const {receiverConf,updateReceiverConf} = useReceiverConf();const {usbFiles} = useUsbFilesConf();const state = {globalConf: reactive({}),tabType: ref("net"),playList: reactive([]),playPosition: reactive({}),imagePlayPosition:reactive({}),ndiList: reactive([]),rxPushConf: reactive([]),srtPushConf: reactive([]),ndiReciveConf: reactive([])}const unDefaultConf = watch(defaultConf, () => {for (let i = 0; i < defaultConf.length; i++) {if (defaultConf[i].type === "net" && Object.keys(state.globalConf).length === 0) {Object.assign(state.globalConf, deepCopy(defaultConf[i]));unDefaultConf();break;}}});const unReceiverConf = watch(receiverConf, () => {if(!isEmpty(receiverConf)) {state.rxPushConf.splice(0, state.rxPushConf.length, ...receiverConf.rtmp)state.srtPushConf.splice(0, state.srtPushConf.length, ...receiverConf.srt)state.ndiReciveConf.splice(0, state.ndiReciveConf.length, ...receiverConf.ndi)unReceiverConf();}});watch(() => state.rxPushConf, list => {list.forEach(item => {item.server = `rtmp://${location.hostname}/live/`;const urlObject = new URL(item.url);const pathname = urlObject.pathname.split('/').pop();item.key = pathname.split('?')[0];let auth = md5(`uname=${item.uname}&passwd=${item.passwd}`).toUpperCase();auth = auth.length > 16 ? auth.substring(0, 16) : auth;if (item.auth)item.key += '?Auth=' + auth;})}, {deep: true});watch(() => state.srtPushConf, list => {list.forEach(item => {let url = `srt://${location.hostname}`if (item.port)url += `:${item.port}`url += `?mode=caller`if (item.latency)url += `&latency=${item.latency}`if (item.passphrase)url += `&passphrase=${item.passphrase}`if (item.streamid)url += `&streamid=${item.streamid}`item.url = url;})}, {deep: true});const handleNetConf = computed(() => {return defaultConf.filter((item, index) => {return item.type === 'net';})})const onDisplayHdmi = (chnId, chnEnable) => {if (chnEnable) {const mix = defaultConf.find(item => item.type === "mix");mix.output.src = chnId;updateDefaultConf("noTip").then(() => {alertMsg("<cn>输出至HDMI成功</cn><en>Display hdmi successfully!</en>", "success");})} elsealertMsg("<cn>通道未开启</cn><en>Display hdmi successfully!</en>", "error");}const handleVideoFileConf = computed(() => {if (defaultConf.length > 0) {return defaultConf.find(item => {if (item.type === "file") {if (item.file.length < 1)item.file.push("");return true;}})}return {};})const handleImageFileConf = computed(() => {if (defaultConf.length > 0) {return defaultConf.find(item => {if (item.type === "image") {if (item.image.length < 1)item.image.push({path: "",hold: "dur",duration: 15});return true;}})}return {};})const handleUsbVideoFile = computed(() => {if(isEmpty(usbFiles))return [];const media_ext = ['mp4', 'ts', 'flv', 'mp3', 'mkv'];return usbFiles.filter(item => {const ext = item.name.split('.').pop()?.toLowerCase();return media_ext.includes(ext);});})const handleUsbImageFile = computed(() => {if(isEmpty(usbFiles))return [];const media_ext = ['jpg', 'jpeg'];return usbFiles.filter(item => {const ext = item.name.split('.').pop()?.toLowerCase();return media_ext.includes(ext);});})const saveGlobalConfByLocal = () => {for (let i = 0; i < defaultConf.length; i++) {if (defaultConf[i].type !== "net")continue;delete state.globalConf.net.path;extend(defaultConf[i].net, deepCopy(state.globalConf.net));}saveDefaultConf();}const onMediaFileOption = (type, index) => {if (type !== "play") {defaultConf.forEach(item => {if ((item.type === "file" || item.type === "image") && item.type === state.tabType.value) {if (type === "up" && index !== 0)swap(item[item.type], index, index - 1);if (type === "down" && index < item.file.length - 1)swap(item[item.type], index, index + 1);if (type === "del")item[item.type].splice(index, 1);return true;}})} else {if(state.tabType.value === "file")rpc("enc.play", [index, 0]);if(state.tabType.value === "image")rpc("enc.imagePlay",[index])}}const onGetPlayList = () => {rpc("enc.getPlayList").then(list => {clearReactiveArray(state.playList);state.playList.push(...list);});}const onGetPlayPosition = () => {rpc("enc.getPlayPosition").then(obj => {clearReactiveObject(state.playPosition);Object.assign(state.playPosition, obj)});setTimeout(onGetPlayPosition, 1000);}const onGetImagePlayPosition = () => {rpc("enc.getImagePlayPosition").then(obj => {clearReactiveObject(state.imagePlayPosition);Object.assign(state.imagePlayPosition, obj)});setTimeout(onGetImagePlayPosition, 1000);}const onHandleFileDuration = fileName => {let duration = 0;if(!fileName)return duration;state.playList.forEach(item => {if (item.name === fileName) {duration = item.duration}})return duration;}const onHandleFilePosition = (fileName,type) => {let position = 0;if(!fileName)return position;if(type === "file") {if (Object.keys(state.playPosition).length > 0 && state.playPosition.file === fileName)position = state.playPosition.position;}if(type === "image") {if (Object.keys(state.imagePlayPosition).length > 0 && state.imagePlayPosition.file === fileName)position = state.imagePlayPosition.position * 1000;}return position;}const onAddMediaFile = () => {defaultConf.forEach(item => {if((item.type === "file" || item.type === "image") && item.type === state.tabType.value) {if(item.type === "image") {item.image.push({path: "",hold: "dur",duration: 15})} else {item.file.push("");}}})}const onTimelineSliderEnd = (val, index) => {const [hours, minutes, seconds] = val.split(':').map(Number);rpc("enc.play", [index, ((hours * 60 + minutes) * 60 + seconds) * 1000]);}const saveDefaultConf = () => {// const maxENC = hardwareConf.capability.encode.maxPixel;// let sum = 0;// for (let i = 0; i < defaultConf.length; i++) {// if (defaultConf[i].enable && defaultConf[i].encv !== undefined) {// if (defaultConf[i].encv.codec !== "close")// sum += defaultConf[i].encv.width * defaultConf[i].encv.height * defaultConf[i].encv.framerate;// if (defaultConf[i].enable2 && defaultConf[i].encv2.codec !== "close") {// sum += defaultConf[i].encv2.width * defaultConf[i].encv2.height * defaultConf[i].encv2.framerate;// }// }// }//// if (maxENC > 0 && sum > maxENC) {// confirm({// title: '<cn>警告</cn><en>Warning</en>',// content: '<cn>超出编码性能上限,请调整编码参数!</cn><en>The limit of encode performance is exceeded. Please adjust the encode parameters!</en>',// buttons: {// ok: {// text: "<cn>知道了</cn><en>I know</en>",// btnClass: 'btn-primary',// keys: ['enter'],// action: () => updateDefaultConf()// }// }// });// return;// }updateDefaultConf().then(onGetPlayList);}const updatePushUrl = (length = 4) => {const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ';let key = '';for (let i = 0; i < length; i++)key += characters.charAt(Math.floor(Math.random() * characters.length));key = key.toLowerCase();return "rtmp://" + window.location.hostname + "/live/" + key;}const onResetRXPushChnUrl = index => {state.rxPushConf[index].url = updatePushUrl();}const onCopyReceiveChnUrl = (index,type = '') => {const textarea = document.createElement("textarea");if(state.tabType.value === 'rtmp') {const urlObject = new URL(state.rxPushConf[index].url);if(type !== 'server') {textarea.value = urlObject.pathname.split('/').pop() + urlObject.search;if(state.rxPushConf[index].auth) {let auth = md5(`uname=${state.rxPushConf[index].uname}&passwd=${state.rxPushConf[index].passwd}`).toUpperCase();auth = auth.length > 16 ? auth.substring(0, 16) : auth;textarea.value = urlObject.pathname.split('/').pop() + urlObject.search + '?Auth=' + auth;}} else {textarea.value = `${urlObject.protocol}//${window.location.hostname}/live`;}}if(state.tabType.value === 'srt')textarea.value = state.srtPushConf[index].urldocument.body.appendChild(textarea);textarea.select();let success = document.execCommand("copy");document.body.removeChild(textarea);if (!success) {alertMsg('<cn>复制失败,请手动复制</cn><en>Copy failed, please copy manually</en>', 'error');return;}alertMsg('<cn>已复制</cn><en>Have copied</en>', 'success');}const onAddReceiveChn = () => {if(state.tabType.value === 'rtmp') {state.rxPushConf.push({"desc": "New Receiver","bind": -1,"auth": false,"uname": "admin","passwd": "admin","url": updatePushUrl()})}if(state.tabType.value === 'srt') {state.srtPushConf.push({"desc": "New Receiver","bind": -1,"port": "700"+(state.srtPushConf.length+1),"latency": "50","passphrase": "","streamid": "","url": "srt://127.0.0.1:700"+(state.srtPushConf.length+1)+"?mode=caller&latency=50"})}if(state.tabType.value === 'ndi') {state.ndiReciveConf.push({"desc": "New NDI Receiver","bind": -1,"url": ""})}}const onDelReceiveChn = index => {if(state.tabType.value === 'rtmp')state.rxPushConf.splice(index, 1);if(state.tabType.value === 'srt')state.srtPushConf.splice(index, 1);if(state.tabType.value === 'ndi')state.ndiReciveConf.splice(index, 1);}const saveReceiveChnConf = () => {let hadBind = false;if(state.tabType.value === 'rtmp') {state.rxPushConf.forEach(item => {if (item.bind !== "-1") {defaultConf.forEach(conf => {if (conf.id === item.bind && conf.type === 'net') {hadBind = true;conf.net.path = item.url.replace(/rtmp:\/\/[^\/]+\/(.*?)(\?.*)?$/, 'rtmp://127.0.0.1/1ドル');if (item.auth)conf.net.path += "?uname=" + item.uname + "&passwd=" + item.passwd;conf.enable = true;conf.net.decodeV = true;conf.net.decodeA = true;}})}})}if(state.tabType.value === 'srt') {state.srtPushConf.forEach(item => {if (item.bind !== "-1") {defaultConf.forEach(conf => {if (conf.id === item.bind && conf.type === 'net') {hadBind = true;//item.url = item.url.replace(location.hostname, '127.0.0.1');item.url = item.url.replace('mode=caller','mode=listener');conf.net.path = item.url;conf.enable = true;conf.net.decodeV = true;conf.net.decodeA = true;}})}})}if(state.tabType.value === 'ndi') {state.ndiReciveConf.forEach(item => {if (item.bind !== "-1") {defaultConf.forEach(conf => {if (conf.id === item.bind && conf.type === 'net') {hadBind = true;conf.net.path = "ndi://" + item.url;conf.enable = true;conf.net.decodeV = true;conf.net.decodeA = true;}})}})}Object.assign(receiverConf, {rtmp: state.rxPushConf,srt: state.srtPushConf,ndi: state.ndiReciveConf});updateReceiverConf().then(()=>{if (hadBind)updateDefaultConf('noTip');})}const refreshNdiSourceList = (tip, retryCount = 0) => {rpc("enc.getNDIList").then(data => {if (data.length === 0 && retryCount < 5) {setTimeout(() => refreshNdiSourceList(tip, retryCount + 1),500);} else {clearReactiveArray(state.ndiList);state.ndiList.push(...data);if (tip !== "noTip")alertMsg("<cn>刷新NDI源列表成功</cn><en>Refresh ndi source list successfully</en>");}});};const handleVdoConf = computed(()=>{if(defaultConf.length === 0) return [];const vodConf = [];defaultConf.forEach((item,index) => {if(item.type === 'net') {if (!item.hasOwnProperty("cap")) {item.cap = {contrast: 0,rotate: 0,crop: {B: 0,L: 0,R: 0,T: 0}}}vodConf.push(item);}});return vodConf;})onMounted(() => {onGetPlayList();onGetPlayPosition();onGetImagePlayPosition();refreshNdiSourceList("noTip");});return {...state, defaultConf, hardwareConf, handleVideoFileConf,handleImageFileConf, handleUsbVideoFile, handleUsbImageFile, onAddMediaFile, onMediaFileOption,formatTime, onTimelineSliderEnd, onHandleFileDuration, onHandleFilePosition, onDisplayHdmi, handleNetConf,saveGlobalConfByLocal, saveDefaultConf, onResetRXPushChnUrl, onCopyReceiveChnUrl,window,onAddReceiveChn, onDelReceiveChn,saveReceiveChnConf,refreshNdiSourceList,handleVdoConf}}});app.use(ignoreCustomElementPlugin);app.use(filterKeywordPlugin);app.mount('#app');</script></body></html>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。