2017-10-12 41 views
0

我正在開發一個應用程序,其中我有一個帶有MediaElement的窗口。用戶可以在此窗口中播放電影。我想添加一個選項來播放帶有字幕的電影。我知道如何在MediaElement上顯示文字,但問題是,如何以特定間隔顯示字幕。在WPF中顯示字幕

我的解決方案(不工作):我會解析.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; })); 
} 
+0

這聽起來非常低效。至少,當你解析它們時,你可以「正常化」時間 - 基本上四捨五入到最近的500ms。那麼你的問題變得更簡單。 – Gui

回答

0

我推薦一個更可重複使用的的方法,它可以讓你尋找,跳過和重播。由於很多代碼在這個問題中缺失,我已經對它的外觀做了一些假設。

將您的字幕保存在一個簡單的類中,該類至少包含應顯示的時間戳和要顯示的文本。如果您在任何時候都不想顯示任何文本,只需爲文本添加一個條目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); 
} 
+0

非常感謝你!它像一個魅力。 – Matey

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 
    }); 
} 
+0

我的第一個想法是提出這樣的建議。但是,我的腦海裏浮現出一個問題。 Timer對象是否足夠精確?我的意思是,如果將'awake'設置爲1.254ms,由於系統/線程同步或類似的東西,是否會在1.254ms或稍微之前或之後實際喚醒? – fharreau

+0

@fharreau它不準確到1毫秒,但是對於給定的任務(顯示字幕)我相信足夠準確。如果在實際時間之前或之後顯示〜10ms的字幕 - 應該足夠好。但是,如果不是,你需要非常高的精度(高達1ms) - 需要另一種解決方案。 – Evk

0

我會用類似EVK的解決方案,但略有不同的方法去。

從字幕的有序列表(按出現時間):

  1. 在第一個字幕
  2. 計算其顯示
  3. 等待時間
  4. 最後的時刻之前的剩餘時間跨度顯示它

取下一個字幕,然後重複。

這是一個使用.NET異步/等待和任務的代碼。

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 
    } 
} 

你可以添加一些其他的東西,如:

  • 如果時刻已經過去,什麼也不做,採取下一步的字幕
  • 你可以使用一個共享的入庫時間變量手動換擋所有字幕幻影時刻(如果字幕與電影不同步)
  • 不要在ProgramSubtitles函數中運行任務,但讓調用者在任務中運行該函數? (根據您的需要)