refactor: 删除部分多余的代码和注释

This commit is contained in:
mofeng-git
2026-05-01 17:31:04 +08:00
parent 74035f8e12
commit d8e7de74a6
165 changed files with 2960 additions and 9917 deletions

View File

@@ -1,20 +1,14 @@
// Audio player composable - handles WebSocket connection, Opus decoding, and Web Audio API playback
import { ref, watch } from 'vue'
import { OpusDecoder } from 'opus-decoder'
import { buildWsUrl } from '@/types/websocket'
// Binary protocol header format (15 bytes)
// [type:1][timestamp:4][duration:2][sequence:4][length:4][data:...]
export function useAudioPlayer() {
// State
const connected = ref(false)
const playing = ref(false)
const volume = ref(0) // Default to 0, user must adjust to enable audio (browser autoplay policy)
const error = ref<string | null>(null)
// Internal variables
let ws: WebSocket | null = null
let audioContext: AudioContext | null = null
let gainNode: GainNode | null = null
@@ -23,7 +17,6 @@ export function useAudioPlayer() {
let nextPlayTime = 0
let isConnecting = false // Prevent concurrent connection attempts
// Initialize decoder
async function initDecoder() {
const opusDecoder = new OpusDecoder({
channels: 2,
@@ -33,7 +26,6 @@ export function useAudioPlayer() {
decoder = opusDecoder
}
// Initialize audio context
function initAudioContext() {
audioContext = new AudioContext({ sampleRate: 48000 })
gainNode = audioContext.createGain()
@@ -41,14 +33,12 @@ export function useAudioPlayer() {
updateVolume()
}
// Connect to WebSocket
async function connect() {
// Prevent concurrent connection attempts (critical fix for multiple WS connections)
if (isConnecting) {
return
}
// Check if already connected
if (ws) {
if (ws.readyState === WebSocket.OPEN) {
return
@@ -64,7 +54,6 @@ export function useAudioPlayer() {
isConnecting = true
try {
// Initialize
if (!decoder) await initDecoder()
if (!audioContext) initAudioContext()
@@ -108,7 +97,6 @@ export function useAudioPlayer() {
}
}
// Disconnect
function disconnect() {
if (ws) {
ws.close()
@@ -140,14 +128,12 @@ export function useAudioPlayer() {
const samplesPerChannel = decoded.samplesDecoded
const channels = decoded.channelData.length
// Create audio buffer
const audioBuffer = audioContext.createBuffer(
channels,
samplesPerChannel,
48000
)
// Fill channel data
for (let ch = 0; ch < channels; ch++) {
const channelData = audioBuffer.getChannelData(ch)
const sourceData = decoded.channelData[ch]
@@ -156,7 +142,6 @@ export function useAudioPlayer() {
}
}
// Schedule playback
const source = audioContext.createBufferSource()
source.buffer = audioBuffer
source.connect(gainNode)
@@ -164,12 +149,10 @@ export function useAudioPlayer() {
const now = audioContext.currentTime
const scheduledAhead = nextPlayTime - now
// Reset if too far behind (audio was paused/lagged)
if (nextPlayTime < now) {
nextPlayTime = now + 0.02 // Start 20ms ahead
}
// Reset if buffer too large (> 500ms ahead)
if (scheduledAhead > 0.5) {
nextPlayTime = now + 0.05 // Keep 50ms buffer
}
@@ -177,33 +160,27 @@ export function useAudioPlayer() {
source.start(nextPlayTime)
nextPlayTime += audioBuffer.duration
} catch {
// Ignore decode errors
}
}
// Update volume
function updateVolume() {
if (gainNode) {
gainNode.gain.value = volume.value
}
}
// Set volume
function setVolume(v: number) {
volume.value = Math.max(0, Math.min(1, v))
updateVolume()
}
// Watch volume changes
watch(volume, updateVolume)
return {
// State
connected,
playing,
volume,
error,
// Methods
connect,
disconnect,
setVolume,

View File

@@ -1,5 +1,3 @@
// Config popover composable - shared logic for config popover components
// Provides common state management and lifecycle hooks
import { ref, watch, type Ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -17,11 +15,9 @@ export interface UseConfigPopoverOptions {
export function useConfigPopover(options: UseConfigPopoverOptions) {
const { t } = useI18n()
// Common state
const applying = ref(false)
const loadingDevices = ref(false)
// Watch open state to initialize
watch(options.open, async (isOpen) => {
if (isOpen) {
options.initializeFromCurrent?.()
@@ -36,7 +32,6 @@ export function useConfigPopover(options: UseConfigPopoverOptions) {
}
})
// Apply config wrapper with loading state and toast
async function applyConfig(applyFn: () => Promise<void>) {
applying.value = true
try {
@@ -44,7 +39,6 @@ export function useConfigPopover(options: UseConfigPopoverOptions) {
toast.success(t('config.applied'))
} catch (e) {
console.info('[ConfigPopover] Apply failed:', e)
// Error toast is usually shown by API layer
} finally {
applying.value = false
}
@@ -62,11 +56,9 @@ export function useConfigPopover(options: UseConfigPopoverOptions) {
}
return {
// State
applying,
loadingDevices,
// Methods
applyConfig,
refreshDevices,
}

View File

@@ -1,6 +1,3 @@
// Console WebSocket events composable - handles all WebSocket event subscriptions
// Extracted from ConsoleView.vue for better separation of concerns
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { useSystemStore } from '@/stores/system'
@@ -33,7 +30,7 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
const systemStore = useSystemStore()
const { on, off, connect } = useWebSocket()
const noop = () => {}
// Stream device monitoring handlers
function handleStreamDeviceLost(data: { device: string; reason: string }) {
if (systemStore.stream) {
systemStore.stream.online = false
@@ -66,20 +63,11 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
handlers.onStreamRecovered?.(_data)
}
function handleStreamStateChanged(data: { state: string }) {
if (data.state === 'error') {
// Handled by video stream composable
}
}
function handleStreamStateChangedForward(data: { state: string; device?: string | null }) {
handleStreamStateChanged(data)
handlers.onStreamStateChanged?.(data)
}
// Subscribe to all events
function subscribe() {
// Stream events
on('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
on('stream.config_applied', handlers.onStreamConfigApplied ?? noop)
on('stream.stats_update', handlers.onStreamStatsUpdate ?? noop)
@@ -92,14 +80,11 @@ export function useConsoleEvents(handlers: ConsoleEventHandlers) {
on('stream.reconnecting', handleStreamReconnecting)
on('stream.recovered', handleStreamRecovered)
// System events
on('system.device_info', handlers.onDeviceInfo ?? noop)
// Connect WebSocket
connect()
}
// Unsubscribe from all events
function unsubscribe() {
off('stream.config_changing', handlers.onStreamConfigChanging ?? noop)
off('stream.config_applied', handlers.onStreamConfigApplied ?? noop)

View File

@@ -1,6 +1,3 @@
// WebSocket HID channel for low-latency keyboard/mouse input (binary protocol)
// Uses the same binary format as WebRTC DataChannel for consistency
import { ref, onUnmounted } from 'vue'
import {
type HidKeyboardEvent,
@@ -23,26 +20,22 @@ const reconnectAttempts = ref(0)
const networkError = ref(false)
const networkErrorMessage = ref<string | null>(null)
let reconnectTimeout: number | null = null
const hidUnavailable = ref(false) // Track if HID is unavailable to prevent unnecessary reconnects
const hidUnavailable = ref(false)
// Connection promise to avoid race conditions
let connectionPromise: Promise<boolean> | null = null
let connectionResolved = false
function connect(): Promise<boolean> {
// If already connected, return immediately
if (wsInstance && wsInstance.readyState === WebSocket.OPEN && connectionResolved) {
return Promise.resolve(true)
}
// If connection is in progress, return the existing promise
if (connectionPromise && !connectionResolved) {
return connectionPromise
}
connectionResolved = false
connectionPromise = new Promise((resolve) => {
// Reset network error flag when attempting new connection
networkError.value = false
networkErrorMessage.value = null
hidUnavailable.value = false
@@ -60,7 +53,6 @@ function connect(): Promise<boolean> {
}
wsInstance.onmessage = (e) => {
// Handle binary response
if (e.data instanceof ArrayBuffer) {
const view = new DataView(e.data)
if (view.byteLength >= 1) {
@@ -126,14 +118,12 @@ function disconnect() {
}
if (wsInstance) {
// Close the websocket
wsInstance.close()
wsInstance = null
connected.value = false
networkError.value = false
}
// Reset connection state
connectionPromise = null
connectionResolved = false
}
@@ -154,7 +144,6 @@ function sendKeyboard(event: HidKeyboardEvent): Promise<void> {
})
}
// Internal function to actually send mouse event
function _sendMouseInternal(event: HidMouseEvent): Promise<void> {
return new Promise((resolve, reject) => {
if (!wsInstance || wsInstance.readyState !== WebSocket.OPEN) {
@@ -175,7 +164,6 @@ function sendMouse(event: HidMouseEvent): Promise<void> {
return _sendMouseInternal(event)
}
// Send consumer control event (multimedia keys)
function sendConsumer(event: HidConsumerEvent): Promise<void> {
return new Promise((resolve, reject) => {
if (!wsInstance || wsInstance.readyState !== WebSocket.OPEN) {
@@ -194,8 +182,6 @@ function sendConsumer(event: HidConsumerEvent): Promise<void> {
export function useHidWebSocket() {
onUnmounted(() => {
// Don't disconnect on component unmount - WebSocket is shared
// Only disconnect when explicitly called or page unloads
})
return {
@@ -212,7 +198,6 @@ export function useHidWebSocket() {
}
}
// Global lifecycle - disconnect when page unloads
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
disconnect()

View File

@@ -1,4 +1,3 @@
// Unified Audio Manager
// Manages audio playback across different video modes (MJPEG/WebSocket and H264/WebRTC)
// Provides a single interface for volume control and audio source switching
@@ -17,7 +16,6 @@ export interface UnifiedAudioState {
}
export function useUnifiedAudio() {
// === State ===
const audioMode = ref<AudioMode>('ws')
const volume = ref(0) // 0-1, default muted (browser autoplay policy)
const muted = ref(false)
@@ -25,11 +23,9 @@ export function useUnifiedAudio() {
const playing = ref(false)
const error = ref<string | null>(null)
// === Internal References ===
const wsPlayer = getAudioPlayer()
let webrtcVideoElement: HTMLVideoElement | null = null
// === Methods ===
/**
* Set the WebRTC video element reference
@@ -39,9 +35,7 @@ export function useUnifiedAudio() {
// Only update if element is provided (don't clear on null to preserve reference)
if (el) {
webrtcVideoElement = el
// Sync current volume to video element
el.volume = volume.value
// Mute if volume is 0 or explicitly muted
const shouldMute = muted.value || volume.value === 0
el.muted = shouldMute
}
@@ -57,7 +51,6 @@ export function useUnifiedAudio() {
const wasConnected = connected.value
const wasPlaying = playing.value
// Disconnect old mode
if (audioMode.value === 'ws') {
wsPlayer.disconnect()
}
@@ -65,12 +58,10 @@ export function useUnifiedAudio() {
audioMode.value = mode
// If was connected/playing and volume > 0, auto-connect new mode
if ((wasConnected || wasPlaying) && volume.value > 0) {
await connect()
}
// Update connection state
updateConnectionState()
}
@@ -82,7 +73,6 @@ export function useUnifiedAudio() {
const newVolume = Math.max(0, Math.min(1, v))
volume.value = newVolume
// Sync to WS player
wsPlayer.setVolume(newVolume)
// Sync to WebRTC video element
@@ -99,7 +89,6 @@ export function useUnifiedAudio() {
function setMuted(m: boolean) {
muted.value = m
// WS player: control via volume (no separate mute)
if (audioMode.value === 'ws') {
wsPlayer.setVolume(m ? 0 : volume.value)
}
@@ -133,7 +122,6 @@ export function useUnifiedAudio() {
}
} else {
// WebRTC audio is automatically connected via video track
// Just ensure video element is not muted (if volume > 0)
if (webrtcVideoElement) {
webrtcVideoElement.muted = muted.value || volume.value === 0
connected.value = true
@@ -173,7 +161,6 @@ export function useUnifiedAudio() {
}
}
// Watch WS player state changes
watch(() => wsPlayer.connected.value, (newConnected) => {
if (audioMode.value === 'ws') {
connected.value = newConnected
@@ -193,7 +180,6 @@ export function useUnifiedAudio() {
})
return {
// State
audioMode,
volume,
muted,
@@ -201,7 +187,6 @@ export function useUnifiedAudio() {
playing,
error,
// Methods
setWebRTCElement,
switchMode,
setVolume,

View File

@@ -85,7 +85,6 @@ async function fetchIceServers(): Promise<RTCIceServer[]> {
}
// Fallback: for local connections, use no ICE servers (host candidates only)
// For remote connections, use Google STUN as fallback
const isLocalConnection = typeof window !== 'undefined' &&
(window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
@@ -105,7 +104,6 @@ async function fetchIceServers(): Promise<RTCIceServer[]> {
]
}
// Shared instance state
let peerConnection: RTCPeerConnection | null = null
let dataChannel: RTCDataChannel | null = null
let sessionId: string | null = null
@@ -146,7 +144,6 @@ const error = ref<string | null>(null)
const dataChannelReady = ref(false)
const connectStage = ref<WebRTCConnectStage>('idle')
// Create RTCPeerConnection with configuration
function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {
const config: RTCConfiguration = {
iceServers,
@@ -155,7 +152,6 @@ function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {
const pc = new RTCPeerConnection(config)
// Handle connection state changes
pc.onconnectionstatechange = () => {
switch (pc.connectionState) {
case 'connecting':
@@ -211,7 +207,6 @@ function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {
}
}
// Handle incoming tracks
pc.ontrack = (event) => {
const track = event.track
@@ -222,7 +217,6 @@ function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {
}
}
// Handle data channel from server
pc.ondatachannel = (event) => {
setupDataChannel(event.channel)
}
@@ -230,7 +224,6 @@ function createPeerConnection(iceServers: RTCIceServer[]): RTCPeerConnection {
return pc
}
// Setup data channel event handlers
function setupDataChannel(channel: RTCDataChannel) {
dataChannel = channel
@@ -243,15 +236,12 @@ function setupDataChannel(channel: RTCDataChannel) {
}
channel.onerror = () => {
// Data channel errors handled silently
}
channel.onmessage = () => {
// Handle incoming messages from server (e.g., LED status)
}
}
// Create data channel for HID events
function createDataChannel(pc: RTCPeerConnection): RTCDataChannel {
const channel = pc.createDataChannel('hid', {
ordered: true,
@@ -307,7 +297,6 @@ async function handleRemoteIceComplete(data: WebRTCIceCompleteEvent) {
try {
await peerConnection.addIceCandidate(null)
} catch {
// End-of-candidates failures are non-fatal
}
}
@@ -348,7 +337,6 @@ async function flushPendingRemoteIce() {
try {
await peerConnection.addIceCandidate(null)
} catch {
// Ignore end-of-candidates errors
}
}
}
@@ -363,14 +351,12 @@ function startStatsCollection() {
try {
const report = await peerConnection.getStats()
// Collect candidate info
const candidates: Record<string, { type: IceCandidateType; protocol: string }> = {}
let selectedPairLocalId = ''
let selectedPairRemoteId = ''
let foundActivePair = false
report.forEach((stat) => {
// Collect all candidates
if (stat.type === 'local-candidate' || stat.type === 'remote-candidate') {
candidates[stat.id] = {
type: (stat.candidateType as IceCandidateType) || 'unknown',
@@ -378,10 +364,7 @@ function startStatsCollection() {
}
}
// Find the active candidate pair
// Priority: nominated > succeeded (for Chrome/Firefox compatibility)
if (stat.type === 'candidate-pair') {
// Check if this is the nominated/selected pair
const isActive = stat.nominated === true ||
(stat.state === 'succeeded' && stat.selected === true) ||
(stat.state === 'in-progress' && !foundActivePair)
@@ -425,8 +408,6 @@ function startStatsCollection() {
stats.value.remoteCandidateType = remoteCandidate.type
}
// Check if using TURN relay
// TURN relay is when either local or remote candidate is 'relay' type
stats.value.isRelay = stats.value.localCandidateType === 'relay' || stats.value.remoteCandidateType === 'relay'
} catch {
// Stats collection errors are non-fatal
@@ -473,7 +454,6 @@ async function connect(): Promise<boolean> {
connectInFlight = (async () => {
registerWebSocketHandlers()
// Prevent concurrent connection attempts
if (isConnecting) {
return state.value === 'connected'
}
@@ -484,7 +464,6 @@ async function connect(): Promise<boolean> {
isConnecting = true
// Clean up any existing connection first
if (peerConnection || sessionId) {
await disconnect()
}
@@ -505,20 +484,16 @@ async function connect(): Promise<boolean> {
peerConnection = createPeerConnection(iceServers)
connectStage.value = 'creating_data_channel'
// Create data channel before offer (for HID)
createDataChannel(peerConnection)
// Add transceiver for receiving video
peerConnection.addTransceiver('video', { direction: 'recvonly' })
peerConnection.addTransceiver('audio', { direction: 'recvonly' })
connectStage.value = 'creating_offer'
// Create offer
const offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer)
connectStage.value = 'waiting_server_answer'
// Send offer to server and get answer
// Do not pass client_id here: each connect creates a fresh session.
const response = await webrtcApi.offer(offer.sdp!)
sessionId = response.session_id
@@ -526,7 +501,6 @@ async function connect(): Promise<boolean> {
// Send any ICE candidates that were queued while waiting for sessionId
await flushPendingIceCandidates()
// Set remote description (answer)
const answer: RTCSessionDescriptionInit = {
type: 'answer',
sdp: response.sdp,
@@ -611,7 +585,6 @@ async function disconnect() {
try {
await webrtcApi.close(oldSessionId)
} catch {
// Ignore close errors
}
}
@@ -671,13 +644,11 @@ function sendMouse(event: HidMouseEvent): boolean {
}
}
// Get MediaStream for video element (cached to avoid recreating)
function getMediaStream(): MediaStream | null {
if (!videoTrack.value && !audioTrack.value) {
return null
}
// Reuse cached stream if tracks match
if (cachedMediaStream) {
const currentVideoTracks = cachedMediaStream.getVideoTracks()
const currentAudioTracks = cachedMediaStream.getAudioTracks()
@@ -693,19 +664,15 @@ function getMediaStream(): MediaStream | null {
return cachedMediaStream
}
// Tracks changed, update the cached stream
// Remove old tracks
currentVideoTracks.forEach(t => cachedMediaStream!.removeTrack(t))
currentAudioTracks.forEach(t => cachedMediaStream!.removeTrack(t))
// Add new tracks
if (videoTrack.value) cachedMediaStream.addTrack(videoTrack.value)
if (audioTrack.value) cachedMediaStream.addTrack(audioTrack.value)
return cachedMediaStream
}
// Create new cached stream
cachedMediaStream = new MediaStream()
if (videoTrack.value) {
cachedMediaStream.addTrack(videoTrack.value)
@@ -716,15 +683,11 @@ function getMediaStream(): MediaStream | null {
return cachedMediaStream
}
// Composable export
export function useWebRTC() {
onUnmounted(() => {
// Don't disconnect on unmount - keep connection alive
// Only disconnect when explicitly called
})
return {
// State
state: state as Ref<WebRTCState>,
videoTrack,
audioTrack,
@@ -734,14 +697,12 @@ export function useWebRTC() {
connectStage,
sessionId: computed(() => sessionId),
// Methods
connect,
disconnect,
sendKeyboard,
sendMouse,
getMediaStream,
// Computed
isConnected: computed(() => state.value === 'connected'),
isConnecting: computed(() => state.value === 'connecting'),
hasVideo: computed(() => videoTrack.value !== null),
@@ -749,7 +710,6 @@ export function useWebRTC() {
}
}
// Cleanup on page unload
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
disconnect()

View File

@@ -1,8 +1,3 @@
// WebSocket composable for real-time event streaming
//
// Usage:
// const { connected, on, off } = useWebSocket()
// on('stream.state_changed', (data) => { ... })
import { ref } from 'vue'
import { buildWsUrl, WS_RECONNECT_DELAY } from '@/types/websocket'
@@ -151,7 +146,6 @@ function handleEvent(payload: WsEvent) {
}
})
}
// Silently ignore events without handlers
}
export function useWebSocket() {
@@ -170,7 +164,6 @@ export function useWebSocket() {
}
}
// Global lifecycle - disconnect when page unloads
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
disconnect()