From 4d09ed914a9e82c07a03e5171e7010bbc45c06d5 Mon Sep 17 00:00:00 2001 From: jasonchenwork Date: Fri, 20 Jun 2025 13:12:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=9C=8B=E9=96=80=E7=8B=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DualScreenDemo.Shared.cs | 10 ++ PrimaryFormParts/PrimaryForm.cs | 46 ++++++- VideoPlayerForm.cs | 221 +++++++++++++++++++++----------- WatchDog.cs | 84 ++++++++++++ 4 files changed, 278 insertions(+), 83 deletions(-) create mode 100644 DualScreenDemo.Shared.cs create mode 100644 WatchDog.cs diff --git a/DualScreenDemo.Shared.cs b/DualScreenDemo.Shared.cs new file mode 100644 index 0000000..5c3810d --- /dev/null +++ b/DualScreenDemo.Shared.cs @@ -0,0 +1,10 @@ +namespace DualScreenDemo.Shared +{ + public class VideoStatus + { + public bool IsGraphOk { get; set; } + public string LastError { get; set; } + public double PositionSeconds { get; set; } + public string PlayState { get; set; } + } +} diff --git a/PrimaryFormParts/PrimaryForm.cs b/PrimaryFormParts/PrimaryForm.cs index 29a3169..4e0a6e2 100644 --- a/PrimaryFormParts/PrimaryForm.cs +++ b/PrimaryFormParts/PrimaryForm.cs @@ -242,6 +242,23 @@ namespace DualScreenDemo LoadConnectionStringFromFile("test.env"); } + public bool IsAppResponsive() + { + try + { + var form = this; + if (form != null) + { + bool dummy = form.InvokeRequired; // 如果 Invoke 卡死,會丟錯 + return true; + } + } + catch + { + return false; + } + return true; + } // 添加 DPI 感知支持 [DllImport("user32.dll")] @@ -940,12 +957,12 @@ namespace DualScreenDemo } catch (Exception ex) { - MessageBox.Show("Failed to send command: " + ex.Message); + Console.WriteLine("Failed to send command: " + ex.Message); } } else { - MessageBox.Show("Serial port is not open."); + Console.WriteLine("Serial port is not open."); } } @@ -1560,7 +1577,7 @@ namespace DualScreenDemo return; } - // 2. 比較本地和服務器文件夾 + // 2. 確認本地文件夾是否存在(不存在則創立) if (!Directory.Exists(localVideoPath)) { Directory.CreateDirectory(localVideoPath); @@ -1575,7 +1592,7 @@ namespace DualScreenDemo .Select(f => new FileInfo(f)) .ToDictionary(f => f.Name, f => f); - // 3. 檢查並更新文件 + // 3-1. 檢查並更新文件 foreach (var serverFile in serverFiles) { bool needsCopy = false; @@ -1607,7 +1624,22 @@ namespace DualScreenDemo } } } - + // 3-2. 清除本地有但伺服器已經沒有的檔案 + foreach (var localFile in localFiles) + { + if (!serverFiles.ContainsKey(localFile.Key)) + { + try + { + File.Delete(localFile.Value.FullName); + Console.WriteLine($"刪除本地多餘影片: {localFile.Key}"); + } + catch (Exception ex) + { + Console.WriteLine($"刪除影片失敗 {localFile.Key}: {ex.Message}"); + } + } + } // 4. 載入更新後的本地文件 LoadLocalVideoFiles(); } @@ -2061,14 +2093,14 @@ namespace DualScreenDemo videoPlayerForm.ReplayCurrentSong(); } - private void PauseButton_Click(object sender, EventArgs e) + public void PauseButton_Click(object sender, EventArgs e) { videoPlayerForm.Pause(); pauseButton.Visible = false; playButton.Visible = true; } - private void PlayButton_Click(object sender, EventArgs e) + public void PlayButton_Click(object sender, EventArgs e) { videoPlayerForm.Play(); playButton.Visible = false; diff --git a/VideoPlayerForm.cs b/VideoPlayerForm.cs index 3bd3575..9d3f713 100644 --- a/VideoPlayerForm.cs +++ b/VideoPlayerForm.cs @@ -3,6 +3,8 @@ using System.Runtime.InteropServices; using DirectShowLib; using DBObj; using OverlayFormObj; +using DualScreenDemo.Shared; + namespace DualScreenDemo { public class VideoPlayerForm : Form @@ -19,12 +21,12 @@ namespace DualScreenDemo return cp; } } - #endregion - + #endregion + // 单例实例 public static VideoPlayerForm Instance { get; private set; } - + // 导入user32.dll API [DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); @@ -150,7 +152,7 @@ namespace DualScreenDemo { if (secondMonitor != null) { - SetWindowPos(this.Handle, IntPtr.Zero, secondMonitor.Bounds.X, secondMonitor.Bounds.Y, + SetWindowPos(this.Handle, IntPtr.Zero, secondMonitor.Bounds.X, secondMonitor.Bounds.Y, secondMonitor.Bounds.Width, secondMonitor.Bounds.Height, 0); } IntPtr exStyle = GetWindowLong(this.Handle, GWL_EXSTYLE); @@ -394,7 +396,7 @@ namespace DualScreenDemo } catch (Exception ex) { - Console.WriteLine($"Exception in AddFilterByClsid: {ex.Message}"); + Console.WriteLine($"Exception in AddFilterByClsid: {ex.Message}"); throw; // Rethrow the exception to handle it further up the call stack } } @@ -454,12 +456,14 @@ namespace DualScreenDemo // 計算縮放後的尺寸(但起點仍是 0,0) int newWidth = (int)(designWidth * scale); int newHeight = (int)(designHeight * scale); - + // trash location with trash flexible of screen size. - if (actualWidth == 1024){ + if (actualWidth == 1024) + { newWidth = (int)(newWidth * 0.84f); } - else if (actualWidth == 1440){ + else if (actualWidth == 1440) + { newWidth = (int)(newWidth * 0.9f); } videoWindowPrimary = (IVideoWindow)videoRendererPrimary; @@ -479,7 +483,7 @@ namespace DualScreenDemo MessageBox.Show(String.Format("Error syncing to primary monitor: {0}", ex.Message)); } } - public void ClosePrimaryScreenPanel() + public void ClosePrimaryScreenPanel() { try { @@ -547,7 +551,7 @@ namespace DualScreenDemo // [2] 停止當前播放,釋放資源(例如關閉影片播放器、清除緩衝) StopAndReleaseResources(); - + // [3] 將新的播放清單指派給 `playingSongList` playingSongList = songList; @@ -561,7 +565,7 @@ namespace DualScreenDemo // [6] 若使用者點播清單有歌,就開始播放 if (isUserPlaylistPlaying) - { + { // [6.1] 設定當前歌曲索引為 -1(意味著即將播放第一首,從 `PlayNextSong` 開始) currentSongIndex = -1; // [6.2] 播放下一首歌(實際會遞增 index 為 0,並播放該首歌) @@ -577,14 +581,14 @@ namespace DualScreenDemo public async Task PlayPublicPlaylist() { Console.WriteLine("開始播放公播清單..."); - + // 在切换到公播之前,确保最后一首用户歌曲状态正确 - if (PrimaryForm.currentSongIndexInHistory >= 0 && + if (PrimaryForm.currentSongIndexInHistory >= 0 && PrimaryForm.currentSongIndexInHistory < PrimaryForm.playStates.Count) { PrimaryForm.playStates[PrimaryForm.currentSongIndexInHistory] = PlayState.Played; Console.WriteLine($"切換到公播前更新最後一首歌曲狀態為已播放,索引:{PrimaryForm.currentSongIndexInHistory}"); - + // 强制刷新显示 if (PrimaryForm.Instance.multiPagePanel != null) { @@ -594,12 +598,12 @@ namespace DualScreenDemo ); } } - + isUserPlaylistPlaying = false; IsPlayingPublicSong = true; // 设置为正在播放公播 currentSongIndex = -1; - try + try { // 重新整理公播清單 publicPlaylist = new List(); @@ -609,7 +613,7 @@ namespace DualScreenDemo if (File.Exists(welcomePath)) { publicPlaylist.Add(new SongData( - "0", "歡迎光臨", "", "","", welcomePath, + "0", "歡迎光臨", "", "", "", welcomePath, "", "", "", "", 1 )); } @@ -621,8 +625,8 @@ namespace DualScreenDemo if (File.Exists(bgmPath)) { publicPlaylist.Add(new SongData( - i.ToString(), $"背景音樂{i:D2}", - "", "", "",bgmPath, "", "", "", "", 1 + i.ToString(), $"背景音樂{i:D2}", + "", "", "", bgmPath, "", "", "", "", 1 )); } } @@ -678,7 +682,8 @@ namespace DualScreenDemo if (overlayForm.InvokeRequired) { - overlayForm.Invoke(new MethodInvoker(() => { + overlayForm.Invoke(new MethodInvoker(() => + { overlayForm.UpdateMarqueeText(nextSongText, OverlayForm.MarqueeStartPosition.Middle, Color.White); })); } @@ -691,7 +696,8 @@ namespace DualScreenDemo // 重置跑马灯文本 if (overlayForm.InvokeRequired) { - overlayForm.Invoke(new MethodInvoker(() => { + overlayForm.Invoke(new MethodInvoker(() => + { overlayForm.ResetMarqueeTextToWelcomeMessage(); })); } @@ -735,7 +741,7 @@ namespace DualScreenDemo // 根據目前播放模式(點歌 or 公播)決定要播放的清單 List currentPlaylist = isUserPlaylistPlaying ? playingSongList : publicPlaylist; - + // 若播放清單是空的,直接返回(不執行播放) if (!currentPlaylist.Any()) return; @@ -760,30 +766,30 @@ namespace DualScreenDemo } - // 可以取得 燈控/聲控 的位置 - var songToPlay = currentPlaylist[currentSongIndex]; - - // pathToPlay 需要調整 - var pathToPlay = File.Exists(songToPlay.SongFilePathHost1) ? songToPlay.SongFilePathHost1 : songToPlay.SongFilePathHost2; + // 可以取得 燈控/聲控 的位置 + var songToPlay = currentPlaylist[currentSongIndex]; - // 若兩個 host 上都找不到檔案就直接結束 - if (!File.Exists(pathToPlay)) - { - Console.WriteLine($"文件不存在:{pathToPlay}"); - return; - } + // pathToPlay 需要調整 + var pathToPlay = File.Exists(songToPlay.SongFilePathHost1) ? songToPlay.SongFilePathHost1 : songToPlay.SongFilePathHost2; - // 更新目前正在播放的歌曲 - currentPlayingSong = songToPlay; + // 若兩個 host 上都找不到檔案就直接結束 + if (!File.Exists(pathToPlay)) + { + Console.WriteLine($"文件不存在:{pathToPlay}"); + return; + } - // 更新畫面上顯示的下一首歌資訊 - UpdateNextSongFromPlaylist(); + // 更新目前正在播放的歌曲 + currentPlayingSong = songToPlay; - // 顯示 QRCode(可能是點歌頁用) - overlayForm.DisplayQRCodeOnOverlay(HttpServer.randomFolderPath); + // 更新畫面上顯示的下一首歌資訊 + UpdateNextSongFromPlaylist(); - // 隱藏「暫停中」標籤 - overlayForm.HidePauseLabel(); + // 顯示 QRCode(可能是點歌頁用) + overlayForm.DisplayQRCodeOnOverlay(HttpServer.randomFolderPath); + + // 隱藏「暫停中」標籤 + overlayForm.HidePauseLabel(); try @@ -810,7 +816,7 @@ namespace DualScreenDemo await Task.Delay(1000); StopAndReleaseResources(); await Task.Delay(1000); - + // 重新初始化 COM int hr = CoInitializeEx(IntPtr.Zero, COINIT.APARTMENTTHREADED); if (hr >= 0) @@ -872,14 +878,14 @@ namespace DualScreenDemo { SyncToPrimaryMonitor(); } - } + } /// /// 跳至下一首歌曲的方法,會根據目前播放清單(使用者清單或公播清單)做切換與播放邏輯控制。 /// public async Task SkipToNextSong() { - try + try { // 停止當前播放並釋放資源(如播放器、影片檔等) StopAndReleaseResources(); @@ -918,21 +924,22 @@ namespace DualScreenDemo } // 如果目前歌曲在播放歷史列表的索引是合法的(防呆) - if (PrimaryForm.currentSongIndexInHistory >= 0 && PrimaryForm.currentSongIndexInHistory < PrimaryForm.playedSongsHistory.Count) + if (PrimaryForm.currentSongIndexInHistory >= 0 && PrimaryForm.currentSongIndexInHistory < PrimaryForm.playedSongsHistory.Count) { var currentSong = PrimaryForm.playedSongsHistory[PrimaryForm.currentSongIndexInHistory]; // 如果新的播放清單還有歌曲,並且當前歷史記錄中的歌曲與播放清單的第一首一致,則標記為已播畢 - if (playingSongList.Count > 0 && currentSong == playingSongList[0]) + if (playingSongList.Count > 0 && currentSong == playingSongList[0]) { PrimaryForm.playStates[PrimaryForm.currentSongIndexInHistory] = PlayState.Played; } } /*如果當前為公播,不可以+1*/ - bool isPlayingPublicList = PrimaryForm.userRequestedSongs.Count == 0 || + bool isPlayingPublicList = PrimaryForm.userRequestedSongs.Count == 0 || (PrimaryForm.currentSongIndexInHistory >= PrimaryForm.userRequestedSongs.Count - 1 && PrimaryForm.Instance.videoPlayerForm.IsPlayingPublicSong); - if(!isPlayingPublicList){ - PrimaryForm.currentSongIndexInHistory+=1; + if (!isPlayingPublicList) + { + PrimaryForm.currentSongIndexInHistory += 1; } Console.WriteLine("currentSongIndexInHistory : " + PrimaryForm.currentSongIndexInHistory); } @@ -952,7 +959,7 @@ namespace DualScreenDemo { isUserPlaylistPlaying = false; currentSongIndex = -1; - + // 重新初始化公播列表 publicPlaylist = new List(); @@ -972,7 +979,7 @@ namespace DualScreenDemo if (File.Exists(bgmPath)) { publicPlaylist.Add(new SongData( - i.ToString(), $"背景音樂{i:D2}", "", "", "", bgmPath, + i.ToString(), $"背景音樂{i:D2}", "", "", "", bgmPath, "", "", "", "", 1 )); } @@ -1273,7 +1280,7 @@ namespace DualScreenDemo { long currentPosition = 0; long duration = 0; - + if (mediaSeekingSecondary.GetCurrentPosition(out currentPosition) >= 0 && mediaSeekingSecondary.GetDuration(out duration) >= 0) { @@ -1281,24 +1288,24 @@ namespace DualScreenDemo double durationSeconds = duration / 10000000.0; // 添加更严格的结束条件判断 - bool isAtEnd = durationSeconds > 0 && currentSeconds > 0 && + bool isAtEnd = durationSeconds > 0 && currentSeconds > 0 && Math.Abs(currentSeconds - durationSeconds) < 0.1 && // 确保真的到了结尾 !isPaused; if (isAtEnd && !isPlayingNext) { Console.WriteLine($"檢測到歌曲結束 - 當前位置: {currentSeconds:F2}秒, 總時長: {durationSeconds:F2}秒"); - + if (!isPlayingNext) { isPlayingNext = true; - + // 添加额外的保护:确保在切换前停止当前播放 if (mediaControlSecondary != null) { mediaControlSecondary.Stop(); } - + this.BeginInvoke(new Action(async () => { try @@ -1311,17 +1318,17 @@ namespace DualScreenDemo { if (playingSongList.Count > 0) { - try + try { // 移除當前播放的歌曲 playingSongList.RemoveAt(0); - + // 更新播放状态逻辑 if (PrimaryForm.currentSongIndexInHistory >= 0) { // 将当前播放的歌曲标记为已播放 PrimaryForm.playStates[PrimaryForm.currentSongIndexInHistory] = PlayState.Played; - + // 如果还有下一首歌 if (playingSongList.Count > 0) { @@ -1337,14 +1344,14 @@ namespace DualScreenDemo if (playingSongList.Count == 0) { Console.WriteLine("用戶播放列表已播放完畢,切換到公共播放列表"); - + // 确保当前歌曲状态更新为已播放 - if (PrimaryForm.currentSongIndexInHistory >= 0 && + if (PrimaryForm.currentSongIndexInHistory >= 0 && PrimaryForm.currentSongIndexInHistory < PrimaryForm.playStates.Count) { PrimaryForm.playStates[PrimaryForm.currentSongIndexInHistory] = PlayState.Played; Console.WriteLine($"已將最後一首歌曲狀態更新為已播放,索引:{PrimaryForm.currentSongIndexInHistory}"); - + // 强制刷新显示 if (PrimaryForm.Instance.multiPagePanel != null) { @@ -1360,7 +1367,7 @@ namespace DualScreenDemo currentSongIndex = -1; // 确保所有未播放的歌曲状态被清除 - for (int i = PrimaryForm.currentSongIndexInHistory + 1; + for (int i = PrimaryForm.currentSongIndexInHistory + 1; i < PrimaryForm.playStates.Count; i++) { if (PrimaryForm.playStates[i] == PlayState.Playing) @@ -1417,14 +1424,14 @@ namespace DualScreenDemo { Console.WriteLine($"監控媒體事件時發生錯誤: {ex.Message}"); isPlayingNext = false; - + // 添加重试机制 if (!isUserPlaylistPlaying && publicPlaylist != null) { await Task.Delay(1000); await PlayNextSong(); } - + await Task.Delay(1000); } @@ -1533,7 +1540,7 @@ namespace DualScreenDemo return -10000; } private bool isVocalRemoved = false; - public async void ToggleVocalRemoval() + public async void ToggleVocalRemoval() { try { @@ -1602,7 +1609,7 @@ namespace DualScreenDemo } catch (Exception ex) { - Console.WriteLine( ex.Message); + Console.WriteLine(ex.Message); } } @@ -1649,7 +1656,7 @@ namespace DualScreenDemo } catch (Exception ex) { - Console.WriteLine( ex.Message); + Console.WriteLine(ex.Message); } } @@ -1688,7 +1695,7 @@ namespace DualScreenDemo // 如果是空列表,设置为正在播放 PrimaryForm.playStates.Add(PlayState.Playing); PrimaryForm.currentSongIndexInHistory = PrimaryForm.playedSongsHistory.Count - 1; - + // 确保之前相同歌曲的状态为已播放 foreach (int index in sameNameIndices) { @@ -1697,14 +1704,14 @@ namespace DualScreenDemo PrimaryForm.playStates[index] = PlayState.Played; } } - + VideoPlayerForm.Instance.SetPlayingSongList(PrimaryForm.userRequestedSongs); } else { // 如果不是空列表,设置为未播放 PrimaryForm.playStates.Add(PlayState.NotPlayed); - + // 更新所有相同歌曲的状态 foreach (int index in sameNameIndices.Where(i => i < PrimaryForm.playedSongsHistory.Count - 1)) { @@ -1767,7 +1774,7 @@ namespace DualScreenDemo PrimaryForm.playedSongsHistory.Add(songData); PrimaryForm.playStates.Add(PlayState.Playing); PrimaryForm.currentSongIndexInHistory = PrimaryForm.playedSongsHistory.Count - 1; - + // 更新之前相同歌曲的状态 foreach (int index in sameNameIndices) { @@ -1776,18 +1783,18 @@ namespace DualScreenDemo PrimaryForm.playStates[index] = PlayState.Played; } } - + VideoPlayerForm.Instance.SetPlayingSongList(PrimaryForm.userRequestedSongs); } else { // 插入到当前播放歌曲之后 int insertIndex = PrimaryForm.currentSongIndexInHistory + 1; - + PrimaryForm.userRequestedSongs.Insert(1, songData); PrimaryForm.playedSongsHistory.Insert(insertIndex, songData); PrimaryForm.playStates.Insert(insertIndex, PlayState.NotPlayed); - + // 更新所有相同歌曲的状态 foreach (int index in sameNameIndices) { @@ -1817,5 +1824,67 @@ namespace DualScreenDemo Console.WriteLine("Error occurred: " + ex.Message); } } + + + public VideoStatus GetCurrentVideoStatus() + { + var status = new VideoStatus(); + try + { + IMediaSeeking mediaSeekingSecondary = graphBuilderSecondary as IMediaSeeking; + if (mediaSeekingSecondary != null) + { + long position; + if (mediaSeekingSecondary.GetCurrentPosition(out position) >= 0) + { + status.PositionSeconds = position / 10000000.0; + } + else + { + status.LastError = "無法取得影片播放位置"; + status.PositionSeconds = -1; + } + } + else + { + status.LastError = "mediaSeekingSecondary 物件為 null"; + status.PositionSeconds = -1; + } + + if (mediaControlSecondary != null) + { + FilterState stateCode; + int hr = mediaControlSecondary.GetState(100, out stateCode); + if (hr >= 0) + { + var state = (FilterState)stateCode; + status.PlayState = state.ToString(); + status.IsGraphOk = true; + } + else + { + status.PlayState = "無法取得播放狀態"; + status.IsGraphOk = false; + } + } + else + { + status.PlayState = "mediaControlSecondary 物件為 null"; + status.IsGraphOk = false; + } + } + catch (Exception ex) + { + status.LastError = "取得影片狀態時發生例外:" + ex.Message; + status.PositionSeconds = -1; + status.PlayState = "Error"; + status.IsGraphOk = false; + } + + return status; + } + } -} \ No newline at end of file +} + + diff --git a/WatchDog.cs b/WatchDog.cs new file mode 100644 index 0000000..4280190 --- /dev/null +++ b/WatchDog.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using DualScreenDemo.Shared; +public class WatchDog +{ + + private Func getVideoStatus; + private Func isApplicationResponsive; + private Thread watchdogThread; + private bool running = false; + private double lastPosition = -1; + private int freezeCounter = 0; + + public WatchDog(Func getVideoPositionFunc, Func isAppResponsiveFunc) + { + getVideoStatus = getVideoPositionFunc; + isApplicationResponsive = isAppResponsiveFunc; + } + + public void Start() + { + running = true; + watchdogThread = new Thread(Run); + watchdogThread.IsBackground = true; + watchdogThread.Start(); + } + + public void Stop() + { + running = false; + watchdogThread?.Join(); + } + + private void Run() +{ + while (running) + { + var status = getVideoStatus(); // 改用 getVideoStatus 取得完整狀態 + bool responsive = isApplicationResponsive(); + + if (!status.IsGraphOk) + { + Log($"影片圖表異常: {status.LastError}"); + } + else if(status.PlayState != "Paused") + { + double currentPosition = status.PositionSeconds; + + if (Math.Abs(currentPosition - lastPosition) < 0.001) + { + freezeCounter++; + if (freezeCounter >= 3) + { + Log($"影片疑似卡死(3次位置沒變):位置={currentPosition:F2}秒,播放狀態={status.PlayState}"); + freezeCounter = 0; // 記得 reset + } + } + else + { + freezeCounter = 0; + } + + lastPosition = currentPosition; + } + + if (!responsive) + { + Log("UI 疑似卡死(Invoke 失敗)"); + } + + Thread.Sleep(5000); + } +} + + + private void Log(string message) + { + string logFilePath = Path.Combine("txt", "watchdog_log.txt"); + File.AppendAllText(logFilePath, $"{DateTime.Now}: {message}{Environment.NewLine}"); + } +}