# client_webrtc.py
import asyncio
import base64
import json
import logging
import tkinter as tk
# import tkinter as tk
from queue import Queue, Empty
from tkinter import messagebox
from tkinter import ttk
import subprocess
import threading
import ctypes
import sys
import urllib.parse
from tkinter import simpledialog
from io import BytesIO
import pygetwindow as gw
import os
from pathlib import Path
import platform

import websockets
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCConfiguration, RTCIceServer, RTCIceCandidate
from aiortc.sdp import candidate_from_sdp
from aiortc.contrib.media import MediaStreamTrack as AiortcMediaStreamTrack

# 嘗試匯入 pynput，如果失敗則無法進行遠端控制
try:
    from pynput.mouse import Button, Controller as MouseController
    from pynput.keyboard import Key, Controller as KeyboardController
    PYNPUT_AVAILABLE = True
    mouse = MouseController()
    keyboard = KeyboardController()
except ImportError:
    PYNPUT_AVAILABLE = False
    
from av import VideoFrame
from mss import mss
from PIL import Image

try:
    import pyaudio
    PYAUDIO_AVAILABLE = True
except ImportError:
    PYAUDIO_AVAILABLE = False


# --- 日誌設定 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("client_webrtc")
logger.propagate = False # 阻止日誌向上傳播到 root logger

class QueueHandler(logging.Handler):
    def __init__(self, log_queue):
        super().__init__()
        self.log_queue = log_queue

    def emit(self, record):
        self.log_queue.put(record)

CONFIG = {}

def get_audio_output_devices():
    """使用 PyAudio 取得所有音訊輸出設備的列表。"""
    if not PYAUDIO_AVAILABLE:
        return []
    p = pyaudio.PyAudio()
    devices = []
    for i in range(p.get_device_count()):
        dev_info = p.get_device_info_by_index(i)
        if dev_info.get('maxOutputChannels') > 0:
            devices.append(dev_info['name'])
    p.terminate()
    return devices

class PyAudioPlayer:
    """一個使用 PyAudio 播放原始 PCM 音訊串流的類別。"""
    def __init__(self, audio_device_name=None, channels=1, rate=48000, audio_format=pyaudio.paInt16):
        if not PYAUDIO_AVAILABLE:
            raise RuntimeError("PyAudio is not available. Cannot create player.")

        logger.info("正在初始化 PyAudio 播放器...")
        self.p = pyaudio.PyAudio()
        self.audio_queue = asyncio.Queue(maxsize=5) # *** 關鍵修正：使用 asyncio.Queue 並限制大小 ***
        self.format_size = self.p.get_sample_size(audio_format) # *** 關鍵修正：儲存樣本大小 ***
        self.buffer = bytearray() # *** 關鍵優化：建立內部緩衝區 ***
        # --- 關鍵優化：改用執行緒安全的 queue.Queue，並設定緩衝區大小 ---
        self.audio_queue = Queue(maxsize=5)
        self.format_size = self.p.get_sample_size(audio_format)

        device_index = None
        if audio_device_name:
            for i in range(self.p.get_device_count()):
                dev_info = self.p.get_device_info_by_index(i)
                if dev_info.get('maxOutputChannels') > 0 and audio_device_name in dev_info['name']:
                    device_index = i
                    logger.info(f"找到匹配的音訊輸出設備: {dev_info['name']} (Index: {i})")
                    break
            if device_index is None:
                logger.warning(f"找不到指定的音訊設備 '{audio_device_name}'，將使用預設設備。")

        # *** 關鍵修正：使用回呼模式 (Callback Mode) ***
        # --- 關鍵優化：繼續使用回呼模式，但簡化回呼邏輯 ---
        self.stream = self.p.open(
            format=audio_format,
            channels=channels,
            rate=rate,
            output=True,
            output_device_index=device_index,
            stream_callback=self._audio_callback
            stream_callback=self._audio_callback,
        )
        logger.info("PyAudio 播放器已啟動。")

    def _audio_callback(self, in_data, frame_count, time_info, status):
        """PyAudio 需要音訊資料時會呼叫此函式。"""
        required_bytes = frame_count * self.stream._channels * self.format_size
        try:
            # *** 關鍵優化：使用內部緩衝區湊齊資料 ***
            while len(self.buffer) < required_bytes:
                # 當緩衝區資料不夠時，從佇列中取出並追加
                self.buffer.extend(self.audio_queue.get_nowait())

            # 從緩衝區頭部取出所需長度的資料
            output_data = bytes(self.buffer[:required_bytes])
            # 更新緩衝區，移除已取出的資料
            self.buffer = self.buffer[required_bytes:]
            
            return (output_data, pyaudio.paContinue)
        except asyncio.QueueEmpty:
            # --- 關鍵優化：直接從執行緒安全的佇列中獲取資料，帶有超時 ---
            # 超時設定很短，以避免阻塞回呼執行緒，同時給予一點緩衝時間
            data = self.audio_queue.get(timeout=0.01)
            return (data, pyaudio.paContinue)
        except Empty:
            # 佇列為空，表示網路延遲，填充靜音資料以避免爆音
            required_bytes = frame_count * self.stream._channels * self.format_size
            return (b'\x00' * required_bytes, pyaudio.paContinue)
        except Exception as e:
            logger.error(f"PyAudio 回呼函式發生錯誤: {e}")
            return (None, pyaudio.paAbort)

    async def write_frame(self, frame):
        """非同步地將音訊幀放入佇列。"""
    def write_frame(self, data):
        """將音訊幀放入佇列 (此為阻塞操作，應在 executor 中執行)。"""
        try:
            # to_ndarray().tobytes() 是 CPU 密集型操作，但通常很快
            data = frame.to_ndarray().tobytes()
            await self.audio_queue.put(data)
        except asyncio.QueueFull:
            logger.warning("音訊佇列已滿，丟棄一個舊的音訊幀以降低延遲。")
            # 丟棄最舊的幀，然後放入新的幀
            self.audio_queue.get_nowait()
            await self.audio_queue.put(data)
            self.audio_queue.put(data, block=False)
        except Full:
            # 佇列已滿，丟棄最舊的幀，然後放入新的幀以降低延遲
            try:
                self.audio_queue.get_nowait()
                self.audio_queue.put(data, block=False)
                logger.warning("音訊佇列已滿，丟棄一個舊的音訊幀。")
            except (Empty, Full):
                pass # 如果在處理期間佇列狀態再次變化，則忽略

    def stop(self):
        """停止並關閉 PyAudio 串流和實例。"""
        if self.stream and self.stream.is_active(): # 確保 stream 存在且活躍
            self.stream.stop_stream()
            self.stream.close()
        if self.p: # 確保 pyaudio 實例存在
            self.p.terminate()
        logger.info("PyAudio 播放器已完全停止。")


