diff --git a/src/ui/Features/Shared/PickMatroskaTrack/PickMatroskaTrackViewModel.cs b/src/ui/Features/Shared/PickMatroskaTrack/PickMatroskaTrackViewModel.cs
index ea36463efa3..fd0417685e7 100644
--- a/src/ui/Features/Shared/PickMatroskaTrack/PickMatroskaTrackViewModel.cs
+++ b/src/ui/Features/Shared/PickMatroskaTrack/PickMatroskaTrackViewModel.cs
@@ -1,6 +1,7 @@
using Nikse.SubtitleEdit.UiLogic.Export;
using Avalonia.Controls;
using Avalonia.Input;
+using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -19,6 +20,7 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
namespace Nikse.SubtitleEdit.Features.Shared.PickMatroskaTrack;
@@ -43,6 +45,18 @@ public partial class PickMatroskaTrackViewModel : ObservableObject
private MatroskaFile? _matroskaFile;
private string _fileName;
+ // MatroskaFile is not thread-safe (single shared FileStream), so preview parsing must be
+ // serialized. The token lets a newer selection discard a stale preview still queued behind it.
+ private readonly SemaphoreSlim _previewLock = new(1, 1);
+ private int _trackChangeToken;
+
+ // GetSubtitle reads (and caches) the whole cluster data on its first call; that is the only
+ // slow part, so the progress window is only shown until that initial read has completed.
+ private bool _clusterLoaded;
+
+ // Only bother with the progress window for files large enough that the read is noticeable.
+ private const long MatroskaProgressWindowMinFileSize = 25 * 1024 * 1024; // 25 MB
+
public PickMatroskaTrackViewModel(IFileHelper fileHelper, IWindowService windowService)
{
_fileHelper = fileHelper;
@@ -212,100 +226,183 @@ internal void OnKeyDownHandler(object? sender, KeyEventArgs e)
internal void DataGridTracksSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
- TrackChanged();
+ _ = TrackChangedAsync();
}
- private bool TrackChanged()
+ ///
+ /// Builds the preview for the selected track. The actual parsing (which, on the first call,
+ /// reads the whole file's cluster data) runs off the UI thread with a progress window so the
+ /// dialog no longer freezes when picking a track in a large multi-subtitle file (#12193).
+ ///
+ private async Task TrackChangedAsync()
{
- var selectedTrack = SelectedTrack;
- if (selectedTrack == null || selectedTrack.MatroskaTrackInfo == null)
+ var trackInfo = SelectedTrack?.MatroskaTrackInfo;
+ var matroskaFile = _matroskaFile;
+ var token = ++_trackChangeToken;
+
+ Rows.Clear();
+ if (trackInfo == null || matroskaFile == null)
{
SubtitleCountText = string.Empty;
- return false;
+ return;
}
- Rows.Clear();
- var count = 0;
- var trackInfo = selectedTrack.MatroskaTrackInfo!;
- var subtitles = _matroskaFile?.GetSubtitle(trackInfo.TrackNumber, null);
- if (trackInfo.CodecId == MatroskaTrackType.SubRip && subtitles != null)
+ await _previewLock.WaitAsync();
+ try
{
- count = AddTextContent(trackInfo, subtitles, new SubRip());
+ // A newer selection arrived while we were waiting for the previous preview to finish;
+ // let that newer call build the preview instead.
+ if (token != _trackChangeToken)
+ {
+ return;
+ }
+
+ PleaseWaitViewModel? pleaseWaitVm = null;
+ if (!_clusterLoaded)
+ {
+ long fileSize = 0;
+ try
+ {
+ fileSize = new FileInfo(matroskaFile.Path).Length;
+ }
+ catch
+ {
+ // ignore - just means no size-based gating
+ }
+
+ if (fileSize >= MatroskaProgressWindowMinFileSize && Window != null)
+ {
+ pleaseWaitVm = _windowService.ShowWindow(Window);
+ pleaseWaitVm.StatusText = Se.Language.Main.ParsingMatroskaFile;
+ }
+ }
+
+ try
+ {
+ var vm = pleaseWaitVm;
+ var preview = await Task.Run(() => BuildPreview(trackInfo, matroskaFile, vm));
+ _clusterLoaded = true;
+
+ // Discard the result if the user moved on to another track meanwhile.
+ if (token != _trackChangeToken)
+ {
+ return;
+ }
+
+ foreach (var cue in preview.Cues)
+ {
+ Rows.Add(new MatroskaSubtitleCueDisplay
+ {
+ Number = cue.Number,
+ Show = cue.Show,
+ Duration = cue.Duration,
+ Text = cue.Text ?? string.Empty,
+ Image = cue.Image != null ? new Image { Source = cue.Image } : null,
+ });
+ }
+
+ SubtitleCountText = string.Format(Se.Language.File.Import.NumberOfSubtitlesX, preview.Count);
+ }
+ catch (Exception exception)
+ {
+ Se.LogError(exception, "Error building Matroska track preview");
+ SubtitleCountText = string.Empty;
+ }
+ finally
+ {
+ pleaseWaitVm?.Close();
+ }
}
- else if (trackInfo.CodecId is MatroskaTrackType.SubStationAlpha or MatroskaTrackType.SubStationAlpha2 && subtitles != null)
+ finally
{
- count = AddTextContent(trackInfo, subtitles, new SubStationAlpha());
+ _previewLock.Release();
}
- else if (trackInfo.CodecId is MatroskaTrackType.AdvancedSubStationAlpha or MatroskaTrackType.AdvancedSubStationAlpha2 && subtitles != null)
+ }
+
+ ///
+ /// Parses the selected track into plain preview data. Runs on a background thread, so it must
+ /// not touch any UI controls - the Avalonia objects it creates are decoded
+ /// here, but the controls that host them are built on the UI thread.
+ ///
+ private static PreviewResult BuildPreview(MatroskaTrackInfo trackInfo, MatroskaFile matroskaFile, PleaseWaitViewModel? pleaseWaitVm)
+ {
+ var cues = new List();
+ var count = 0;
+
+ MatroskaFile.LoadMatroskaCallback? callback =
+ pleaseWaitVm != null ? (position, total) => pleaseWaitVm.ReportProgress(position, total) : null;
+ var subtitles = matroskaFile.GetSubtitle(trackInfo.TrackNumber, callback);
+ if (subtitles == null)
{
- count = AddTextContent(trackInfo, subtitles, new AdvancedSubStationAlpha());
+ return new PreviewResult(0, cues);
}
- else if (trackInfo.CodecId is MatroskaTrackType.WebVTT or MatroskaTrackType.WebVTT2 && subtitles != null)
+
+ if (trackInfo.CodecId is MatroskaTrackType.SubRip
+ or MatroskaTrackType.SubStationAlpha or MatroskaTrackType.SubStationAlpha2
+ or MatroskaTrackType.AdvancedSubStationAlpha or MatroskaTrackType.AdvancedSubStationAlpha2
+ or MatroskaTrackType.WebVTT or MatroskaTrackType.WebVTT2)
{
- count = AddTextContent(trackInfo, subtitles, new WebVTT());
+ var sub = new Subtitle();
+ Utilities.LoadMatroskaTextSubtitle(trackInfo, matroskaFile, subtitles, sub);
+ count = sub.Paragraphs.Count;
+ foreach (var p in sub.Paragraphs)
+ {
+ cues.Add(new PreviewCueData
+ {
+ Number = p.Number,
+ Text = p.Text,
+ Show = TimeSpan.FromMilliseconds(p.StartTime.TotalMilliseconds),
+ Duration = TimeSpan.FromMilliseconds(p.EndTime.TotalMilliseconds - p.StartTime.TotalMilliseconds),
+ });
+ }
}
- else if (trackInfo.CodecId == MatroskaTrackType.BluRay && subtitles != null && _matroskaFile != null)
+ else if (trackInfo.CodecId == MatroskaTrackType.BluRay)
{
- var pcsData = BluRaySupParser.ParseBluRaySupFromMatroska(trackInfo, _matroskaFile);
+ var pcsData = BluRaySupParser.ParseBluRaySupFromMatroska(trackInfo, matroskaFile);
count = pcsData.Count;
for (var i = 0; i < 20 && i < pcsData.Count; i++)
{
var item = pcsData[i];
- var bitmap = item.GetBitmap();
- var cue = new MatroskaSubtitleCueDisplay()
+ cues.Add(new PreviewCueData
{
Number = i + 1,
Show = TimeSpan.FromMilliseconds(item.StartTimeCode.TotalMilliseconds),
Duration = TimeSpan.FromMilliseconds(item.EndTimeCode.TotalMilliseconds - item.StartTimeCode.TotalMilliseconds),
- Image = new Image { Source = bitmap.ToAvaloniaBitmap() },
- };
- Rows.Add(cue);
+ Image = item.GetBitmap().ToAvaloniaBitmap(),
+ });
}
}
- else if (trackInfo.CodecId == MatroskaTrackType.TextSt && subtitles != null && _matroskaFile != null)
+ else if (trackInfo.CodecId == MatroskaTrackType.TextSt)
{
var subtitle = new Subtitle();
- Utilities.LoadMatroskaTextSubtitle(trackInfo, _matroskaFile, subtitles, subtitle);
+ Utilities.LoadMatroskaTextSubtitle(trackInfo, matroskaFile, subtitles, subtitle);
Utilities.ParseMatroskaTextSt(trackInfo, subtitles, subtitle);
-
count = subtitle.Paragraphs.Count;
for (var i = 0; i < 20 && i < subtitle.Paragraphs.Count; i++)
{
var item = subtitle.Paragraphs[i];
- var cue = new MatroskaSubtitleCueDisplay()
+ cues.Add(new PreviewCueData
{
Number = i + 1,
Show = item.StartTime.TimeSpan,
Duration = TimeSpan.FromMilliseconds(item.EndTime.TotalMilliseconds - item.StartTime.TotalMilliseconds),
Text = item.Text,
- };
- Rows.Add(cue);
+ });
}
}
- SubtitleCountText = string.Format(Se.Language.File.Import.NumberOfSubtitlesX, count);
- return true;
+ return new PreviewResult(count, cues);
}
- private int AddTextContent(MatroskaTrackInfo trackInfo, List subtitles, SubtitleFormat format)
- {
- var sub = new Subtitle();
- Utilities.LoadMatroskaTextSubtitle(trackInfo, _matroskaFile, subtitles, sub);
- var raw = format.ToText(sub, string.Empty);
- for (var i = 0; i < sub.Paragraphs.Count; i++)
- {
- var p = sub.Paragraphs[i];
- var cue = new MatroskaSubtitleCueDisplay()
- {
- Number = p.Number,
- Text = p.Text,
- Show = TimeSpan.FromMilliseconds(p.StartTime.TotalMilliseconds),
- Duration = TimeSpan.FromMilliseconds(p.EndTime.TotalMilliseconds - p.StartTime.TotalMilliseconds),
- };
- Rows.Add(cue);
- }
+ private sealed record PreviewResult(int Count, List Cues);
- return sub.Paragraphs.Count;
+ private sealed class PreviewCueData
+ {
+ public int Number { get; init; }
+ public TimeSpan Show { get; init; }
+ public TimeSpan Duration { get; init; }
+ public string? Text { get; init; }
+ public Bitmap? Image { get; init; }
}
internal void SelectAndScrollToRow(int index)
@@ -319,7 +416,7 @@ internal void SelectAndScrollToRow(int index)
{
TracksGrid.SelectedIndex = index;
TracksGrid.ScrollIntoView(TracksGrid.SelectedItem, null);
- TrackChanged();
+ _ = TrackChangedAsync();
}, DispatcherPriority.Background);
}
}