You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
378 lines
12 KiB
378 lines
12 KiB
<template> |
|
<el-dialog |
|
title="设置" |
|
:visible.sync="visible" |
|
width="600px" |
|
:close-on-click-modal="false" |
|
@close="handleClose" |
|
> |
|
<el-tabs v-model="activeTab" :tab-position="'left'" class="settings-tabs"> |
|
<!-- 视频设置 --> |
|
<el-tab-pane name="video"> |
|
<span slot="label"><i class="el-icon-video-camera"></i> 视频</span> |
|
<el-form :model="videoConfig" label-width="140px" class="settings-form"> |
|
<!-- 摄像头选择:使用外部传入的摄像头列表 --> |
|
<el-form-item label="摄像头/工作站"> |
|
<el-select |
|
v-model="videoConfig.camera" |
|
placeholder="请选择" |
|
style="width: 240px" |
|
@change="getCameraResolutions" |
|
> |
|
<el-option |
|
v-for="cam in effectiveCameraList" |
|
:key="cam.id" |
|
:label="cam.name" |
|
:value="cam.id" |
|
/> |
|
</el-select> |
|
</el-form-item> |
|
|
|
<el-form-item label="采集卡"> |
|
<el-select |
|
v-model="videoConfig.captureCard" |
|
placeholder="请选择" |
|
style="width: 240px" |
|
/> |
|
</el-form-item> |
|
<el-form-item label="网络调控策略"> |
|
<el-select |
|
v-model="videoConfig.networkStrategy" |
|
placeholder="请选择" |
|
style="width: 240px" |
|
> |
|
<el-option label="弱网下流畅度优先" value="smooth" /> |
|
<el-option label="清晰度优先" value="quality" /> |
|
<el-option label="平衡模式" value="balance" /> |
|
</el-select> |
|
</el-form-item> |
|
<!-- 摄像头分辨率 --> |
|
<el-form-item label="摄像头分辨率"> |
|
<el-select |
|
v-model="videoConfig.cameraResolution" |
|
placeholder="请选择" |
|
style="width: 240px" |
|
> |
|
<el-option |
|
v-for="res in supportedResolutions" |
|
:key="res.label" |
|
:label="res.label" |
|
:value="res.label" |
|
/> |
|
</el-select> |
|
</el-form-item> |
|
<el-form-item label="采集卡分辨率"> |
|
<el-select |
|
v-model="videoConfig.captureResolution" |
|
placeholder="请选择" |
|
style="width: 240px" |
|
/> |
|
</el-form-item> |
|
|
|
<el-form-item label="采集卡帧率"> |
|
<el-select |
|
v-model="videoConfig.captureFps" |
|
placeholder="请选择" |
|
style="width: 240px" |
|
/> |
|
</el-form-item> |
|
|
|
<el-form-item label="采集卡码率(2-8M)"> |
|
<el-input |
|
v-model="videoConfig.captureBitrate" |
|
style="width: 240px" |
|
/> |
|
<i class="el-icon-edit edit-icon" style="margin-left: 10px"></i> |
|
</el-form-item> |
|
<el-form-item label="视频镜像"> |
|
<el-radio-group v-model="videoConfig.mirror"> |
|
<el-radio :label="true">是</el-radio> |
|
<el-radio :label="false">否</el-radio> |
|
</el-radio-group> |
|
</el-form-item> |
|
</el-form> |
|
</el-tab-pane> |
|
|
|
<!-- 音频设置 --> |
|
<el-tab-pane name="audio"> |
|
<span slot="label"><i class="el-icon-microphone"></i> 音频</span> |
|
<el-form :model="audioConfig" label-width="140px" class="settings-form"> |
|
<el-form-item label="麦克风"> |
|
<el-select |
|
v-model="audioConfig.mic" |
|
placeholder="请选择麦克风" |
|
style="width: 240px" |
|
@change="startMicTest" |
|
> |
|
<el-option |
|
v-for="mic in effectiveMicList" |
|
:key="mic.id" |
|
:label="mic.name" |
|
:value="mic.id" |
|
/> |
|
</el-select> |
|
</el-form-item> |
|
<!-- 麦克风音量实时测试条 --> |
|
<el-form-item label="麦克风测试"> |
|
<el-slider |
|
v-model="micTestPercent" |
|
class="progress-slider" |
|
disabled |
|
/> |
|
</el-form-item> |
|
<!-- 扬声器选择 --> |
|
<el-form-item label="扬声器"> |
|
<el-select |
|
v-model="audioConfig.speaker" |
|
placeholder="请选择扬声器" |
|
style="width: 240px" |
|
@change="playSpeakerTest" |
|
> |
|
<el-option |
|
v-for="spk in effectiveSpeakerList" |
|
:key="spk.id" |
|
:label="spk.name" |
|
:value="spk.id" |
|
/> |
|
</el-select> |
|
</el-form-item> |
|
<!-- 扬声器音量实时测试条 --> |
|
<el-form-item label="扬声器测试"> |
|
<el-slider |
|
v-model="speakerTestPercent" |
|
class="progress-slider" |
|
disabled |
|
/> |
|
</el-form-item> |
|
</el-form> |
|
</el-tab-pane> |
|
</el-tabs> |
|
</el-dialog> |
|
</template> |
|
|
|
<script> |
|
export default { |
|
name: "SettingsDialog", |
|
props: { |
|
visible: { |
|
type: Boolean, |
|
default: false, |
|
}, |
|
videoConfig: { |
|
type: Object, |
|
required: true, |
|
}, |
|
audioConfig: { |
|
type: Object, |
|
required: true, |
|
}, |
|
cameraList: { |
|
type: Array, |
|
default: () => [], |
|
}, |
|
micList: { |
|
type: Array, |
|
default: () => [], |
|
}, |
|
speakerList: { |
|
type: Array, |
|
default: () => [], |
|
}, |
|
}, |
|
data() { |
|
return { |
|
activeTab: "video", |
|
micTestPercent: 0, |
|
speakerTestPercent: 0, |
|
videoDevices: [], |
|
audioInputDevices: [], |
|
audioOutputDevices: [], |
|
supportedResolutions: [], |
|
micAudioContext: null, |
|
micAnalyser: null, |
|
micAnimation: null, |
|
speakerAudio: null, |
|
}; |
|
}, |
|
computed: { |
|
effectiveCameraList() { |
|
if (this.cameraList && this.cameraList.length) { |
|
return this.cameraList; |
|
} |
|
return this.videoDevices.map(d => ({ id: d.deviceId, name: d.label || '未知摄像头' })); |
|
}, |
|
effectiveMicList() { |
|
if (this.micList && this.micList.length) { |
|
return this.micList; |
|
} |
|
return this.audioInputDevices.map(d => ({ id: d.deviceId, name: d.label || '未知麦克风' })); |
|
}, |
|
effectiveSpeakerList() { |
|
if (this.speakerList && this.speakerList.length) { |
|
return this.speakerList; |
|
} |
|
return this.audioOutputDevices.map(d => ({ id: d.deviceId, name: d.label || '未知扬声器' })); |
|
}, |
|
}, |
|
watch: { |
|
visible(val) { |
|
if (val) { |
|
this.getDevices(); |
|
this.micTestPercent = 10; |
|
this.speakerTestPercent = 10; |
|
} else { |
|
this.stopAllTest(); |
|
} |
|
}, |
|
}, |
|
methods: { |
|
async getDevices() { |
|
if (this.cameraList && this.cameraList.length) { |
|
this.getCameraResolutions(); |
|
return; |
|
} |
|
try { |
|
await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); |
|
const devices = await navigator.mediaDevices.enumerateDevices(); |
|
this.videoDevices = devices.filter((d) => d.kind === "videoinput"); |
|
this.audioInputDevices = devices.filter((d) => d.kind === "audioinput"); |
|
this.audioOutputDevices = devices.filter((d) => d.kind === "audiooutput"); |
|
|
|
if (this.videoDevices.length && !this.videoConfig.camera) { |
|
this.videoConfig.camera = this.videoDevices[0].deviceId; |
|
this.getCameraResolutions(); |
|
} |
|
if (this.audioInputDevices.length && !this.audioConfig.mic) { |
|
this.audioConfig.mic = this.audioInputDevices[0].deviceId; |
|
this.startMicTest(); |
|
} |
|
if (this.audioOutputDevices.length && !this.audioConfig.speaker) { |
|
this.audioConfig.speaker = this.audioOutputDevices[0].deviceId; |
|
} |
|
} catch (err) { |
|
console.error("获取设备失败", err); |
|
} |
|
}, |
|
async getCameraResolutions() { |
|
if (!this.videoConfig.camera) return; |
|
try { |
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
video: { deviceId: this.videoConfig.camera }, |
|
}); |
|
const track = stream.getVideoTracks()[0]; |
|
const caps = track.getCapabilities ? track.getCapabilities() : { width: { max: 1920 } }; |
|
const list = [ |
|
{ w: 3840, h: 2160, label: "4K" }, |
|
{ w: 1920, h: 1080, label: "1080P" }, |
|
{ w: 1280, h: 720, label: "720P" }, |
|
{ w: 640, h: 480, label: "480P" }, |
|
].filter((item) => (caps.width && caps.width.max >= item.w) || true); |
|
this.supportedResolutions = list; |
|
if (list.length && !this.videoConfig.cameraResolution) { |
|
this.videoConfig.cameraResolution = list[0].label; |
|
} |
|
stream.getTracks().forEach((t) => t.stop()); |
|
} catch (e) { |
|
this.supportedResolutions = [ |
|
{ w: 1920, h: 1080, label: "1080P" }, |
|
{ w: 1280, h: 720, label: "720P" }, |
|
{ w: 640, h: 480, label: "480P" }, |
|
]; |
|
} |
|
}, |
|
async startMicTest() { |
|
this.stopMicTest(); |
|
try { |
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
audio: { deviceId: this.audioConfig.mic }, |
|
}); |
|
|
|
this.micAudioContext = new AudioContext(); |
|
this.micAnalyser = this.micAudioContext.createAnalyser(); |
|
this.micAnalyser.fftSize = 256; |
|
const source = this.micAudioContext.createMediaStreamSource(stream); |
|
source.connect(this.micAnalyser); |
|
|
|
const dataArray = new Uint8Array(this.micAnalyser.frequencyBinCount); |
|
const update = () => { |
|
if (!this.micAnalyser) return; |
|
this.micAnalyser.getByteFrequencyData(dataArray); |
|
const sum = dataArray.reduce((a, b) => a + b, 0); |
|
const vol = Math.min(100, sum / 50); |
|
this.micTestPercent = vol; |
|
this.micAnimation = requestAnimationFrame(update); |
|
}; |
|
update(); |
|
} catch (err) { |
|
console.log("麦克风测试失败"); |
|
} |
|
}, |
|
stopMicTest() { |
|
if (this.micAnimation) { |
|
cancelAnimationFrame(this.micAnimation); |
|
this.micAnimation = null; |
|
} |
|
if (this.micAudioContext) { |
|
this.micAudioContext.close(); |
|
this.micAudioContext = null; |
|
this.micAnalyser = null; |
|
} |
|
this.micTestPercent = 10; |
|
}, |
|
async playSpeakerTest() { |
|
try { |
|
if (this.speakerAudio) { |
|
this.speakerAudio.pause(); |
|
this.speakerAudio = null; |
|
} |
|
this.speakerTestPercent = 10; |
|
|
|
const audio = new Audio( |
|
"data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJmWl5hQVjMpOkhUXMDc1d3d1dPQ1NDR0dLR0dDQ0M/Pz8vLy8rKysnJyeHh4d3d3dHR0dDQ0M/Pz8vLy8rKysnJyeHh4d3d3dHR0dHR0dHR0dHR0dHR0dHQ=" |
|
); |
|
audio.muted = false; |
|
|
|
if (this.audioConfig.speaker && audio.setSinkId) { |
|
await audio.setSinkId(this.audioConfig.speaker); |
|
} |
|
|
|
audio.onplaying = () => { |
|
this.speakerTestPercent = 80; |
|
setTimeout(() => (this.speakerTestPercent = 40), 300); |
|
setTimeout(() => (this.speakerTestPercent = 70), 600); |
|
setTimeout(() => (this.speakerTestPercent = 30), 900); |
|
setTimeout(() => (this.speakerTestPercent = 10), 1200); |
|
}; |
|
audio.play(); |
|
this.speakerAudio = audio; |
|
} catch (e) { |
|
console.log("扬声器测试失败"); |
|
} |
|
}, |
|
stopAllTest() { |
|
this.stopMicTest(); |
|
if (this.speakerAudio) { |
|
this.speakerAudio.pause(); |
|
this.speakerAudio = null; |
|
} |
|
this.speakerTestPercent = 10; |
|
}, |
|
handleClose() { |
|
this.$emit("update:visible", false); |
|
}, |
|
}, |
|
}; |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.settings-form { |
|
padding: 10px; |
|
} |
|
.edit-icon { |
|
margin-left: 5px; |
|
cursor: pointer; |
|
} |
|
.progress-slider { |
|
width: 240px; |
|
} |
|
</style> |