mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-30 19:11:44 +08:00
refactor: 删除部分多余的代码和注释
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user