初步可用
This commit is contained in:
273
app/composables/useTauri.ts
Normal file
273
app/composables/useTauri.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
|
||||
// 类型定义,对应 Rust 端的结构
|
||||
export interface ConnectionConfig {
|
||||
host: string
|
||||
port: number
|
||||
timeout: number
|
||||
autoReconnect: boolean
|
||||
reconnectInterval: number
|
||||
}
|
||||
|
||||
export interface VideoInfo {
|
||||
path: string
|
||||
title: string
|
||||
duration?: number
|
||||
size?: number
|
||||
format?: string
|
||||
}
|
||||
|
||||
export interface PlayerState {
|
||||
connectionStatus: 'connected' | 'connecting' | 'disconnected' | { error: string }
|
||||
playbackStatus: 'playing' | 'paused' | 'stopped' | 'loading'
|
||||
currentVideo?: VideoInfo
|
||||
position: number
|
||||
duration: number
|
||||
volume: number
|
||||
isLooping: boolean
|
||||
isFullscreen: boolean
|
||||
playlist: VideoInfo[]
|
||||
currentPlaylistIndex?: number
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type PlaybackCommand =
|
||||
| 'play'
|
||||
| 'pause'
|
||||
| 'stop'
|
||||
| { seek: { position: number } }
|
||||
| { setVolume: { volume: number } }
|
||||
| { setLoop: { enabled: boolean } }
|
||||
| 'toggleFullscreen'
|
||||
| { loadVideo: { path: string } }
|
||||
| { setPlaylist: { videos: string[] } }
|
||||
| { playFromPlaylist: { index: number } }
|
||||
|
||||
export interface AppSettings {
|
||||
connection: ConnectionConfig
|
||||
defaultVolume: number
|
||||
defaultLoop: boolean
|
||||
autoFullscreen: boolean
|
||||
playbackEndBehavior: 'stop' | 'next' | 'repeat'
|
||||
theme: string
|
||||
language: string
|
||||
showNotifications: boolean
|
||||
debugMode: boolean
|
||||
cacheSize: number
|
||||
proxy?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Tauri API composable
|
||||
* 提供与 Tauri 后端通信的方法
|
||||
*/
|
||||
export function useTauri() {
|
||||
const toast = useToast()
|
||||
|
||||
/**
|
||||
* 安全地调用 Tauri API
|
||||
*/
|
||||
async function safeInvoke<T>(command: string, args?: any): Promise<T | null> {
|
||||
try {
|
||||
const result = await invoke<T>(command, args)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`Tauri invoke error (${command}):`, error)
|
||||
toast.add({
|
||||
title: '操作失败',
|
||||
description: `调用 ${command} 失败: ${error}`,
|
||||
color: 'error'
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到视频播放器
|
||||
*/
|
||||
async function connectToPlayer(config: ConnectionConfig): Promise<PlayerState | null> {
|
||||
const response = await safeInvoke<ApiResponse<PlayerState>>('connect_to_player', { config })
|
||||
if (response?.success && response.data) {
|
||||
toast.add({
|
||||
title: '连接成功',
|
||||
description: response.message || '已成功连接到视频播放器',
|
||||
color: 'success'
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
const host = config?.host || '127.0.0.1'
|
||||
const port = config?.port || 6666
|
||||
const reason = response?.error || '无法连接到视频播放器,请检查地址与端口是否正确,以及播放器是否已启动'
|
||||
toast.add({
|
||||
title: '连接失败',
|
||||
description: `${reason}(目标:${host}:${port})`,
|
||||
color: 'error'
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开与视频播放器的连接
|
||||
*/
|
||||
async function disconnectFromPlayer(): Promise<boolean> {
|
||||
const response = await safeInvoke<ApiResponse<void>>('disconnect_from_player')
|
||||
if (response?.success) {
|
||||
toast.add({
|
||||
title: '已断开连接',
|
||||
description: response.message || '已断开与视频播放器的连接',
|
||||
color: 'warning'
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
async function getConnectionStatus(): Promise<PlayerState | null> {
|
||||
const response = await safeInvoke<ApiResponse<PlayerState>>('get_connection_status')
|
||||
return response?.success && response.data ? response.data : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送播放控制命令
|
||||
*/
|
||||
async function sendPlaybackCommand(command: PlaybackCommand): Promise<boolean> {
|
||||
const response = await safeInvoke<ApiResponse<void>>('send_playback_command', { command })
|
||||
if (response?.success) {
|
||||
// 不显示成功消息,避免过多通知
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新连接配置
|
||||
*/
|
||||
async function updateConnectionConfig(config: ConnectionConfig): Promise<boolean> {
|
||||
const response = await safeInvoke<ApiResponse<void>>('update_connection_config', { config })
|
||||
if (response?.success) {
|
||||
toast.add({
|
||||
title: '配置更新成功',
|
||||
description: response.message || '连接配置已更新',
|
||||
color: 'success'
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用设置
|
||||
*/
|
||||
async function getSettings(): Promise<AppSettings | null> {
|
||||
const response = await safeInvoke<ApiResponse<AppSettings>>('get_settings')
|
||||
return response?.success && response.data ? response.data : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存应用设置
|
||||
*/
|
||||
async function saveSettings(settings: AppSettings): Promise<boolean> {
|
||||
const response = await safeInvoke<ApiResponse<void>>('save_settings', { settings })
|
||||
if (response?.success) {
|
||||
toast.add({
|
||||
title: '设置保存成功',
|
||||
description: response.message || '应用设置已保存',
|
||||
color: 'success'
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择文件
|
||||
*/
|
||||
async function selectFile(): Promise<string | null> {
|
||||
const response = await safeInvoke<ApiResponse<string>>('select_file')
|
||||
return response?.success && response.data ? response.data : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择多个文件
|
||||
*/
|
||||
async function selectFiles(): Promise<string[] | null> {
|
||||
const response = await safeInvoke<ApiResponse<string[]>>('select_files')
|
||||
return response?.success && response.data ? response.data : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听播放器状态更新事件
|
||||
*/
|
||||
async function listenPlayerStateUpdate(callback: (state: PlayerState) => void): Promise<UnlistenFn | null> {
|
||||
try {
|
||||
return await listen<PlayerState>('player-state-update', (event) => {
|
||||
callback(event.payload)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('监听播放器状态更新失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听播放命令发送事件
|
||||
*/
|
||||
async function listenPlaybackCommandSent(callback: (command: PlaybackCommand) => void): Promise<UnlistenFn | null> {
|
||||
try {
|
||||
return await listen<PlaybackCommand>('playback-command-sent', (event) => {
|
||||
callback(event.payload)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('监听播放命令发送失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听连接状态变化事件
|
||||
*/
|
||||
async function listenConnectionStatusChange(callback: (status: string) => void): Promise<UnlistenFn | null> {
|
||||
try {
|
||||
return await listen<string>('connection-status-change', (event) => {
|
||||
callback(event.payload)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('监听连接状态变化失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 连接管理
|
||||
connectToPlayer,
|
||||
disconnectFromPlayer,
|
||||
getConnectionStatus,
|
||||
updateConnectionConfig,
|
||||
|
||||
// 播放控制
|
||||
sendPlaybackCommand,
|
||||
|
||||
// 设置管理
|
||||
getSettings,
|
||||
saveSettings,
|
||||
|
||||
// 文件操作
|
||||
selectFile,
|
||||
selectFiles,
|
||||
|
||||
// 事件监听
|
||||
listenPlayerStateUpdate,
|
||||
listenPlaybackCommandSent,
|
||||
listenConnectionStatusChange,
|
||||
}
|
||||
}
|
@@ -157,6 +157,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import type { ConnectionConfig, PlayerState } from "~/composables/useTauri";
|
||||
|
||||
interface VideoControllerState {
|
||||
connectionStatus: "connected" | "connecting" | "disconnected"
|
||||
currentVideo: string | null
|
||||
@@ -167,6 +170,16 @@
|
||||
playerAddress: string
|
||||
}
|
||||
|
||||
// Tauri API
|
||||
const {
|
||||
connectToPlayer,
|
||||
disconnectFromPlayer,
|
||||
sendPlaybackCommand,
|
||||
selectFile,
|
||||
listenPlayerStateUpdate,
|
||||
listenPlaybackCommandSent
|
||||
} = useTauri();
|
||||
|
||||
// 响应式状态
|
||||
const connectionStatus = ref<VideoControllerState["connectionStatus"]>("disconnected");
|
||||
const currentVideo = ref<string | null>(null);
|
||||
@@ -174,7 +187,16 @@
|
||||
const volume = ref(50);
|
||||
const isLooping = ref(false);
|
||||
const playlist = ref<string[]>([]);
|
||||
const playerAddress = ref("192.168.1.100:8080");
|
||||
const playerAddress = ref("127.0.0.1:6666");
|
||||
const playerState = ref<PlayerState | null>(null);
|
||||
|
||||
// 禁用不必要的自动更新,避免循环触发
|
||||
const skipProgressUpdate = ref(false);
|
||||
const skipVolumeUpdate = ref(false);
|
||||
|
||||
// 事件监听器去除函数
|
||||
let unlistenPlayerState: UnlistenFn | null = null;
|
||||
let unlistenCommandSent: UnlistenFn | null = null;
|
||||
|
||||
// 计算属性
|
||||
const isConnected = computed(() => connectionStatus.value === "connected");
|
||||
@@ -187,67 +209,163 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 创建连接配置
|
||||
const createConnectionConfig = (): ConnectionConfig => {
|
||||
const address = playerAddress.value || "";
|
||||
const parts = address.split(":");
|
||||
const host = parts[0] || "192.168.1.197";
|
||||
const port = Number.parseInt(parts[1] || "") || 6666;
|
||||
return {
|
||||
host,
|
||||
port,
|
||||
timeout: 10,
|
||||
autoReconnect: true,
|
||||
reconnectInterval: 5
|
||||
};
|
||||
};
|
||||
|
||||
// 更新UI状态
|
||||
const updateUIFromPlayerState = (state: PlayerState) => {
|
||||
playerState.value = state;
|
||||
|
||||
// 更新连接状态
|
||||
if (typeof state.connectionStatus === "string") {
|
||||
connectionStatus.value = state.connectionStatus as any;
|
||||
} else if (state.connectionStatus && typeof state.connectionStatus === "object" && "error" in state.connectionStatus) {
|
||||
connectionStatus.value = "disconnected";
|
||||
}
|
||||
|
||||
// 更新播放状态
|
||||
if (state.currentVideo) {
|
||||
currentVideo.value = state.currentVideo.title || state.currentVideo.path;
|
||||
}
|
||||
|
||||
// 避免循环更新
|
||||
skipProgressUpdate.value = true;
|
||||
skipVolumeUpdate.value = true;
|
||||
|
||||
// 更新进度和音量
|
||||
if (state.duration > 0) {
|
||||
progress.value = Math.round((state.position / state.duration) * 100);
|
||||
}
|
||||
volume.value = Math.round(state.volume);
|
||||
isLooping.value = state.isLooping;
|
||||
|
||||
// 更新播放列表
|
||||
if (state.playlist && state.playlist.length > 0) {
|
||||
playlist.value = state.playlist.map((video) => video.title || video.path);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
skipProgressUpdate.value = false;
|
||||
skipVolumeUpdate.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 方法
|
||||
const toggleConnection = async () => {
|
||||
if (connectionStatus.value === "connected") {
|
||||
// 断开连接
|
||||
connectionStatus.value = "disconnected";
|
||||
const success = await disconnectFromPlayer();
|
||||
if (success) {
|
||||
connectionStatus.value = "disconnected";
|
||||
playerState.value = null;
|
||||
currentVideo.value = null;
|
||||
|
||||
// 清理事件监听器
|
||||
if (unlistenPlayerState) {
|
||||
unlistenPlayerState();
|
||||
unlistenPlayerState = null;
|
||||
}
|
||||
if (unlistenCommandSent) {
|
||||
unlistenCommandSent();
|
||||
unlistenCommandSent = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 尝试连接
|
||||
connectionStatus.value = "connecting";
|
||||
// TODO: 在这里调用 Tauri API 进行实际连接
|
||||
setTimeout(() => {
|
||||
connectionStatus.value = "connected"; // 模拟连接成功
|
||||
}, 1000);
|
||||
const config = createConnectionConfig();
|
||||
const state = await connectToPlayer(config);
|
||||
|
||||
if (state) {
|
||||
connectionStatus.value = "connected";
|
||||
updateUIFromPlayerState(state);
|
||||
|
||||
// 设置事件监听器
|
||||
unlistenPlayerState = await listenPlayerStateUpdate(updateUIFromPlayerState);
|
||||
unlistenCommandSent = await listenPlaybackCommandSent((command) => {
|
||||
console.log("播放命令已发送:", command);
|
||||
});
|
||||
} else {
|
||||
connectionStatus.value = "disconnected";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const playVideo = async () => {
|
||||
// TODO: 调用 Tauri API
|
||||
console.log("播放视频");
|
||||
await sendPlaybackCommand("play");
|
||||
};
|
||||
|
||||
const pauseVideo = async () => {
|
||||
// TODO: 调用 Tauri API
|
||||
console.log("暂停视频");
|
||||
await sendPlaybackCommand("pause");
|
||||
};
|
||||
|
||||
const stopVideo = async () => {
|
||||
// TODO: 调用 Tauri API
|
||||
console.log("停止视频");
|
||||
await sendPlaybackCommand("stop");
|
||||
};
|
||||
|
||||
const toggleLoop = async () => {
|
||||
isLooping.value = !isLooping.value;
|
||||
// TODO: 调用 Tauri API
|
||||
console.log("循环播放:", isLooping.value);
|
||||
const newLooping = !isLooping.value;
|
||||
isLooping.value = newLooping;
|
||||
await sendPlaybackCommand({ setLoop: { enabled: newLooping } });
|
||||
};
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
// TODO: 调用 Tauri API
|
||||
console.log("切换全屏");
|
||||
await sendPlaybackCommand("toggleFullscreen");
|
||||
// 同步将控制端窗口也切换全屏,提升操控体验
|
||||
try {
|
||||
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
||||
const win = getCurrentWindow();
|
||||
const isFs = await win.isFullscreen();
|
||||
await win.setFullscreen(!isFs);
|
||||
} catch (e) {
|
||||
console.warn("切换控制端全屏失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const openVideoFile = async () => {
|
||||
// TODO: 调用 Tauri API 打开文件选择器
|
||||
console.log("打开文件");
|
||||
// 模拟添加到播放列表
|
||||
playlist.value.push(`示例视频${playlist.value.length + 1}.mp4`);
|
||||
const filePath = await selectFile();
|
||||
if (filePath) {
|
||||
// 加载视频文件
|
||||
await sendPlaybackCommand({ loadVideo: { path: filePath } });
|
||||
|
||||
// 添加到播放列表
|
||||
const fileName = filePath.split("/").pop() || filePath;
|
||||
if (!playlist.value.includes(fileName)) {
|
||||
playlist.value.push(fileName);
|
||||
}
|
||||
|
||||
// 更新当前视频
|
||||
currentVideo.value = fileName;
|
||||
}
|
||||
};
|
||||
|
||||
const playVideoFromPlaylist = async (index: number) => {
|
||||
await sendPlaybackCommand({ playFromPlaylist: { index } });
|
||||
currentVideo.value = playlist.value[index] || null;
|
||||
// TODO: 调用 Tauri API 播放指定视频
|
||||
console.log("播放:", currentVideo.value);
|
||||
};
|
||||
|
||||
const removeFromPlaylist = (index: number) => {
|
||||
playlist.value.splice(index, 1);
|
||||
// TODO: 同步更新播放器端播放列表
|
||||
};
|
||||
|
||||
const clearPlaylist = () => {
|
||||
const clearPlaylist = async () => {
|
||||
playlist.value = [];
|
||||
currentVideo.value = null;
|
||||
// 发送空播放列表到播放器
|
||||
await sendPlaybackCommand({ setPlaylist: { videos: [] } });
|
||||
};
|
||||
|
||||
// 监听进度变化
|
||||
|
@@ -10,9 +10,9 @@
|
||||
<div class="space-y-4">
|
||||
<UFormGroup label="视频播放器地址" description="设置要连接的视频播放器的IP地址和端口">
|
||||
<div class="flex space-x-2">
|
||||
<UInput v-model="settings.playerHost" placeholder="192.168.1.100" class="flex-1 mb-3" />
|
||||
<UInput v-model="settings.playerHost" placeholder="127.0.0.1" class="flex-1 mb-3" />
|
||||
<span class="self-center text-gray-500">:</span>
|
||||
<UInput v-model="settings.playerPort" type="number" placeholder="8080" class="w-24" />
|
||||
<UInput v-model="settings.playerPort" type="number" placeholder="6666" class="w-24" />
|
||||
</div>
|
||||
</UFormGroup>
|
||||
|
||||
@@ -169,8 +169,8 @@
|
||||
|
||||
// 响应式设置数据
|
||||
const settings = ref<AppSettings>({
|
||||
playerHost: "192.168.1.100",
|
||||
playerPort: 8080,
|
||||
playerHost: "127.0.0.1",
|
||||
playerPort: 6666,
|
||||
connectionTimeout: 10,
|
||||
autoReconnect: true,
|
||||
reconnectInterval: 5,
|
||||
@@ -222,8 +222,8 @@
|
||||
const resetSettings = async () => {
|
||||
// 重置为默认设置
|
||||
settings.value = {
|
||||
playerHost: "192.168.1.100",
|
||||
playerPort: 8080,
|
||||
playerHost: "127.0.0.1",
|
||||
playerPort: 6666,
|
||||
connectionTimeout: 10,
|
||||
autoReconnect: true,
|
||||
reconnectInterval: 5,
|
||||
|
@@ -79,7 +79,7 @@ impl Default for ConnectionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "192.168.1.100".to_string(),
|
||||
port: 8080,
|
||||
port: 6666,
|
||||
timeout: 10,
|
||||
auto_reconnect: true,
|
||||
reconnect_interval: 5,
|
||||
|
@@ -3,17 +3,20 @@ use futures_util::{SinkExt, StreamExt};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::{interval, sleep, Duration};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message, WebSocketStream, MaybeTlsStream};
|
||||
|
||||
pub type WebSocket = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConnectionService {
|
||||
config: Arc<RwLock<ConnectionConfig>>,
|
||||
state: Arc<RwLock<PlayerState>>,
|
||||
tx: Option<mpsc::UnboundedSender<PlaybackCommand>>,
|
||||
websocket: Arc<Mutex<Option<WebSocket>>>,
|
||||
stop_signal: Arc<AtomicBool>,
|
||||
task_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ConnectionService {
|
||||
@@ -23,12 +26,14 @@ impl ConnectionService {
|
||||
state: Arc::new(RwLock::new(PlayerState::default())),
|
||||
tx: None,
|
||||
websocket: Arc::new(Mutex::new(None)),
|
||||
stop_signal: Arc::new(AtomicBool::new(false)),
|
||||
task_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<(), String> {
|
||||
let config = self.config.read().await.clone();
|
||||
let url = format!("ws://{}:{}/ws", config.host, config.port);
|
||||
let url = format!("ws://{}:{}", config.host, config.port);
|
||||
|
||||
info!("尝试连接到视频播放器: {}", url);
|
||||
|
||||
@@ -92,12 +97,17 @@ impl ConnectionService {
|
||||
let websocket = self.websocket.clone();
|
||||
let state = self.state.clone();
|
||||
let config = self.config.clone();
|
||||
let stop_flag = self.stop_signal.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let handle = tokio::spawn(async move {
|
||||
// 心跳检测
|
||||
let mut heartbeat_interval = interval(Duration::from_secs(30));
|
||||
|
||||
loop {
|
||||
if stop_flag.load(Ordering::Relaxed) {
|
||||
info!("停止消息处理任务");
|
||||
break;
|
||||
}
|
||||
tokio::select! {
|
||||
// 处理发送的命令
|
||||
command = rx.recv() => {
|
||||
@@ -124,6 +134,7 @@ impl ConnectionService {
|
||||
message = Self::receive_message(&websocket) => {
|
||||
match message {
|
||||
Ok(msg) => {
|
||||
if msg.is_empty() { continue; }
|
||||
if let Err(e) = Self::handle_player_message(&state, msg).await {
|
||||
error!("处理播放器消息失败: {}", e);
|
||||
}
|
||||
@@ -139,6 +150,8 @@ impl ConnectionService {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.task_handle = Some(handle);
|
||||
}
|
||||
|
||||
async fn send_command(
|
||||
@@ -148,10 +161,29 @@ impl ConnectionService {
|
||||
let mut ws_guard = websocket.lock().await;
|
||||
|
||||
if let Some(ws) = ws_guard.as_mut() {
|
||||
let command_json = serde_json::to_string(&command)
|
||||
// Convert internal command enum to player-compatible wire format
|
||||
let payload = match command.clone() {
|
||||
PlaybackCommand::Play => serde_json::json!({"type": "play"}),
|
||||
PlaybackCommand::Pause => serde_json::json!({"type": "pause"}),
|
||||
PlaybackCommand::Stop => serde_json::json!({"type": "stop"}),
|
||||
PlaybackCommand::Seek { position } => serde_json::json!({"type": "seek", "position": position}),
|
||||
PlaybackCommand::SetVolume { volume } => serde_json::json!({"type": "setVolume", "volume": volume}),
|
||||
PlaybackCommand::SetLoop { enabled } => serde_json::json!({"type": "setLoop", "enabled": enabled}),
|
||||
PlaybackCommand::ToggleFullscreen => serde_json::json!({"type": "toggleFullscreen"}),
|
||||
PlaybackCommand::LoadVideo { path } => serde_json::json!({"type": "loadVideo", "path": path}),
|
||||
PlaybackCommand::SetPlaylist { videos } => serde_json::json!({"type": "setPlaylist", "videos": videos}),
|
||||
PlaybackCommand::PlayFromPlaylist { index } => serde_json::json!({"type": "playFromPlaylist", "index": index}),
|
||||
};
|
||||
|
||||
let message = serde_json::json!({
|
||||
"type": "command",
|
||||
"data": payload
|
||||
});
|
||||
|
||||
let message_text = serde_json::to_string(&message)
|
||||
.map_err(|e| format!("序列化命令失败: {}", e))?;
|
||||
|
||||
ws.send(Message::Text(command_json))
|
||||
|
||||
ws.send(Message::Text(message_text))
|
||||
.await
|
||||
.map_err(|e| format!("发送消息失败: {}", e))?;
|
||||
|
||||
@@ -203,7 +235,9 @@ impl ConnectionService {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err("WebSocket 连接不可用".to_string())
|
||||
// 未连接时避免刷屏日志:短暂等待并返回空消息
|
||||
sleep(Duration::from_millis(300)).await;
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,29 +249,83 @@ impl ConnectionService {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 尝试解析播放器状态更新
|
||||
match serde_json::from_str::<PlayerState>(&message) {
|
||||
Ok(new_state) => {
|
||||
let mut current_state = state.write().await;
|
||||
current_state.playback_status = new_state.playback_status;
|
||||
current_state.position = new_state.position;
|
||||
current_state.duration = new_state.duration;
|
||||
current_state.volume = new_state.volume;
|
||||
current_state.is_looping = new_state.is_looping;
|
||||
current_state.is_fullscreen = new_state.is_fullscreen;
|
||||
|
||||
if let Some(video) = new_state.current_video {
|
||||
current_state.current_video = Some(video);
|
||||
}
|
||||
|
||||
debug!("更新播放器状态");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("无法解析播放器状态: {} - 消息: {}", e, message);
|
||||
Ok(()) // 不是致命错误,继续运行
|
||||
}
|
||||
// 解析播放器发来的 { type: "status", data: { ... } }
|
||||
let value: serde_json::Value = serde_json::from_str(&message)
|
||||
.map_err(|e| format!("解析JSON失败: {}", e))?;
|
||||
|
||||
let msg_type = value.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if msg_type != "status" {
|
||||
// 忽略非状态消息
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = value.get("data").ok_or_else(|| "缺少data字段".to_string())?;
|
||||
|
||||
// 将字符串状态映射为内部枚举
|
||||
let playback_status = match data.get("playback_status").and_then(|v| v.as_str()).unwrap_or("stopped") {
|
||||
"playing" => crate::models::PlaybackStatus::Playing,
|
||||
"paused" => crate::models::PlaybackStatus::Paused,
|
||||
"loading" => crate::models::PlaybackStatus::Loading,
|
||||
_ => crate::models::PlaybackStatus::Stopped,
|
||||
};
|
||||
|
||||
let connection_status = match data.get("connection_status").and_then(|v| v.as_str()).unwrap_or("disconnected") {
|
||||
"connected" => crate::models::ConnectionStatus::Connected,
|
||||
"connecting" => crate::models::ConnectionStatus::Connecting,
|
||||
"disconnected" => crate::models::ConnectionStatus::Disconnected,
|
||||
other => crate::models::ConnectionStatus::Error(other.to_string()),
|
||||
};
|
||||
|
||||
let position = data.get("position").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let duration = data.get("duration").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let volume = data.get("volume").and_then(|v| v.as_f64()).unwrap_or(50.0);
|
||||
let is_looping = data.get("is_looping").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let is_fullscreen = data.get("is_fullscreen").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
|
||||
let current_video = if let Some(v) = data.get("current_video") {
|
||||
let path = v.get("path").and_then(|s| s.as_str()).unwrap_or("").to_string();
|
||||
if path.is_empty() { None } else {
|
||||
Some(crate::models::VideoInfo {
|
||||
title: v.get("title").and_then(|s| s.as_str()).unwrap_or("").to_string(),
|
||||
path,
|
||||
duration: v.get("duration").and_then(|n| n.as_f64()),
|
||||
size: v.get("size").and_then(|n| n.as_u64()),
|
||||
format: v.get("format").and_then(|s| s.as_str()).map(|s| s.to_string()),
|
||||
})
|
||||
}
|
||||
} else { None };
|
||||
|
||||
let playlist = if let Some(arr) = data.get("playlist").and_then(|a| a.as_array()) {
|
||||
arr.iter().filter_map(|v| {
|
||||
let path = v.get("path").and_then(|s| s.as_str()).unwrap_or("").to_string();
|
||||
if path.is_empty() { None } else {
|
||||
Some(crate::models::VideoInfo {
|
||||
title: v.get("title").and_then(|s| s.as_str()).unwrap_or("").to_string(),
|
||||
path,
|
||||
duration: v.get("duration").and_then(|n| n.as_f64()),
|
||||
size: v.get("size").and_then(|n| n.as_u64()),
|
||||
format: v.get("format").and_then(|s| s.as_str()).map(|s| s.to_string()),
|
||||
})
|
||||
}
|
||||
}).collect()
|
||||
} else { vec![] };
|
||||
|
||||
let current_playlist_index = data.get("current_playlist_index").and_then(|n| n.as_u64()).map(|n| n as usize);
|
||||
|
||||
let mut current_state = state.write().await;
|
||||
current_state.connection_status = connection_status;
|
||||
current_state.playback_status = playback_status;
|
||||
current_state.position = position;
|
||||
current_state.duration = duration;
|
||||
current_state.volume = volume;
|
||||
current_state.is_looping = is_looping;
|
||||
current_state.is_fullscreen = is_fullscreen;
|
||||
current_state.current_video = current_video;
|
||||
current_state.playlist = playlist;
|
||||
current_state.current_playlist_index = current_playlist_index;
|
||||
|
||||
debug!("更新播放器状态");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_reconnection(
|
||||
@@ -274,6 +362,9 @@ impl ConnectionService {
|
||||
pub async fn disconnect(&mut self) {
|
||||
info!("断开连接");
|
||||
|
||||
// 通知后台任务停止并尝试结束
|
||||
self.stop_signal.store(true, Ordering::Relaxed);
|
||||
|
||||
// 关闭 WebSocket 连接
|
||||
{
|
||||
let mut ws_guard = self.websocket.lock().await;
|
||||
@@ -283,6 +374,14 @@ impl ConnectionService {
|
||||
*ws_guard = None;
|
||||
}
|
||||
|
||||
// 停止后台任务
|
||||
if let Some(handle) = self.task_handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
// 重置停止标志,方便下次连接
|
||||
self.stop_signal.store(false, Ordering::Relaxed);
|
||||
|
||||
// 更新状态
|
||||
{
|
||||
let mut state = self.state.write().await;
|
||||
|
Reference in New Issue
Block a user