class RegionSelector(tk.Toplevel): # 這裡的 RegionSelector 應該是 App 的內部類別或由 App 實例化
    """一個可拖動、可調整大小的半透明視窗，用於選擇分享區域"""
    def __init__(self, app_instance):
        super().__init__()
        self.app = app_instance

        # 移除預設的視窗邊框和標題列
        self.overrideredirect(True)

        # 設定視窗最上層顯示
        self.attributes("-topmost", True)
        # --- 新增：設定半透明效果 (0.0 完全透明, 1.0 完全不透明) ---
        self.attributes("-alpha", 0.75)

        # --- 美化：定義科技感配色 ---
        BG_COLOR = "#282c34"
        ACCENT_COLOR = "#00FFFF" # 亮青色
        TRANSPARENT_KEY = "#abcdef" # 一個不太可能出現的顏色作為透明色鍵

        # 建立一個假的標題列
        title_bar = tk.Frame(self, bg=BG_COLOR, relief="raised", bd=0)
        title_bar.pack(fill=tk.X)
        
        title_label = tk.Label(title_bar, text="拖曳或縮放此區域", bg=BG_COLOR, fg="white", font=("Consolas", 10))
        title_label.pack(side=tk.LEFT, padx=5)

        close_button = tk.Button(title_bar, text="✕", bg=BG_COLOR, fg="white", activebackground="#E81123", activeforeground="white", command=self.close_and_stop, relief="flat", bd=0, width=4)
        close_button.pack(side=tk.RIGHT)
        close_button.bind("<Enter>", lambda e: e.widget.config(bg="#E81123"))
        close_button.bind("<Leave>", lambda e: e.widget.config(bg=BG_COLOR))

        # 讓標題列可以拖動視窗
        title_bar.bind("<ButtonPress-1>", self.on_drag_start)
        title_bar.bind("<B1-Motion>", self.on_drag_motion)
        title_label.bind("<ButtonPress-1>", self.on_drag_start)
        title_label.bind("<B1-Motion>", self.on_drag_motion)
        
        # 設定視窗主體為透明
        self.config(bg=TRANSPARENT_KEY)
        self.wm_attributes("-transparentcolor", TRANSPARENT_KEY)

        # --- 修正：使用 Canvas 來繪製邊框，確保在透明背景下可見 ---
        self.border_canvas = tk.Canvas(self, bg=TRANSPARENT_KEY, highlightthickness=0)
        self.border_canvas.pack(fill=tk.BOTH, expand=True)
        self.border_canvas.create_rectangle(2, 2, 0, 0, outline=ACCENT_COLOR, width=4) # 繪製矩形邊框

        # 綁定拖曳和縮放事件
        self.bind("<Configure>", self.on_drag_or_resize_end) # 監聽視窗位置或大小變化
        
        # --- 美化：右下角的縮放點 ---
        self.grip = tk.Frame(self, bg=TRANSPARENT_KEY)
        self.grip.place(relx=1.0, rely=1.0, anchor="se", width=20, height=20)
        grip_canvas = tk.Canvas(self.grip, width=20, height=20, bg=TRANSPARENT_KEY, highlightthickness=0, cursor="sizing")
        grip_canvas.pack()
        grip_canvas.create_line(8, 20, 20, 8, fill=ACCENT_COLOR, width=3)
        grip_canvas.create_line(14, 20, 20, 14, fill=ACCENT_COLOR, width=3)
        # --- 修正：將縮放事件直接綁定到 Canvas 上 ---
        grip_canvas.bind("<ButtonPress-1>", self.on_resize_start)
        grip_canvas.bind("<B1-Motion>", self.on_resize_motion)

        # 綁定 Canvas 的 Configure 事件來更新邊框大小
        self.border_canvas.bind("<Configure>", self.on_canvas_resize)

        # --- 修正：將拖曳事件從整個視窗分離，避免與縮放衝突 ---
        self.border_canvas.bind("<ButtonPress-1>", self.on_drag_start)
        self.border_canvas.bind("<B1-Motion>", self.on_drag_motion)

    def on_drag_start(self, event):
        self.x = event.x
        self.y = event.y
        return "break" # 同樣阻止事件傳播

    def on_drag_motion(self, event):
        x = self.winfo_x() - self.x + event.x
        y = self.winfo_y() - self.y + event.y
        self.geometry(f"+{x}+{y}")

    def on_resize_start(self, event):
        self.resize_start_x = event.x_root
        self.resize_start_y = event.y_root
        self.start_width = self.winfo_width()
        self.start_height = self.winfo_height()
        return "break" # 阻止事件傳播，避免觸發拖動

    def on_resize_motion(self, event):
        delta_x = event.x_root - self.resize_start_x
        delta_y = event.y_root - self.resize_start_y
        new_width = max(100, self.start_width + delta_x)
        new_height = max(100, self.start_height + delta_y)
        self.geometry(f"{new_width}x{new_height}")

    def on_canvas_resize(self, event):
        """當 Canvas 大小改變時，重繪邊框以填滿 Canvas"""
        self.border_canvas.coords(1, 2, 2, event.width-2, event.height-2)

    def on_drag_or_resize_end(self, event):
        # 拖曳或調整大小結束時，立即通知 App 更新分享區域
        region = {
            "top": self.winfo_y(),
            "left": self.winfo_x(),
            "width": self.winfo_width(),
            "height": self.winfo_height(),
        }
        # 更新 App 中的擷取區域並儲存到 config.json
        self.app.on_region_selected(('region', region))

    def close_and_stop(self):
        """關閉視窗並觸發停止分享"""
        self.app.stop_sharing()
        
    def get_region(self):
        """回傳當前視窗的幾何資訊"""
        return {
            "top": self.winfo_y(),
            "left": self.winfo_x(),
            "width": self.winfo_width(),
            "height": self.winfo_height(),
        }

class ScreenShareTrack(AiortcMediaStreamTrack):
    """
    一個 MediaStreamTrack，用於從螢幕擷取影像。
    """
    kind = "video"

    def __init__(self, window_title=None, region=None):
        super().__init__()
        self.sct = mss()
        self.window_title = window_title
        self.region = region
        self.target_window = None

        if self.region:
            logger.info(f"設定為分享區域: {self.region}")
        elif self.window_title:
            logger.info(f"設定為分享視窗，標題包含: '{self.window_title}'")
        else:
            logger.info("設定為分享整個主螢幕。")

    async def next_timestamp(self):
        """
        手動實作時間戳產生器，以確保相容性。
        """
        if not hasattr(self, "_timestamp"):
            self._timestamp = 0
        self._timestamp += 3000  # 假設 30 FPS (90000 / 30)
        return self._timestamp, 90000  # 90kHz 是視訊的標準時間基

    async def recv(self):
        capture_region = self.region
        if capture_region is None and self.window_title:
            try:
                # 嘗試尋找視窗
                windows = gw.getWindowsWithTitle(self.window_title)
                if windows:
                    self.target_window = windows[0]
                    # 確保視窗不是最小化狀態
                    if not self.target_window.isMinimized:
                        capture_region = {
                            "top": self.target_window.top,
                            "left": self.target_window.left,
                            "width": self.target_window.width,
                            "height": self.target_window.height,
                        }
                else:
                    self.target_window = None # 視窗已關閉
            except Exception as e:
                logger.warning(f"尋找視窗 '{self.window_title}' 時發生錯誤: {e}")
                self.target_window = None
        
        # 如果找不到視窗或未設定視窗標題，則退回分享整個主螢幕
        if capture_region is None:
            capture_region = self.sct.monitors[1]

        sct_img = self.sct.grab(capture_region)
        img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")

        # aiortc 需要 VideoFrame 格式
        frame = VideoFrame.from_image(img)
        
        # 設定時間戳
        pts, time_base = await self.next_timestamp()
        frame.pts = pts
        frame.time_base = time_base
        
        return frame

