在WPF中显示字幕

时间:2017-10-12 21:04:46

标签: c# wpf

我正在开发一个应用程序,其中我有一个MediaElement的窗口。用户可以在此窗口中播放电影。我想添加一个选项来播放带字幕的电影。我知道如何在MediaElement上显示文字,但问题是,如何以特定间隔显示字幕。

我的解决方案(不起作用):我会将.src文件解析为Keystart timevaluetext的词典

接下来,我有一个间隔为1毫秒的计时器,每隔一段时间我会检查字典中是否存在电影时间。如果是,我会显示价值。问题是,我无法每毫秒检查字典,但间隔大约是20毫秒,这就是问题所在。所以你知道如何每1毫秒调用一次吗?

private void timer_Tick(object sender, EventArgs e)
{
   string text = MediaElement.Position.ToString("HH:mm:ss.fff");
   Thread t = new Thread(() => SearchThread(text));
   t.Start();
   if (MediaElement.NaturalDuration.HasTimeSpan)
      timer.Text = String.Format("{0} / {1}", MediaElement.Position,
                   MediaElement.NaturalDuration.TimeSpan.ToString());
}

private void SearchThread(string pos)
{
   string text = srcFileControler.Get(pos); //take value from dictionary if exist
   if (text != "")
      this.txtSub.Dispatcher.Invoke(DispatcherPriority.Normal,
           new Action(() => { txtSub.Text = text; }));  
}

3 个答案:

答案 0 :(得分:1)

我建议使用更多可重复使用的方法,让您搜索,跳过和重播。由于问题中缺少很多代码,我对它的外观做了一些假设。

将字幕保存在一个简单的类中,该类至少包含一个应该出现的时间戳和要显示的文本。如果您想在任何时候根本不显示任何文本,只需为文本添加String.Empty条目。

public class SubtitleEntry
{
    public TimeSpan TimeStamp { get; set; }
    public string Text { get; set; }
}

要跟踪您所处的位置(时间戳和字幕索引),请检查下一个条目的时间戳是否早于上次已知的时间戳。如果“当前”字幕条目已更改,请引发事件以更新文本。

public class SubtitleManager
{
    public event EventHandler<string> UpdateSubtitles; 

    private List<SubtitleEntry> _entries;

    private int _currentIndex = -1;
    private TimeSpan _currentTimeStamp = TimeSpan.MinValue;

    public SubtitleManager()
    {
        _entries = new List<SubtitleEntry>();
    }

    public void SetEntries(IEnumerable<SubtitleEntry> entries)
    {
        // Set entries and reset previous "last" entry
        _entries = new List<SubtitleEntry>(entries);
        _currentTimeStamp = TimeSpan.MinValue;
        _currentIndex = -1;
    }

    public void UpdateTime(TimeSpan timestamp)
    {
        // If there are no entries, there is nothing to do
        if (_entries == null || _entries.Count == 0)
            return;

        // Remember position of last displayed subtitle entry
        int previousIndex = _currentIndex;

        // User must have skipped backwards, re-find "current" entry
        if (timestamp < _currentTimeStamp)
            _currentIndex = FindPreviousEntry(timestamp);

        // Remember current timestamp
        _currentTimeStamp = timestamp;

        // First entry not hit yet
        if (_currentIndex < 0 && timestamp < _entries[0].TimeStamp)
            return;

        // Try to find a later entry than the current to be displayed
        while (_currentIndex + 1 < _entries.Count && _entries[_currentIndex + 1].TimeStamp < timestamp)
        {
            _currentIndex++;
        }

        // Has the current entry changed? Notify!
        if(_currentIndex >= 0 && _currentIndex < _entries.Count && _currentIndex != previousIndex)
            OnUpdateSubtitles(_entries[_currentIndex].Text);
    }

    private int FindPreviousEntry(TimeSpan timestamp)
    {
        // Look for the last entry that is "earlier" than the specified timestamp
        for (int i = _entries.Count - 1; i >= 0; i--)
        {
            if (_entries[i].TimeStamp < timestamp)
                return i;
        }

        return -1;
    }

    protected virtual void OnUpdateSubtitles(string e)
    {
        UpdateSubtitles?.Invoke(this, e);
    }
}

在你的窗口中,看起来像这样:

private DispatcherTimer _timer;
private SubtitleManager _manager;

