2025-08-13 09:51:11 +08:00
|
|
|
using LibVLCSharp.Shared;
|
2025-08-07 18:34:45 +08:00
|
|
|
|
|
|
|
namespace DualScreenDemo.Services
|
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
public class MediaService : IDisposable
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
private readonly LibVLC _libVLC;
|
|
|
|
private readonly MediaPlayer _mediaPlayerPrimary;
|
|
|
|
private readonly MediaPlayer _mediaPlayerSecondary;
|
|
|
|
private Media? _media;
|
|
|
|
private Media? _mediaNoAudio;
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
public MediaService()
|
2025-08-12 09:46:50 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
Core.Initialize();
|
|
|
|
_libVLC = new LibVLC(
|
|
|
|
"--aout=directsound",
|
|
|
|
//"--avcodec-hw=dxva2",
|
|
|
|
"--network-caching=300",
|
|
|
|
"--file-caching=300",
|
|
|
|
"--audio-time-stretch"
|
|
|
|
);
|
|
|
|
|
|
|
|
_mediaPlayerPrimary = new MediaPlayer(_libVLC);
|
|
|
|
_mediaPlayerSecondary = new MediaPlayer(_libVLC);
|
|
|
|
}
|
|
|
|
|
|
|
|
#region Player Setup
|
|
|
|
public void SetVideoFormPrimary(nint handle, int width, int height)
|
|
|
|
{
|
|
|
|
_mediaPlayerPrimary.Hwnd = handle;
|
|
|
|
_mediaPlayerPrimary.AspectRatio = $"{width}:{height}";
|
|
|
|
_mediaPlayerPrimary.Scale = 0; // 保持原比例
|
|
|
|
}
|
|
|
|
|
|
|
|
public void SetVideoFormSecondary(nint handle, int width, int height)
|
|
|
|
{
|
|
|
|
_mediaPlayerSecondary.Hwnd = handle;
|
|
|
|
_mediaPlayerSecondary.AspectRatio = $"{width}:{height}";
|
|
|
|
_mediaPlayerSecondary.Scale = 0;
|
2025-08-12 09:46:50 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Playback
|
|
|
|
public MediaPlayer PrimaryPlayer => _mediaPlayerPrimary;
|
|
|
|
public MediaPlayer SecondaryPlayer => _mediaPlayerSecondary;
|
|
|
|
public bool IsPlaying => _mediaPlayerSecondary.IsPlaying;
|
|
|
|
|
2025-08-12 09:46:50 +08:00
|
|
|
public bool IsAtEnd()
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
var duration = _mediaPlayerSecondary.Media?.Duration ?? 0;
|
|
|
|
var time = _mediaPlayerSecondary.Time;
|
|
|
|
return duration > 0 && Math.Abs(duration - time) < 1000;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void LoadMedia(string filePath, int audioTrackIndex = 0)
|
|
|
|
{
|
|
|
|
_media?.Dispose();
|
|
|
|
|
|
|
|
// 建立一個完整有聲音的 Media 給 secondary 播放器
|
|
|
|
_media = new Media(_libVLC, filePath, FromType.FromPath);
|
|
|
|
_media.AddOption(":audio-output=directsound");
|
|
|
|
_media.AddOption($":audio-track={audioTrackIndex}");
|
|
|
|
|
|
|
|
// 同時準備給 primary 播放器用的無聲音版本 Media
|
|
|
|
_mediaNoAudio?.Dispose();
|
|
|
|
_mediaNoAudio = new Media(_libVLC, filePath, FromType.FromPath);
|
|
|
|
_mediaNoAudio.AddOption(":no-audio"); // 關閉聲音輸出
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
|
|
|
|
public async Task PlayAsync()
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
if (_media == null || _mediaNoAudio == null) return;
|
2025-08-12 09:46:50 +08:00
|
|
|
|
2025-08-13 09:51:11 +08:00
|
|
|
_mediaPlayerPrimary.Play(_mediaNoAudio); // 播放無聲音版本
|
|
|
|
await Task.Delay(100);
|
|
|
|
_mediaPlayerSecondary.Play(_media); // 播放有聲音版本
|
2025-08-12 09:46:50 +08:00
|
|
|
|
2025-08-13 09:51:11 +08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Sync
|
|
|
|
private async Task SyncPlayersAsync()
|
|
|
|
{
|
|
|
|
while (_mediaPlayerPrimary.IsPlaying && _mediaPlayerSecondary.IsPlaying)
|
2025-08-12 09:46:50 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
var t1 = _mediaPlayerPrimary.Time;
|
|
|
|
var t2 = _mediaPlayerSecondary.Time;
|
|
|
|
|
|
|
|
if (Math.Abs(t1 - t2) > 100)
|
|
|
|
_mediaPlayerSecondary.Time = t1;
|
|
|
|
|
|
|
|
await Task.Delay(200);
|
2025-08-12 09:46:50 +08:00
|
|
|
}
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Mute
|
|
|
|
public bool Mute(bool isMuted)
|
|
|
|
{
|
|
|
|
_mediaPlayerSecondary.Mute = isMuted;
|
|
|
|
return _mediaPlayerSecondary.Mute;
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Volume
|
2025-08-07 18:34:45 +08:00
|
|
|
public void SetVolume(int volume)
|
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
_mediaPlayerPrimary.Volume = volume;
|
|
|
|
_mediaPlayerSecondary.Volume = volume;
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
public int GetVolume() => _mediaPlayerSecondary?.Volume ?? 0;
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Audio Tracks
|
|
|
|
public List<TrackDescription> GetAudioTracks()
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
var result = new List<TrackDescription>();
|
|
|
|
var media = _mediaPlayerSecondary.Media;
|
|
|
|
|
|
|
|
if (media == null) return result;
|
|
|
|
|
|
|
|
if (!media.IsParsed)
|
|
|
|
media.Parse(MediaParseOptions.ParseLocal);
|
|
|
|
|
|
|
|
var tracks = media.Tracks;
|
|
|
|
if (tracks == null) return result;
|
|
|
|
|
|
|
|
foreach (var track in tracks.Where(t => t.TrackType == TrackType.Audio))
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
result.Add(new TrackDescription
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
Id = track.Id,
|
|
|
|
Name = !string.IsNullOrEmpty(track.Language) ? track.Language : $"Audio Track {track.Id}"
|
|
|
|
});
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
return result;
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
|
|
|
|
public async Task SetAudioTrackToAsync(int trackIndex)
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
var audioTracks = GetAudioTracks();
|
|
|
|
if (trackIndex < 0 || trackIndex >= audioTracks.Count) return;
|
2025-08-07 18:34:45 +08:00
|
|
|
|
2025-08-13 09:51:11 +08:00
|
|
|
if (!_mediaPlayerSecondary.IsPlaying)
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
_mediaPlayerSecondary.Play();
|
|
|
|
await Task.Delay(500);
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
|
|
|
|
_mediaPlayerSecondary.SetAudioTrack(audioTracks[trackIndex].Id);
|
|
|
|
await Task.Delay(300);
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Dispose
|
|
|
|
public void Dispose()
|
2025-08-07 18:34:45 +08:00
|
|
|
{
|
2025-08-13 09:51:11 +08:00
|
|
|
if (_disposed) return;
|
2025-08-07 18:34:45 +08:00
|
|
|
|
2025-08-13 09:51:11 +08:00
|
|
|
_mediaPlayerPrimary?.Dispose();
|
|
|
|
_mediaPlayerSecondary?.Dispose();
|
|
|
|
_media?.Dispose();
|
|
|
|
_mediaNoAudio?.Dispose();
|
|
|
|
_libVLC?.Dispose();
|
|
|
|
|
|
|
|
_mediaNoAudio = null;
|
|
|
|
_media = null;
|
|
|
|
_disposed = true;
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
2025-08-13 09:51:11 +08:00
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
|
|
|
|
public class TrackDescription
|
|
|
|
{
|
|
|
|
public int Id { get; set; }
|
|
|
|
public string Name { get; set; } = string.Empty;
|
2025-08-07 18:34:45 +08:00
|
|
|
}
|
|
|
|
}
|