class WebRTCClient:
    def __init__(self, sharer_id, password, server_url, ice_servers):
        self.sharer_id = sharer_id
        self.password = password
        self.server_url = server_url
        self.ice_servers = ice_servers
        self.websocket = None
        self.loop = None
        self.peer_connections = {}  # {controller_id: RTCPeerConnection}
        self.audio_player = None
        self.audio_tasks = []  # --- 新增：追蹤所有音訊播放任務 ---
        
    async def connect(self):
        """連線到信令伺服器並註冊"""
        logger.info("正在嘗試連線到信令伺服器 %s", self.server_url)
        self.websocket = await websockets.connect(self.server_url)
        logger.info("成功連線到信令伺服器")

        register_payload = {
            "type": "register_sharer",
            "id": self.sharer_id,
            "password": self.password,
        }
        await self.websocket.send(json.dumps(register_payload))
        logger.info("已註冊為分享端，ID: %s", self.sharer_id)

    async def run(self):
        """主執行迴圈，處理信令訊息"""
        await self.connect()
        try:
            async for message in self.websocket:
                await self.handle_message(message)
        except websockets.exceptions.ConnectionClosed as e:
            logger.warning("與信令伺服器的連線已關閉: %s", e)
        finally:
            logger.info("正在關閉所有 WebRTC 連線和播放器...")
            # --- 新增：取消所有正在運行的音訊任務 ---
            for task in self.audio_tasks:
                if not task.done():
                    task.cancel()
            self.audio_tasks.clear()
            for pc in self.peer_connections.values():
                await pc.close()
            self.peer_connections.clear()
            self.stop_audio_player()

    async def handle_message(self, message):
        """解析並分派信令訊息"""
        try:
            data = json.loads(message)
            msg_type = data.get("type")
            controller_id = data.get("from_id")

            if msg_type == "request_to_connect" and controller_id:
                await self.create_peer_connection(controller_id)
            elif msg_type == "offer_to_sharer" and controller_id:
                await self.handle_offer(controller_id, data["offer"])
            elif msg_type == "answer_to_sharer" and controller_id:
                await self.handle_answer(controller_id, data["answer"])
            elif msg_type == "ice_to_sharer" and controller_id:
                await self.handle_ice_candidate(controller_id, data["ice"])
            elif msg_type == "request_screenshot":
                # 處理來自儀表板的截圖請求
                dashboard_id = data.get("from_id")
                await self.send_screenshot(dashboard_id)
            elif msg_type == "remote_control":
                command = data.get("command")
                if command and command.get("type") == "shutdown_client":
                    logger.warning("收到遠端關機指令，程式將在 3 秒後關閉...")
                    if app_instance:
                        app_instance.root.after(3000, app_instance.on_closing)
            elif msg_type == "restart_sharer":
                logger.warning("收到來自 %s 的重啟指令，準備重啟...", controller_id)
                app_instance.restart_app()
            elif msg_type == "controller_disconnected" and controller_id:
                logger.info("控制器 %s 已斷線，正在清理連線。", controller_id)
                await self.cleanup_peer_connection(controller_id)
            else:
                logger.warning("收到未知的或不完整的訊息: %s", data)
        except json.JSONDecodeError:
            logger.error("收到非 JSON 格式的訊息: %s", message)
        except Exception as e:
            logger.error("處理訊息時發生錯誤: %s", e, exc_info=True)

    async def _setup_peer_connection(self, controller_id):
        """
        (內部函式) 建立並設定一個新的 RTCPeerConnection 實例及事件處理。
        返回建立的 pc 物件。
        """
        # 將字典列表轉換為 RTCIceServer 物件列表
        ice_servers_obj = [RTCIceServer(**server) for server in self.ice_servers]
        # 建立 RTCConfiguration 物件
        configuration = RTCConfiguration(iceServers=ice_servers_obj)
        # 將 configuration 傳遞給 RTCPeerConnection
        pc = RTCPeerConnection(configuration=configuration)
        self.peer_connections[controller_id] = pc

        # 如果這是第一個連線，則啟動音訊播放器
        if self.audio_player is None and PYAUDIO_AVAILABLE:
            self.start_audio_player()

        # 當連線狀態改變時的處理
        @pc.on("connectionstatechange")
        async def on_connectionstatechange():
            logger.info("WebRTC 連線狀態 (%s): %s", controller_id, pc.connectionState)
            if pc.connectionState == "failed" or pc.connectionState == "closed":
                await self.cleanup_peer_connection(controller_id)
            elif pc.connectionState == "connected":
                if app_instance:
                    app_instance.update_status(f"與 {controller_id} 的 WebRTC 連線已建立。")

        @pc.on("track")
        def on_track(track):
            logger.info(f"收到來自 {controller_id} 的軌道: {track.kind}")
            if track.kind == "audio":
                # --- 關鍵修正：動態初始化 PyAudioPlayer ---
                # 只有在收到第一個音訊幀時才初始化播放器，以匹配其屬性
                if self.audio_player is None and PYAUDIO_AVAILABLE:
                    async def _init_and_play_first_frame():
                        try:
                            first_frame = await track.recv()
                            if first_frame:
                                logger.info(f"收到第一個音訊幀，屬性: {first_frame.sample_rate}Hz, {first_frame.layout.nb_channels} channels")
                                self.audio_player = PyAudioPlayer(
                                    audio_device_name=CONFIG.get("audio_output_device"),
                                    channels=first_frame.layout.nb_channels,
                                    rate=first_frame.sample_rate,
                                    audio_format=pyaudio.paInt16 # aiortc 通常解碼為 s16
                                )
                                # 播放第一個幀
                                await self.audio_player.write_frame(first_frame)
                                audio_data = first_frame.to_ndarray().tobytes()
                                await self.loop.run_in_executor(None, self.audio_player.write_frame, audio_data)
                                self._update_volume_visualization(first_frame)
                                
                                # 建立新任務來處理後續的幀
                                audio_task = asyncio.create_task(self._play_subsequent_audio_frames(track))
                                self.audio_tasks.append(audio_task)
                            else:
                                logger.warning("未收到第一個音訊幀，無法初始化 PyAudio 播放器。")
                        except Exception as e:
                            logger.error(f"初始化 PyAudio 播放器或播放第一個幀時出錯: {e}", exc_info=True)
                            # --- 關鍵優化：更精準的錯誤處理 ---
                            logger.error(f"初始化或播放第一個音訊幀時出錯: {e}", exc_info=True)
                            self.stop_audio_player() # 清理失敗的播放器
                            # 不觸發全域重連，等待下一次音訊軌道
                    
                    asyncio.create_task(_init_and_play_first_frame())
                elif self.audio_player:
                    # 如果播放器已經初始化 (例如，來自另一個控制器的連線)，直接開始播放
                    audio_task = asyncio.create_task(self._play_subsequent_audio_frames(track))
                    self.audio_tasks.append(audio_task)
                else:
                    logger.warning("PyAudio 不可用，無法播放音訊。")

        # 建立 DataChannel 用於接收控制指令
        control_channel = pc.createDataChannel("control")
        logger.info("已建立 'control' DataChannel")

        @control_channel.on("open")
        def on_open():
            if app_instance:
                app_instance.update_status("DataChannel 'control' 已開啟。")

        @control_channel.on("message")
        def on_message(message):
            try:
                command = json.loads(message)
                cmd_type = command.get("type")
                
                # --- 移除跑馬燈和系統狀態的處理 ---
                if cmd_type == "stats_update":
                    pass # 忽略
                elif cmd_type == "text_update":
                    # 這裡可以增加處理共用文字區的邏輯 (如果需要)
                    pass
                elif PYNPUT_AVAILABLE:
                    # 處理滑鼠和鍵盤指令
                    if cmd_type == "mouse_move":
                        mouse.position = (int(command['x']), int(command['y']))
                    elif cmd_type == "mouse_down":
                        button = Button.left if command['button'] == 'left' else Button.right
                        mouse.press(button)
                    elif cmd_type == "mouse_up":
                        button = Button.left if command['button'] == 'left' else Button.right
                        mouse.release(button)
                    elif cmd_type == "key_down":
                        key = command['key']
                        keyboard.press(key)
                    elif cmd_type == "key_up":
                        key = command['key']
                        keyboard.release(key)

            except json.JSONDecodeError:
                logger.warning(f"收到非 JSON 格式的 DataChannel 訊息: {message}")
            except Exception as e:
                logger.error(f"處理 DataChannel 訊息時出錯: {e}")

        # 當 aiortc 產生 ICE candidate 時，將其發送到信令伺服器
        @pc.on("icecandidate")
        async def on_icecandidate(candidate):
            if candidate:
                logger.info("產生 ICE Candidate: %s", candidate.candidate)
                ice_payload = {
                    "type": "ice_to_controller",
                    "from_id": self.sharer_id,
                    "target_id": controller_id,
                    "ice": {"candidate": candidate.candidate, "sdpMid": candidate.sdpMid, "sdpMLineIndex": candidate.sdpMLineIndex},
                }
                await self.websocket.send(json.dumps(ice_payload))

        return pc

    async def handle_offer(self, controller_id, offer_sdp):
        """處理來自控制器的 Offer，並回傳 Answer。"""
        logger.info("收到來自 %s 的 Offer", controller_id)
        offer = RTCSessionDescription(sdp=offer_sdp["sdp"], type=offer_sdp["type"])
        pc = self.peer_connections.get(controller_id)
        if not pc or pc.connectionState == "closed":
            logger.info("為 %s 建立新的 PeerConnection 以處理 Offer", controller_id)
            pc = await self._setup_peer_connection(controller_id)
        else:
            # --- 關鍵修正：這是重新協商流程 ---
            # 如果連線已存在，則不應該重新建立，只需設定遠端描述即可。
            logger.info("在現有連線 %s 上處理重新協商 Offer", controller_id)
        
        await pc.setRemoteDescription(offer)
        
        answer = await pc.createAnswer()
        await pc.setLocalDescription(answer)
        
        answer_payload = {"type": "answer_to_controller", "from_id": self.sharer_id, "target_id": controller_id, "answer": {"sdp": answer.sdp, "type": answer.type}}
        await self.websocket.send(json.dumps(answer_payload))
        logger.info("已針對 Offer 發送 Answer 給 %s", controller_id)

    async def create_peer_connection(self, controller_id):
        """為新的控制器建立一個 Peer Connection (由 client 主動發起 Offer)"""
        logger.info("收到來自 %s 的連線請求，正在建立 WebRTC 連線...", controller_id)

        pc = await self._setup_peer_connection(controller_id)

        # 這個流程是由 client 主動發起，所以需要 addTrack
        pc.addTrack(ScreenShareTrack(window_title=CONFIG.get("window_title"), region=CONFIG.get("region")))

        # 代理模式下，初始 Offer 也不包含視訊軌道，等待截圖請求
        offer = await pc.createOffer()
        await pc.setLocalDescription(offer)
        
        offer_payload = {
            "type": "offer_to_controller",
            "from_id": self.sharer_id,
            "target_id": controller_id,
            "offer": {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type},
        }
        await self.websocket.send(json.dumps(offer_payload))
        logger.info("已發送 Offer 給 %s", controller_id)

    def start_audio_player(self):
        """此方法現在僅用於標記播放器為已啟動狀態，實際初始化在收到第一個幀時進行。"""
        if self.audio_player is None and PYAUDIO_AVAILABLE:
            logger.info("PyAudio 播放器將在收到第一個音訊幀時初始化。")

    def stop_audio_player(self):
        """停止音訊播放器"""
        if self.audio_player:
            self.audio_player.stop()
            self.audio_player = None
            logger.info("PyAudio 播放器已停止。")

    async def _play_subsequent_audio_frames(self, track):
        """從軌道接收後續音訊幀並交給播放器。"""
        if not self.audio_player:
            logger.warning("PyAudio 播放器未初始化或不可用，無法播放音訊。")
            return
        try:
            while True:
                frame = await track.recv() # 從 WebRTC 軌道接收音訊幀
                # *** 關鍵修正：非同步地將幀放入佇列 ***
                await self.audio_player.write_frame(frame)
                # --- 關鍵優化：在 executor 中執行阻塞的 put 操作 ---
                audio_data = frame.to_ndarray().tobytes()
                await self.loop.run_in_executor(None, self.audio_player.write_frame, audio_data)
                self._update_volume_visualization(frame)
        except asyncio.CancelledError:
            logger.info("音訊播放任務已取消。")
        except Exception as e:
            # --- 關鍵修正：音訊播放出錯時，觸發重連 ---
            logger.error(f"從軌道接收或播放音訊幀時出錯，將觸發重連: {e}", exc_info=True)
            if app_instance:
                app_instance.gui_queue.put((app_instance.schedule_reconnect, ()))
            # --- 關鍵優化：音訊播放出錯時，僅清理音訊部分，不觸發全域重連 ---
            logger.error(f"從軌道接收或播放音訊幀時出錯: {e}", exc_info=True)
            self.stop_audio_player()
            # 不再呼叫 schedule_reconnect()

    def _update_volume_visualization(self, frame):
        """更新 GUI 上的音量視覺化。"""
        audio_array = frame.to_ndarray()
        if audio_array.size > 0:
            rms = (audio_array.astype(float)**2).mean()**0.5
            volume_percentage = 0 if rms != rms else min(100, int((rms / 32767) * 100 * 20))
            if app_instance:
                app_instance.gui_queue.put((app_instance.draw_waveform, (volume_percentage,)))

    async def handle_answer(self, controller_id, answer_sdp):
        """處理來自控制端的 Answer"""
        pc = self.peer_connections.get(controller_id)
        if pc:
            logger.info("收到來自 %s 的 Answer", controller_id)
            answer = RTCSessionDescription(sdp=answer_sdp["sdp"], type=answer_sdp["type"])
            await pc.setRemoteDescription(answer)

    async def handle_ice_candidate(self, controller_id, ice_candidate):
        """處理來自控制端的 ICE Candidate"""
        pc = self.peer_connections.get(controller_id)
        if pc and ice_candidate:
            # 正確的作法是使用 addIceCandidate 方法
            # aiortc 0.9.28 之後的版本需要 RTCIceCandidate 物件
            candidate = candidate_from_sdp(ice_candidate["candidate"])
            candidate.sdpMid = ice_candidate["sdpMid"]
            candidate.sdpMLineIndex = ice_candidate["sdpMLineIndex"]
            await pc.addIceCandidate(candidate)

    async def cleanup_peer_connection(self, controller_id):
        """清理指定的 Peer Connection"""
        # --- 新增：在清理前取消相關的音訊任務 ---
        # 雖然 run() 的 finally 會處理，但提前處理更精確
        for task in self.audio_tasks:
            if not task.done():
                task.cancel()
        self.audio_tasks.clear()
        pc = self.peer_connections.pop(controller_id, None)
        if pc and pc.connectionState != "closed":
            await pc.close()
            # 如果這是最後一個連線，停止播放器
            if not self.peer_connections:
                self.stop_audio_player()
            logger.info("已關閉與 %s 的 WebRTC 連線", controller_id)

    async def send_screenshot(self, dashboard_id):
        """擷取當前畫面並透過 WebSocket 傳送給儀表板"""
        if not dashboard_id:
            return
        try:
            # 使用與 ScreenShareTrack 相同的邏輯來擷取畫面
            track = ScreenShareTrack(window_title=CONFIG.get("window_title"), region=CONFIG.get("region"))
            frame = await track.recv()
            img = frame.to_image()

            # 將圖片轉換為 JPEG 格式並進行 Base64 編碼
            buffered = BytesIO()
            img.save(buffered, format="JPEG", quality=75) # 使用較低的品質以減少大小
            img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')

            response = {"type": "screenshot_response", "from_id": self.sharer_id, "target_id": dashboard_id, "image_data": img_str}
            await self.websocket.send(json.dumps(response))
            logger.info(f"已傳送截圖給儀表板 {dashboard_id}")
        except Exception as e:
            logger.error(f"傳送截圖時發生錯誤: {e}")