public MainWindow()
{
    InitializeComponent();

    _manager = new SubtitleManager();
    _manager.SetEntries(new List<SubtitleEntry>()
    {
        new SubtitleEntry{Text = "1s", TimeStamp = TimeSpan.FromSeconds(1)},
        new SubtitleEntry{Text = "2s", TimeStamp = TimeSpan.FromSeconds(2)},
        new SubtitleEntry{Text = "4s", TimeStamp = TimeSpan.FromSeconds(4)},
        new SubtitleEntry{Text = "10s", TimeStamp = TimeSpan.FromSeconds(10)},
        new SubtitleEntry{Text = "12s", TimeStamp = TimeSpan.FromSeconds(12)},
    });
    _manager.UpdateSubtitles += ManagerOnUpdateSubtitles;
}

private void ManagerOnUpdateSubtitles(object sender, string text)
{
    txtSubtitle.Text = text;
}

private void BtnLoadVideo_Click(object sender, RoutedEventArgs e)
{
    OpenFileDialog dialog = new OpenFileDialog();
    if (dialog.ShowDialog(this) != true) return;

    element.Source = new Uri(dialog.FileName, UriKind.Absolute);

    _timer = new DispatcherTimer();
    _timer.Tick += Timer_Tick;
    _timer.Interval = new TimeSpan(0,0,0,0,50); //50 ms is fast enough
    _timer.Start();
}

private void Timer_Tick(object sender, EventArgs eventArgs)
{
    _manager.UpdateTime(element.Position);
}

答案 1 :(得分:1)

我会采用类似于Evk解决方案的方法,但略有不同。

从有序的字幕列表(出现时间):

  1. 取第一个副标题
  2. 在显示
  3. 之前计算剩余时间跨度
  4. 等待那段时间
  5. 最后显示
  6. 取下一个副标题,然后重复。

    以下是使用.NET async / await和Task。

    的代码
    public class Subtitle
    {
        /// <summary>
        /// Gets the absolute (in the movie timespan) moment where the subtitle must be displayed.
        /// </summary>
        public TimeSpan Moment { get; set; }
    
        /// <summary>
        /// Gets the text of the subtitle.
        /// </summary>
        public string Text { get; set; }
    }
    
    public class SubtitleManager
    {
        /// <summary>
        /// Starts a task that display the specified subtitles at the right moment, considering the movie playing start date.
        /// </summary>
        /// <param name="movieStartDate"></param>
        /// <param name="subtitles"></param>
        /// <returns></returns>
        public Task ProgramSubtitles(DateTime movieStartDate, IEnumerable<Subtitle> subtitles)
        {
            return Task.Run(async () =>
            {
                foreach (var subtitle in subtitles.OrderBy(s => s.Moment))
                {
                    // Computes for each subtitle the time to sleep from the current DateTime.Now to avoid shifting due to the duration of the subtitle display for example
                    var sleep = DateTime.Now - (movieStartDate + subtitle.Moment);
    
                    // Waits for the right moment to display the subtitle
                    await Task.Delay(sleep);
    
                    // Show the subtitle
                    this.ShowText(subtitle.Text);
                }
            });
        }
    
        private void ShowText(string text)
        {
            // Do your stuff here
            // Since the calling thread is not the UI thread, you will probably need to call the text display in the dispatcher thread
        }
    }
    

    您可以添加一些其他内容:

    • 如果时机已过,请不要做任何事情并采取下一个字幕
    • 您可以使用共享的Timespan变量手动移动幻影的所有字幕时刻(如果字幕未与电影同步)
    • 不要在ProgramSubtitles函数中运行任务,而是让调用者在任务中运行该函数? (根据您的需要)

答案 2 :(得分:0)

我会说采取另一种方法更好。当电影开始时 - 从列表中获取第一个字幕间隔(比如说它是00:01:00)并启动计时器,此时间后将触发。然后当这个计时器触发所有你需要的只是显示相应的字幕并重复抓住下一个间隔时间并再次启动计时器。一些草图代码:

// assuming queue is "sorted" by interval
private readonly Queue<Tuple<TimeSpan, string>> _subtitles = new Queue<Tuple<TimeSpan, string>>();
// call this once, when your movie starts playing
private void CreateTimer() {
    var next = _subtitles.Dequeue();
    if (next == null) {
        ShowText(null);
        return;
    }
    System.Threading.Timer timer = null;
    timer = new System.Threading.Timer(_ => {
        timer.Dispose();
        ShowText(next.Item2);                
        CreateTimer();
    }, null, next.Item1, Timeout.InfiniteTimeSpan);
}

private void ShowText(string text) {
    Dispatcher.Invoke(() =>
    {
        // show text
    });
}