202508151141

調整 VLC 雙螢幕同步顯示
This commit is contained in:
allen.yan 2025-08-15 11:42:56 +08:00
parent 32eeaa0a83
commit 4d94dc6d8e
3 changed files with 40 additions and 110 deletions

View File

@ -5,142 +5,67 @@ namespace DualScreenDemo.Services
public class MediaService : IDisposable public class MediaService : IDisposable
{ {
private readonly LibVLC _libVLC; private readonly LibVLC _libVLC;
private readonly MediaPlayer _mediaPlayerPrimary; private readonly MediaPlayer _mediaPlayer;
private readonly MediaPlayer _mediaPlayerSecondary;
private Media? _media; private Media? _media;
private Media? _mediaNoAudio;
private bool _disposed; private bool _disposed;
public MediaService() public MediaService(nint handle)
{ {
Core.Initialize(); Core.Initialize();
_libVLC = new LibVLC( _libVLC = new LibVLC(
"--aout=directsound",
//"--avcodec-hw=dxva2",
"--network-caching=300", "--network-caching=300",
"--file-caching=300", "--file-caching=300",
"--audio-time-stretch" "--audio-time-stretch",
"--video-filter=clone",
$"--clone-views={handle}" // 這是 clone 的目標視窗
); );
_mediaPlayerPrimary = new MediaPlayer(_libVLC); _mediaPlayer = new MediaPlayer(_libVLC);
_mediaPlayerSecondary = new MediaPlayer(_libVLC);
} }
#region Player Setup #region Player Setup
public void SetVideoFormPrimary(nint handle, int width, int height) public void SetVideoOutput(nint handle, int width, int height)
{ {
_mediaPlayerPrimary.Hwnd = handle; _mediaPlayer.Hwnd = handle;
_mediaPlayerPrimary.AspectRatio = $"{width}:{height}"; _mediaPlayer.AspectRatio = $"{width}:{height}";
_mediaPlayerPrimary.Scale = 0; // 保持原比例 _mediaPlayer.Scale = 0;
}
public void SetVideoFormSecondary(nint handle, int width, int height)
{
_mediaPlayerSecondary.Hwnd = handle;
_mediaPlayerSecondary.AspectRatio = $"{width}:{height}";
_mediaPlayerSecondary.Scale = 0;
} }
#endregion #endregion
#region Playback #region Playback
public MediaPlayer PrimaryPlayer => _mediaPlayerPrimary; public MediaPlayer Player => _mediaPlayer;
public MediaPlayer SecondaryPlayer => _mediaPlayerSecondary; public bool IsPlaying => _mediaPlayer.IsPlaying;
public bool IsPlaying => _mediaPlayerSecondary.IsPlaying;
public bool IsAtEnd() public bool IsAtEnd()
{ {
var duration = _mediaPlayerSecondary.Media?.Duration ?? 0; var duration = _mediaPlayer.Media?.Duration ?? 0;
var time = _mediaPlayerSecondary.Time; var time = _mediaPlayer.Time;
return duration > 0 && Math.Abs(duration - time) < 1000; return duration > 0 && Math.Abs(duration - time) < 1000;
} }
public void LoadMedia(string filePath, int audioTrackIndex = 0) public void LoadMedia(string filePath, int audioTrackIndex = 0)
{ {
_mediaPlayerPrimary.Pause(); _mediaPlayer.Stop();
_mediaPlayerSecondary.Pause();
_media?.Dispose(); _media?.Dispose();
// 建立一個完整有聲音的 Media 給 secondary 播放器
_media = new Media(_libVLC, filePath, FromType.FromPath); _media = new Media(_libVLC, filePath, FromType.FromPath);
_media.AddOption(":audio-output=directsound");
_media.AddOption($":audio-track={audioTrackIndex}"); _media.AddOption($":audio-track={audioTrackIndex}");
// 同時準備給 primary 播放器用的無聲音版本 Media
_mediaNoAudio?.Dispose();
_mediaNoAudio = new Media(_libVLC, filePath, FromType.FromPath);
_mediaNoAudio.AddOption(":no-audio"); // 關閉聲音輸出
} }
public async Task PlayAsync() public void Play()
{ {
if (_media == null || _mediaNoAudio == null) return; if (_media == null) return;
_mediaPlayer.Play(_media);
_mediaPlayerPrimary.Play(_mediaNoAudio); // 播放無聲音版本
await Task.Delay(100);
_mediaPlayerSecondary.Play(_media); // 播放有聲音版本
await SyncPlayersAsync();
}
public async Task Play()
{
_mediaPlayerPrimary.Play(); // 播放無聲音版本
await Task.Delay(100);
_mediaPlayerSecondary.Play(); // 播放有聲音版本
await SyncPlayersAsync();
}
public void Pause()
{
_mediaPlayerPrimary.Pause();
_mediaPlayerSecondary.Pause();
}
public void Stop()
{
_mediaPlayerPrimary.Stop();
_mediaPlayerSecondary.Stop();
} }
public void Pause() => _mediaPlayer.Pause();
public void Stop() => _mediaPlayer.Stop();
#endregion #endregion
#region Sync #region Audio
private async Task SyncPlayersAsync()
{
while (_mediaPlayerPrimary.IsPlaying && _mediaPlayerSecondary.IsPlaying)
{
var t1 = _mediaPlayerPrimary.Time;
var t2 = _mediaPlayerSecondary.Time;
if (Math.Abs(t1 - t2) > 100)
_mediaPlayerSecondary.Time = t1;
await Task.Delay(200);
}
}
#endregion
#region Mute
public bool Mute(bool isMuted)
{
_mediaPlayerSecondary.Mute = isMuted;
return _mediaPlayerSecondary.Mute;
}
#endregion
#region Volume
public void SetVolume(int volume)
{
_mediaPlayerPrimary.Volume = volume;
_mediaPlayerSecondary.Volume = volume;
}
public int GetVolume() => _mediaPlayerSecondary?.Volume ?? 0;
#endregion
#region Audio Tracks
public List<TrackDescription> GetAudioTracks() public List<TrackDescription> GetAudioTracks()
{ {
var result = new List<TrackDescription>(); var result = new List<TrackDescription>();
var media = _mediaPlayerSecondary.Media; var media = _mediaPlayer.Media;
if (media == null) return result; if (media == null) return result;
@ -161,26 +86,29 @@ namespace DualScreenDemo.Services
return result; return result;
} }
public void SetAudioTrackToAsync(int trackIndex) public void SetAudioTrack(int trackIndex)
{ {
var audioTracks = GetAudioTracks(); var audioTracks = GetAudioTracks();
if (trackIndex < 0 || trackIndex >= audioTracks.Count) return; if (trackIndex < 0 || trackIndex >= audioTracks.Count) return;
_mediaPlayerSecondary.SetAudioTrack(audioTracks[trackIndex].Id); _mediaPlayer.SetAudioTrack(audioTracks[trackIndex].Id);
} }
#endregion #endregion
#region Volume
public void SetVolume(int volume) => _mediaPlayer.Volume = volume;
public int GetVolume() => _mediaPlayer.Volume;
public bool Mute(bool isMuted) => _mediaPlayer.Mute = isMuted;
#endregion
#region Dispose #region Dispose
public void Dispose() public void Dispose()
{ {
if (_disposed) return; if (_disposed) return;
_mediaPlayerPrimary?.Dispose(); _mediaPlayer.Dispose();
_mediaPlayerSecondary?.Dispose();
_media?.Dispose(); _media?.Dispose();
_mediaNoAudio?.Dispose(); _libVLC.Dispose();
_libVLC?.Dispose();
_mediaNoAudio = null;
_media = null; _media = null;
_disposed = true; _disposed = true;
} }

View File

@ -67,7 +67,7 @@ namespace DualScreenDemo
private const int WS_EX_TOPMOST = 0x00000008; private const int WS_EX_TOPMOST = 0x00000008;
private const uint SWP_NOZORDER = 0x0004; private const uint SWP_NOZORDER = 0x0004;
//private MediaServicePrimary primary = new MediaServicePrimary(); //private MediaServicePrimary primary = new MediaServicePrimary();
private MediaService secondary= new MediaService(); private MediaService secondary;
public static OverlayForm overlayForm; public static OverlayForm overlayForm;
public bool isMuted = false; public bool isMuted = false;
public int previousVolume = 100; public int previousVolume = 100;
@ -120,8 +120,9 @@ namespace DualScreenDemo
} }
else else
{ {
secondary.SetVideoFormPrimary(PrimaryForm.Instance.videoPanel.Handle,PrimaryForm.Instance.videoPanel.Width,PrimaryForm.Instance.videoPanel.Height); secondary= new MediaService(PrimaryForm.Instance.videoPanel.Handle);
secondary.SetVideoFormSecondary(this.Handle,secondMonitor.Bounds.Width,secondMonitor.Bounds.Height); secondary.SetVideoOutput(this.Handle, secondMonitor.Bounds.Width, secondMonitor.Bounds.Height);
PlayNextSong(); PlayNextSong();
} }
} }
@ -322,7 +323,7 @@ namespace DualScreenDemo
// 渲染媒體文件 // 渲染媒體文件
secondary.LoadMedia(pathToPlay,song.isPublicSong ? 0 : 1); secondary.LoadMedia(pathToPlay,song.isPublicSong ? 0 : 1);
secondary.Mute(isMuted); secondary.Mute(isMuted);
secondary.PlayAsync(); secondary.Play();
// 音量處理 // 音量處理
//SetVolume(isMuted ? 0 : previousVolume); //SetVolume(isMuted ? 0 : previousVolume);
@ -459,7 +460,7 @@ namespace DualScreenDemo
{ {
isVocalRemoved=!isVocalRemoved; isVocalRemoved=!isVocalRemoved;
int trackIndex = isVocalRemoved ? 1:0; int trackIndex = isVocalRemoved ? 1:0;
secondary.SetAudioTrackToAsync(trackIndex); secondary.SetAudioTrack(trackIndex);
OverlayForm.MainForm.ShowTopRightLabelTime(isVocalRemoved ? "無人聲" : "有人聲"); OverlayForm.MainForm.ShowTopRightLabelTime(isVocalRemoved ? "無人聲" : "有人聲");
} }
} }

View File

@ -9,6 +9,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<ApplicationManifest>app.manifest</ApplicationManifest> <!-- 確保這一行引用了 manifest 文件 --> <ApplicationManifest>app.manifest</ApplicationManifest> <!-- 確保這一行引用了 manifest 文件 -->
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<NoWarn>CS8618,CS8602,CS8622,CS8625,CS8600,CS8603,CS8601,CS8604,CS4014</NoWarn> <NoWarn>CS8618,CS8602,CS8622,CS8625,CS8600,CS8603,CS8601,CS8604,CS4014</NoWarn>