def find_chrome_executable():
    """在常見的路徑中尋找 Chrome 或 Edge 執行檔。"""
    candidates = []
    if sys.platform == "win32":
        program_files = os.environ.get("ProgramFiles", "C:\\Program Files")
        program_files_x86 = os.environ.get("ProgramFiles(x86)", "C:\\Program Files (x86)")
        
        candidates.extend([
            Path(program_files) / "Google/Chrome/Application/chrome.exe",
            Path(program_files_x86) / "Google/Chrome/Application/chrome.exe",
            Path(program_files) / "Microsoft/Edge/Application/msedge.exe",
            Path(program_files_x86) / "Microsoft/Edge/Application/msedge.exe",
        ])
    elif sys.platform == "darwin":
        candidates.append(Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"))
        candidates.append(Path("/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"))
    else:
        candidates.extend([
            Path("/usr/bin/google-chrome"),
            Path("/usr/bin/microsoft-edge"),
            Path("/usr/bin/chromium-browser"),
        ])

    for path in candidates:
        if path.exists():
            logger.info(f"找到瀏覽器執行檔: {path}")
            return str(path)

    logger.error("找不到任何相容的瀏覽器 (Chrome, Edge, Chromium)。")
    messagebox.showerror(
        "找不到瀏覽器",
        "無法自動找到 Chrome 或 Edge 瀏覽器。\n\n"
        "音訊播放功能將無法使用。"
    )
    return None

    def handle_file_upload(self, command):
        filename = command.get("filename")
        content_b64 = command.get("content")
        save_path = Path.home() / "Downloads" / filename
        save_path.write_bytes(base64.b64decode(content_b64))
        logger.info(f"檔案 '{filename}' 已儲存到下載資料夾。")

def load_config():
    """載入設定檔"""
    global CONFIG
    try:
        config_path = Path("config.json")
        if not config_path.exists():
            logger.warning("config.json 未找到，將建立一個預設的設定檔。")
            default_config = {
                "connection": {
                    "secure": False,
                    "host": "www.winway.tw",
                    "signaling_port": 6759,
                    "turn_port_secure": 5349,
                    "turn_port_insecure": 3478
                },
                "turn_credentials": {
                    "username": "winway",
                    "credential": "WinwayE=mc^26758"
                },
                "sharer_info": {
                    "id": f"sharer-{platform.node()}",
                    "password": "6758hs"
                },
                "region": None,
                "window_title": None,
                "audio_output_device": None
            }
            with open(config_path, 'w', encoding='utf-8') as f:
                json.dump(default_config, f, indent=4, ensure_ascii=False)
            logger.info("已建立 config.json，請根據您的伺服器設定修改它。")
            CONFIG = default_config
            return

        with open(config_path, 'r', encoding='utf-8') as f:
            CONFIG = json.load(f)
    except (json.JSONDecodeError) as e:
        logger.error(f"錯誤：config.json 格式不正確或已損毀。 {e}")
        # 在 GUI 模式下，彈出錯誤訊息框比直接退出更好
        if app_instance and app_instance.root:
             # 需要在主執行緒中顯示 messagebox
             app_instance.root.after(0, lambda: messagebox.showerror("設定檔錯誤", f"config.json 格式錯誤，請修正或刪除它以重新生成。\n\n錯誤: {e}"))
        # 賦予一個空的預設值，讓程式可以繼續執行但功能可能受限
        CONFIG = {
            "connection": {}, "turn_credentials": {}, "sharer_info": {},
            "region": None, "window_title": None, "audio_output_device": None, "audio_volume": 100
        }

def save_config():
    """儲存設定檔"""
    with open("config.json", 'w', encoding='utf-8') as f:
        json.dump(CONFIG, f, indent=4, ensure_ascii=False)
        
def build_connection_configs():
    """根據設定檔動態建立連線 URL 和 ICE 伺服器列表"""
    conn_config = CONFIG.get("connection", {})
    is_secure = conn_config.get("secure", False)
    host = conn_config.get("host", "localhost")

    # 建立信令伺服器 URL
    ws_protocol = "wss" if is_secure else "ws"
    signaling_port = conn_config.get("signaling_port", 6759)
    signaling_url = f"{ws_protocol}://{host}:{signaling_port}"

    # 建立 ICE 伺服器列表
    stun_protocol = "stuns" if is_secure else "stun"
    turn_protocol = "turns" if is_secure else "turn"
    turn_port = conn_config.get("turn_port_secure", 5349) if is_secure else conn_config.get("turn_port_insecure", 3478)
    
    turn_creds = CONFIG.get("turn_credentials", {})

    ice_servers = [
        # *** 關鍵修正：提供標準的 STUN 和 TURN 設定 ***
        # 1. STUN 伺服器，用於探索公網 IP
        {"urls": f"{stun_protocol}:{host}:{turn_port}"},
        # 2. TURN 伺服器 (使用 UDP)，這是首選的中繼方式
        {
            "urls": f"{turn_protocol}:{host}:{turn_port}?transport=udp",
            "username": turn_creds.get("username"),
            "credential": turn_creds.get("credential")
        },
        # 3. TURN 伺服器 (使用 TCP)，作為 UDP 被封鎖時的備案
        {
            "urls": f"{turn_protocol}:{host}:{turn_port}?transport=tcp",
            "username": turn_creds.get("username"),
            "credential": turn_creds.get("credential")
        }
    ]
    
    return signaling_url, ice_servers

# 將 App 實例設為全域變數，以便從 AudioPlayerTrack 訪問
app_instance = None

class App:
    def __init__(self, root, loop, log_queue):
        self.root = root
        self.loop = loop
        self.root.title("WebRTC 分享端 v1.4")
        
        # --- 修正：讓視窗在螢幕中央顯示 ---
        self.root.geometry("800x480")
        self.root.update_idletasks() # 確保視窗尺寸已計算
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()
        window_width = self.root.winfo_width()
        window_height = self.root.winfo_height()
        self.root.geometry(f"+{(screen_width // 2) - (window_width // 2)}+{(screen_height // 2) - (window_height // 2)}")

        # --- 美化：定義顏色和字體 ---
        self.BG_COLOR = "#1a2a1a"  # 深綠色背景
        self.FG_COLOR = "#a0d0a0"  # 亮綠色文字
        self.ACCENT_COLOR = "#00ff00" # 螢光綠
        self.BUTTON_BG = "#2a4a2a" # 較深的按鈕背景
        self.ENTRY_BG = "#102010"  # 更深的輸入框背景
        self.LOG_BG = "#102010"
        self.ERROR_FG = "#e06c75"
        self.WARNING_FG = "#d19a66"
        self.DEBUG_FG = "#56b6c2"
        self.FONT_NORMAL = ("Arial", 10)
        self.FONT_TITLE = ("Arial", 12, "bold")
        self.FONT_LOG = ("Consolas", 9)

        # --- 美化：設定無邊框半透明視窗 ---
        self.root.overrideredirect(True) # 移除原生視窗邊框
        self.root.attributes("-alpha", 0.9)
        self.root.config(bg=self.BG_COLOR, bd=2, relief=tk.RAISED) # 加個邊框增加立體感

        # --- 美化：自訂標題列 ---
        title_bar = tk.Frame(root, bg=self.BUTTON_BG, relief="raised", bd=0, height=30)
        title_bar.pack(fill=tk.X)
        title_bar.bind("<ButtonPress-1>", self.on_drag_start)
        title_bar.bind("<B1-Motion>", self.on_drag_motion)

        title_label = tk.Label(title_bar, text="WebRTC 分享端 v1.4", bg=self.BUTTON_BG, fg=self.FG_COLOR, font=self.FONT_NORMAL)
        title_label.pack(side=tk.LEFT, padx=10)
        title_label.bind("<ButtonPress-1>", self.on_drag_start)
        title_label.bind("<B1-Motion>", self.on_drag_motion)

        # --- 美化：自訂視窗按鈕 ---
        close_button = tk.Button(title_bar, text="✕", bg=self.BUTTON_BG, fg=self.FG_COLOR, activebackground="#E81123", activeforeground="white", command=self.on_closing, relief=tk.FLAT, bd=0, width=4)
        close_button.pack(side=tk.RIGHT)
        close_button.bind("<Enter>", lambda e: e.widget.config(bg="#E81123"))
        close_button.bind("<Leave>", lambda e: e.widget.config(bg=self.BUTTON_BG))

        minimize_button = tk.Button(title_bar, text="—", bg=self.BUTTON_BG, fg=self.FG_COLOR, activebackground=self.ACCENT_COLOR, activeforeground=self.BG_COLOR, command=self.minimize_window, relief=tk.FLAT, bd=0, width=4)
        minimize_button.pack(side=tk.RIGHT)
        minimize_button.bind("<Enter>", lambda e: e.widget.config(bg=self.ENTRY_BG))
        minimize_button.bind("<Leave>", lambda e: e.widget.config(bg=self.BUTTON_BG))
        
        # --- 新增：日誌顯示/隱藏按鈕 ---
        self.log_button = tk.Button(title_bar, text="日誌", bg=self.BUTTON_BG, fg=self.FG_COLOR, activebackground=self.ACCENT_COLOR, activeforeground=self.BG_COLOR, command=self.toggle_log_pane, relief=tk.FLAT, bd=0, width=4)
        self.log_button.pack(side=tk.RIGHT)
        self.log_visible = False # 預設隱藏

        style = ttk.Style()
        style.theme_use('clam')
        style.configure("TProgressbar", foreground=self.ACCENT_COLOR, background=self.ACCENT_COLOR, troughcolor=self.ENTRY_BG, bordercolor=self.BG_COLOR, lightcolor=self.BG_COLOR, darkcolor=self.BG_COLOR)

        self.client = None
        self.client_task = None

        # 儲存當前分享模式和區域
        self.current_sharing_mode = None
        self.current_capture_region = None

        # 用於從背景執行緒安全地更新 GUI 的佇列
        self.gui_queue = Queue()
        self.log_queue = log_queue

        # --- 主佈局 ---
        self.paned_window = tk.PanedWindow(root, orient=tk.HORIZONTAL, sashrelief=tk.FLAT, bg=self.FG_COLOR, sashwidth=4, bd=0)
        self.paned_window.pack(fill=tk.BOTH, expand=True)

        # --- GUI 元素 ---
        control_frame = tk.Frame(self.paned_window, padx=10, pady=5, bg=self.BG_COLOR)
        self.paned_window.add(control_frame, width=380)

        # --- 新增：ID 和密碼輸入框 ---
        id_frame = tk.Frame(control_frame, bg=self.BG_COLOR)
        id_frame.pack(fill=tk.X, pady=2)
        tk.Label(id_frame, text="分享端 ID:", width=10, bg=self.BG_COLOR, fg=self.FG_COLOR, font=self.FONT_NORMAL).pack(side=tk.LEFT)
        self.id_entry = tk.Entry(id_frame, bg=self.ENTRY_BG, fg=self.FG_COLOR, insertbackground=self.FG_COLOR, relief=tk.FLAT)
        self.id_entry.pack(fill=tk.X, expand=True)

        pw_frame = tk.Frame(control_frame, bg=self.BG_COLOR)
        pw_frame.pack(fill=tk.X, pady=2)
        tk.Label(pw_frame, text="密碼:", width=10, bg=self.BG_COLOR, fg=self.FG_COLOR, font=self.FONT_NORMAL).pack(side=tk.LEFT)
        self.pw_entry = tk.Entry(pw_frame, show="*", bg=self.ENTRY_BG, fg=self.FG_COLOR, insertbackground=self.FG_COLOR, relief=tk.FLAT)
        self.pw_entry.pack(fill=tk.X, expand=True)

        # --- 新增：音訊輸出設備選擇 ---
        audio_device_frame = tk.Frame(control_frame, bg=self.BG_COLOR)
        audio_device_frame.pack(fill=tk.X, pady=2)
        tk.Label(audio_device_frame, text="音訊輸出:", width=10, bg=self.BG_COLOR, fg=self.FG_COLOR, font=self.FONT_NORMAL).pack(side=tk.LEFT)
        self.audio_device_combo = ttk.Combobox(audio_device_frame, state="readonly")
        self.audio_device_combo.pack(fill=tk.X, expand=True)

        # 填充設備列表
        self.audio_devices = get_audio_output_devices()
        if self.audio_devices:
            self.audio_device_combo['values'] = self.audio_devices
            # 從設定檔載入上次選擇的設備
            selected_device = CONFIG.get("audio_output_device")
            if selected_device and selected_device in self.audio_devices:
                self.audio_device_combo.set(selected_device)
            elif self.audio_devices:
                self.audio_device_combo.current(0)

        # --- 新增：音量控制滑桿 ---
        volume_frame = tk.Frame(control_frame, bg=self.BG_COLOR)
        volume_frame.pack(fill=tk.X, pady=2)
        tk.Label(volume_frame, text="播放音量:", width=10, bg=self.BG_COLOR, fg=self.FG_COLOR, font=self.FONT_NORMAL).pack(side=tk.LEFT)
        self.volume_scale = tk.Scale(volume_frame, from_=0, to=100, orient=tk.HORIZONTAL,
                                     bg=self.BG_COLOR, fg=self.FG_COLOR, troughcolor=self.ENTRY_BG,
                                     highlightthickness=0, command=self.on_volume_change)
        self.volume_scale.set(CONFIG.get("audio_volume", 100))
        self.volume_scale.pack(fill=tk.X, expand=True)

        # 從設定檔載入初始值
        sharer_info = CONFIG.get("sharer_info", {})
        self.id_entry.insert(0, sharer_info.get("id", f"sharer-{platform.node()}"))
        self.pw_entry.insert(0, sharer_info.get("password", "default_password"))

        # 分隔線
        ttk.Separator(control_frame, orient='horizontal').pack(fill='x', pady=10)

        tk.Label(control_frame, text="分享模式:", font=self.FONT_TITLE, bg=self.BG_COLOR, fg=self.FG_COLOR).pack(anchor="w")

        self.screen_button = tk.Button(control_frame, text="開始分享", command=lambda: self.start_sharing_flow('screen'), bg=self.BUTTON_BG, fg=self.FG_COLOR, relief=tk.FLAT, activebackground=self.ACCENT_COLOR, activeforeground=self.BG_COLOR)
        self.screen_button.pack(fill=tk.X, pady=5)

        self.stop_button = tk.Button(control_frame, text="停止分享", command=self.stop_sharing, state=tk.DISABLED, bg=self.BUTTON_BG, fg=self.FG_COLOR, relief=tk.FLAT, activebackground=self.ERROR_FG, activeforeground="white")
        self.stop_button.pack(fill=tk.X, pady=5)

        # 音波視覺化 Canvas
        self.wave_canvas = tk.Canvas(control_frame, height=40, bg=self.ENTRY_BG, highlightthickness=0)
        self.wave_canvas.pack(fill=tk.X, pady=10)
        self.wave_line = self.wave_canvas.create_line(0, 0, 0, 0, fill=self.ACCENT_COLOR, width=2)

        # --- 新增：右側日誌區塊 ---
        self.log_frame = tk.Frame(self.paned_window, padx=5, pady=5, bg=self.BG_COLOR)
        # 預設不加入到 paned_window

        # --- 新增：日誌篩選器 ---
        filter_frame = tk.Frame(self.log_frame, bg=self.BG_COLOR)
        filter_frame.pack(fill=tk.X, pady=(0, 5))
        tk.Label(filter_frame, text="日誌等級:", bg=self.BG_COLOR, fg=self.FG_COLOR, font=self.FONT_NORMAL).pack(side=tk.LEFT, padx=(0, 5))
        
        self.log_levels = {
            "DEBUG": tk.BooleanVar(value=True),
            "INFO": tk.BooleanVar(value=True),
            "WARNING": tk.BooleanVar(value=True),
            "ERROR": tk.BooleanVar(value=True),
            "CRITICAL": tk.BooleanVar(value=True),
        }
        for level, var in self.log_levels.items():
            cb = tk.Checkbutton(filter_frame, text=level, variable=var, bg=self.BG_COLOR, fg=self.FG_COLOR, selectcolor=self.ENTRY_BG, activebackground=self.BG_COLOR, activeforeground=self.FG_COLOR, font=self.FONT_NORMAL)
            cb.pack(side=tk.LEFT)

        log_text_frame = tk.Frame(self.log_frame)
        log_text_frame.pack(fill=tk.BOTH, expand=True)

        self.log_text = tk.Text(log_text_frame, height=15, state='disabled', bg=self.LOG_BG, fg=self.FG_COLOR, font=self.FONT_LOG, relief=tk.FLAT, insertbackground=self.FG_COLOR)
        self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        # 設定不同日誌等級的顏色標籤
        self.log_text.tag_config("INFO", foreground=self.FG_COLOR)
        self.log_text.tag_config("DEBUG", foreground=self.DEBUG_FG)
        self.log_text.tag_config("WARNING", foreground=self.WARNING_FG)
        self.log_text.tag_config("ERROR", foreground=self.ERROR_FG)
        self.log_text.tag_config("CRITICAL", background=self.ERROR_FG, foreground="#ffffff")

        log_scrollbar = tk.Scrollbar(log_text_frame, command=self.log_text.yview, bg=self.BG_COLOR, troughcolor=self.ENTRY_BG, activebackground=self.ACCENT_COLOR, relief=tk.FLAT)
        log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.log_text.config(yscrollcommand=log_scrollbar.set)

        # 狀態列
        self.status_label = tk.Label(root, text="狀態: 未連線", bd=1, relief=tk.SUNKEN, anchor="w", bg=self.BUTTON_BG, fg=self.FG_COLOR)
        self.status_label.pack(side=tk.BOTTOM, fill=tk.X)

        # self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # 已被自訂按鈕取代

        # 區域選擇器實例
        self.region_selector = None
        
        # 啟動 GUI 更新迴圈
        self.process_gui_queue()
        self.process_log_queue()
        
        # --- 新增：程式啟動後 1 秒自動開始連線 ---
        self.root.after(1000, lambda: self.start_sharing_flow('screen'))

    def on_volume_change(self, value):
        """當音量滑桿被拖動時呼叫"""
        # 因為 subprocess 模式無法直接控制音量，此函式暫時停用
        # 將音量儲存到設定中，以便下次啟動時記住
        CONFIG["audio_volume"] = int(value)

    def toggle_log_pane(self):
        self.log_visible = not self.log_visible
        if self.log_visible:
            self.paned_window.add(self.log_frame)
            self.log_button.config(relief=tk.SUNKEN)
        else:
            self.paned_window.remove(self.log_frame)
            self.log_button.config(relief=tk.RAISED)

    def minimize_window(self):
        self.root.iconify()

    def process_gui_queue(self):
        """處理 GUI 更新佇列中的任務"""
        try:
            func, args = self.gui_queue.get_nowait()
            func(*args)
        except Empty:
            pass
        self.root.after(100, self.process_gui_queue)

    def process_log_queue(self):
        """處理日誌佇列中的訊息"""
        while not self.log_queue.empty():
            try:
                record: logging.LogRecord = self.log_queue.get_nowait()
                
                # 根據勾選的等級進行篩選
                if self.log_levels.get(record.levelname, tk.BooleanVar(value=False)).get():
                    self.log_text.config(state='normal')
                    # 插入訊息並套用對應的顏色標籤
                    self.log_text.insert('end', self.format_log_record(record) + '\n', record.levelname)
                    self.log_text.config(state='disabled')
                    self.log_text.see('end') # 捲動到最底部
            except Empty:
                break
        self.root.after(100, self.process_log_queue)

    def format_log_record(self, record: logging.LogRecord) -> str:
        """格式化 LogRecord 物件為字串"""
        return f"{record.getMessage()}"

    def on_drag_start(self, event):
        self.x = event.x
        self.y = event.y

    def on_drag_motion(self, event):
        x = self.root.winfo_x() - self.x + event.x
        y = self.root.winfo_y() - self.y + event.y
        self.root.geometry(f"+{x}+{y}")

    def show_marquee(self, text, repeat):
        """在主執行緒中建立並顯示一個靜態的、置中的提示框，並有淡入淡出效果"""
        marquee_window = tk.Toplevel(self.root)
        marquee_window.overrideredirect(True)  # 無邊框
        marquee_window.attributes("-topmost", True)  # 置頂
        marquee_window.attributes("-alpha", 0.0)     # 初始完全透明
        marquee_window.config(bg="#1a2a1a", padx=40, pady=20) # 深綠色背景和內邊距

        font_size = 48
        marquee_label = tk.Label(marquee_window, text=text, font=("Arial", font_size, "bold"), fg="#00ff00", bg="#1a2a1a")
        marquee_label.pack()

        # 計算視窗在螢幕中央的位置
        marquee_window.update_idletasks()
        width = marquee_window.winfo_width()
        height = marquee_window.winfo_height()
        x = (self.root.winfo_screenwidth() // 2) - (width // 2)
        y = (self.root.winfo_screenheight() // 2) - (height // 2)
        marquee_window.geometry(f'{width}x{height}+{x}+{y}')

        # --- 關鍵修正：使用非阻塞的 after() 實現動畫 ---
        def fade_in(alpha=0.0):
            if alpha < 0.8:
                alpha += 0.1
                marquee_window.attributes("-alpha", alpha)
                self.root.after(50, fade_in, alpha)
            else:
                # 淡入完成後，等待指定時間再開始淡出
                self.root.after(repeat * 2000, fade_out)

        def fade_out(alpha=0.8):
            if alpha > 0.0:
                alpha -= 0.1
                marquee_window.attributes("-alpha", alpha)
                self.root.after(50, fade_out, alpha)
            else:
                # 淡出完成後銷毀視窗
                marquee_window.destroy()

        # 啟動淡入動畫
        fade_in()

    def update_status(self, message):
        self.root.after(0, lambda: self.status_label.config(text=f"狀態: {message}"))

    def draw_waveform(self, volume_percentage):
        """在 Canvas 上繪製音波視覺化"""
        canvas_width = self.wave_canvas.winfo_width()
        canvas_height = self.wave_canvas.winfo_height()
        
        # 清除舊的線條
        self.wave_canvas.delete(self.wave_line)

        # 將音量轉換為高度 (0-100% -> 0-canvas_height)
        line_height = (volume_percentage / 100) * canvas_height
        
        # 繪製一個簡單的垂直線條，代表音量
        # 這裡可以設計更複雜的音波效果
        self.wave_line = self.wave_canvas.create_line(
            canvas_width / 2, canvas_height, 
            canvas_width / 2, canvas_height - line_height, 
            fill=self.ACCENT_COLOR, width=5
        )
        # 為了模擬波動，可以讓線條左右擺動或使用多條線
        # 這裡只是一個簡單的單條線示意

    def start_sharing_flow(self, mode):
        """啟動分享流程，處理模式選擇"""
        self.current_sharing_mode = mode
        self.update_status(f"正在啟動分享...")
        self.screen_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)

        self.run_client(mode='screen')

    def run_client(self, mode, value=None):
        # 重新載入設定，以防萬一
        load_config()
        
        # --- 嚴格使用 config.json 中的設定 ---
        sharer_info = CONFIG.get("sharer_info", {})
        sharer_id = sharer_info.get("id")
        password = sharer_info.get("password")

        # 如果設定檔中缺少 ID 或密碼，則不連線
        if not sharer_id or not password:
            logger.error("config.json 中缺少 'sharer_info' 的 'id' 或 'password'。")
            messagebox.showerror("設定錯誤", "config.json 中缺少分享端 ID 或密碼，無法連線。")
            self.reset_gui()
            return
        
        # 將設定檔中的值同步到 GUI
        self.id_entry.delete(0, tk.END)
        self.id_entry.insert(0, sharer_id)
        self.pw_entry.delete(0, tk.END)
        self.pw_entry.insert(0, password)
        
        # 根據模式設定 CONFIG
        CONFIG["window_title"] = None
        CONFIG["region"] = None
        CONFIG["audio_output_device"] = self.audio_device_combo.get()
        CONFIG["audio_volume"] = self.volume_scale.get()

        signaling_server_url, ice_servers_config = build_connection_configs()

        self.client = WebRTCClient(
            sharer_id=sharer_id,
            password=password,
            server_url=signaling_server_url,
            ice_servers=ice_servers_config
        )
        self.client.loop = self.loop # 傳遞事件迴圈
        self.client_task = asyncio.run_coroutine_threadsafe(self.client.run(), self.loop)
        self.client_task.add_done_callback(self.on_client_done)

    def schedule_reconnect(self):
        """安排一個自動重連任務"""
        logger.info("將在 10 秒後嘗試自動重連...")
        self.update_status("連線中斷，10秒後重試...")
        self.root.after(10000, lambda: self.start_sharing_flow('screen'))

    def stop_sharing(self):
        if self.client_task and not self.client_task.done():
            # 這會觸發 client.run() 中的 finally 區塊
            # 確保在事件迴圈中安全地取消任務
            self.loop.call_soon_threadsafe(self.client_task.cancel) 

        self.reset_gui()

    def on_client_done(self, task):
        try:
            task.result()
        except asyncio.CancelledError:
            logger.info("客戶端任務已被取消。")
            # 手動取消，不需要重連
        except Exception as e:
            logger.error("客戶端任務因錯誤而終止: %s", e)
            self.update_status(f"錯誤: {e}") # 在 GUI 上顯示錯誤
            # --- 新增：發生錯誤時觸發自動重連 ---
            self.schedule_reconnect()
        self.root.after(0, self.reset_gui)

    def restart_app(self):
        """重啟分享服務，但不關閉主程式"""
        logger.info("正在執行服務重啟程序...")
        # 步驟 B & A: 停止當前分享，這會關閉 audio_player.html
        self.stop_sharing()
        # 步驟 C: 短暫延遲後，自動開始新的分享
        self.root.after(1000, lambda: self.start_sharing_flow('screen'))

    def reset_gui(self):
        self.screen_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.update_status("未連線")
        self.wave_canvas.delete("all") # 清除音波

    def on_closing(self):
        self.stop_sharing()
        # 確保 loop 存在且正在運行
        if self.loop and self.loop.is_running():
            self.loop.call_soon_threadsafe(self.loop.stop)
        self.root.destroy()

if __name__ == "__main__":
    # 1. 建立一個新的 asyncio 事件迴圈
    asyncio_loop = asyncio.new_event_loop()
    
    log_queue = Queue()
    queue_handler = QueueHandler(log_queue)
    logger.addHandler(queue_handler)

    # 2. 在一個新的背景執行緒中運行這個事件迴圈
    thread = threading.Thread(target=asyncio_loop.run_forever, daemon=True, name="AsyncioLoopThread")
    thread.start()

    # 3. 在主執行緒中建立並運行 Tkinter GUI
    root = tk.Tk()
    app_instance = App(root, asyncio_loop, log_queue)

    if not PYAUDIO_AVAILABLE:
        root.after(150, lambda: messagebox.showwarning(
            "缺少 PyAudio",
            "找不到 PyAudio 函式庫。\n請執行 'pip install pyaudio'。\n音訊設備偵測與播放功能將無法使用。"
        ))

    root.mainloop()

    # 4. 當 mainloop 結束後 (使用者關閉視窗)，等待背景執行緒結束
    thread.join(timeout=1)