Skip to content

Instantly share code, notes, and snippets.

@igorkulman
Last active April 18, 2016 17:30
Show Gist options
  • Save igorkulman/813c1a2a394639236331 to your computer and use it in GitHub Desktop.
Save igorkulman/813c1a2a394639236331 to your computer and use it in GitHub Desktop.
SRT plugin for PlayerFramework
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace AlzaMedia.Code.PlayReady
{
public static class Extensions
{
public static async Task<Stream> LoadToStream(this Uri source)
{
switch (source.Scheme.ToLowerInvariant())
{
case "ms-appx":
case "ms-appdata":
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(source);
return await file.OpenStreamForReadAsync();
default:
using (var client = new HttpClient())
{
using (var stream = await client.GetStreamAsync(source))
{
var result = new MemoryStream();
await stream.CopyToAsync(result);
result.Seek(0, SeekOrigin.Begin);
return result;
}
}
}
}
public static async Task<string> LoadToString(this Uri source)
{
using (var stream = await source.LoadToStream())
{
return new StreamReader(stream).ReadToEnd();
}
}
}
}
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Microsoft.Media.WebVTT;
using Microsoft.PlayerFramework;
namespace AlzaMedia.Code.PlayReady
{
/// <summary>
/// A player framework plugin capable of displaying timed text captions.
/// </summary>
public class SrtPlugin : PluginBase
{
WebVTTPanel captionsPanel;
Panel captionsContainer;
DispatcherTimer timer;
MediaMarkerManager markerManager;
/// <summary>
/// Creates a new instance of the CaptionsPlugin
/// </summary>
public SrtPlugin()
{
PollingInterval = TimeSpan.FromSeconds(10);
}
/// <summary>
/// Gets or sets the amount of time to check the server for updated data. Only applies when MediaPlayer.IsLive = true
/// </summary>
public TimeSpan PollingInterval { get; set; }
/// <summary>
/// Gets whether the captions panel is visible. Returns true if any captions were found.
/// </summary>
public bool IsSourceLoaded { get; private set; }
/// <summary>
/// Gets the Panel control used to display captions. This can be used to modify the default styles use on captions.
/// </summary>
public WebVTTPanel CaptionsPanel { get { return captionsPanel; } }
/// <summary>
/// Gets or sets the style to be used for the TimedTextCaptions
/// </summary>
public Style CaptionsPanelStyle { get; set; }
void MediaPlayer_SelectedCaptionChanged(object sender, RoutedPropertyChangedEventArgs<Caption> e)
{
if (e.OldValue != null)
{
e.OldValue.PayloadChanged -= caption_PayloadChanged;
e.OldValue.PayloadAugmented -= caption_PayloadAugmented;
}
MediaPlayer.IsCaptionsActive = (e.NewValue as Caption != null);
UpdateCaption(e.NewValue as Caption);
if (e.NewValue != null)
{
e.NewValue.PayloadChanged += caption_PayloadChanged;
e.NewValue.PayloadAugmented += caption_PayloadAugmented;
}
}
void MediaPlayer_PositionChanged(object sender, RoutedPropertyChangedEventArgs<TimeSpan> e)
{
if (MediaPlayer.SelectedCaption != null)
{
captionsPanel.UpdatePosition(MediaPlayer.Position);
markerManager.CheckMarkerPositions(MediaPlayer.Position);
}
}
void MediaPlayer_IsLiveChanged(object sender, RoutedPropertyChangedEventArgs<bool> e)
{
if (MediaPlayer.IsLive)
{
InitializeTimer();
}
else
{
ShutdownTimer();
}
}
private void InitializeTimer()
{
timer = new DispatcherTimer();
timer.Interval = PollingInterval;
timer.Tick += timer_Tick;
timer.Start();
}
private void ShutdownTimer()
{
if (timer != null)
{
timer.Tick -= timer_Tick;
if (timer.IsEnabled) timer.Stop();
timer = null;
}
}
void timer_Tick(object sender, object e)
{
var caption = MediaPlayer.SelectedCaption as Caption;
RefreshCaption(caption);
}
/// <inheritdoc />
protected override bool OnActivate()
{
var mediaContainer = MediaPlayer.Containers.OfType<Panel>().FirstOrDefault(c => c.Name == MediaPlayerTemplateParts.MediaContainer);
captionsContainer = mediaContainer.Children.OfType<Panel>().FirstOrDefault(c => c.Name == MediaPlayerTemplateParts.CaptionsContainer);
if (captionsContainer != null)
{
markerManager = new MediaMarkerManager();
markerManager.MarkerLeft += MarkerManager_MarkerLeft;
markerManager.MarkerReached += MarkerManager_MarkerReached;
captionsPanel = new WebVTTPanel();
if (CaptionsPanelStyle != null) captionsPanel.Style = CaptionsPanelStyle;
MediaPlayer.IsCaptionsActive = (MediaPlayer.SelectedCaption as Caption != null);
captionsContainer.Children.Add(captionsPanel);
UpdateCaption(MediaPlayer.SelectedCaption as Caption);
MediaPlayer.PositionChanged += MediaPlayer_PositionChanged;
MediaPlayer.SelectedCaptionChanged += MediaPlayer_SelectedCaptionChanged;
MediaPlayer.IsLiveChanged += MediaPlayer_IsLiveChanged;
if (MediaPlayer.IsLive) InitializeTimer();
return true;
}
return false;
}
/// <inheritdoc />
protected override void OnDeactivate()
{
MediaPlayer.PositionChanged -= MediaPlayer_PositionChanged;
MediaPlayer.SelectedCaptionChanged -= MediaPlayer_SelectedCaptionChanged;
MediaPlayer.IsLiveChanged -= MediaPlayer_IsLiveChanged;
MediaPlayer.IsCaptionsActive = false;
captionsContainer.Children.Remove(captionsPanel);
captionsContainer = null;
captionsPanel.Clear();
captionsPanel = null;
markerManager.MarkerLeft -= MarkerManager_MarkerLeft;
markerManager.MarkerReached -= MarkerManager_MarkerReached;
markerManager.Clear();
markerManager = null;
IsSourceLoaded = false;
}
void MarkerManager_MarkerLeft(object sender, MediaMarkerEventArgs e)
{
HideCue((WebVTTCue)e.Marker.Content);
}
void MarkerManager_MarkerReached(object sender, MediaMarkerEventArgs e)
{
ShowCue((WebVTTCue)e.Marker.Content);
}
void ShowCue(WebVTTCue cue)
{
captionsPanel.ActiveCues.Add(cue);
}
void HideCue(WebVTTCue cue)
{
captionsPanel.ActiveCues.Remove(cue);
}
/// <summary>
/// Updates the current caption track.
/// Will cause the caption source to download and get parsed, and will will start playing.
/// </summary>
/// <param name="caption">The caption track to use.</param>
public void UpdateCaption(Caption caption)
{
markerManager.Clear();
captionsPanel.Clear();
RefreshCaption(caption);
}
void caption_PayloadChanged(object sender, EventArgs e)
{
RefreshCaption(sender as Caption);
}
void caption_PayloadAugmented(object sender, PayloadAugmentedEventArgs e)
{
AugmentCaption(sender as Caption, e.Payload, e.StartTime, e.EndTime);
}
private async void AugmentCaption(Caption caption, object payload, TimeSpan startTime, TimeSpan endTime)
{
if (caption != null)
{
string result = null;
if (payload is byte[])
{
var byteArray = (byte[])payload;
result = await Task.Run(() => Encoding.UTF8.GetString(byteArray, 0, byteArray.Length));
//result = System.Text.Encoding.UTF8.GetString(byteArray, 0, byteArray.Length);
}
else if (payload is string)
{
result = (string)payload;
}
if (result != null)
{
allTasks = EnqueueTask(() => AugmentWebVTT(result, startTime, endTime), allTasks);
await allTasks;
}
}
}
private async Task<WebVTTDocument> LoadWebVTTDocument(string webvtt, TimeSpan startTime, TimeSpan endTime)
{
return await Task.Run(() =>
{
webvtt = ConvertSrtToWebVTT(webvtt);
return WebVTTParser.ParseDocument(webvtt, startTime, endTime);
});
}
/// <summary>
/// Always either 1 or -1.
/// 1 means add the offset time, -1 means subtract the offset time
/// </summary>
private static int _nOffsetDirection = 1;
private static uint _offsetMs;
private string ConvertSrtToWebVTT(string subtitles)
{
var sb = new StringBuilder();
var rgxDialogNumber = new Regex(@"^\d+$");
var rgxTimeFrame = new Regex(@"(\d\d:\d\d:\d\d,\d\d\d) --> (\d\d:\d\d:\d\d,\d\d\d)");
// Write starting line for the WebVTT file
sb.AppendLine("WEBVTT");
sb.AppendLine("");
// Handle each line of the SRT file
string sLine;
var lines = subtitles.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);
foreach (var line in lines)
{
sLine = line;
var rgxFont = new Regex(@"<font.*?>(.*?)<\/font>");
match = rgxFont.Match(sLine);
if (match.Success)
{
sLine = match.Groups[1].Value;
}
// We only care about lines that aren't just an integer (aka ignore dialog id number lines)
if (rgxDialogNumber.IsMatch(sLine))
continue;
// If the line is a time frame line, reformat and output the time frame
Match match = rgxTimeFrame.Match(sLine);
if (match.Success)
{
if (_offsetMs > 0)
{
// Extract the times from the matched time frame line
var tsStartTime = TimeSpan.Parse(match.Groups[1].Value.Replace(',', '.'));
var tsEndTime = TimeSpan.Parse(match.Groups[2].Value.Replace(',', '.'));
// Modify the time with the offset
long startTimeMs = _nOffsetDirection * _offsetMs + (uint)tsStartTime.TotalMilliseconds;
long endTimeMs = _nOffsetDirection * _offsetMs + (uint)tsEndTime.TotalMilliseconds;
tsStartTime = TimeSpan.FromMilliseconds(startTimeMs < 0 ? 0 : startTimeMs);
tsEndTime = TimeSpan.FromMilliseconds(endTimeMs < 0 ? 0 : endTimeMs);
// Construct the new time frame line
sLine = tsStartTime.ToString(@"hh\:mm\:ss\.fff") +
" --> " +
tsEndTime.ToString(@"hh\:mm\:ss\.fff");
}
else
{
sLine = sLine.Replace(',', '.'); // Simply replace the comma in the time with a period
}
}
sb.AppendLine(sLine); // Write out the line
}
return sb.ToString();
}
private async Task AugmentWebVTT(string webvtt, TimeSpan startTime, TimeSpan endTime)
{
var doc = await LoadWebVTTDocument(webvtt, startTime, endTime);
// merge
foreach (var cue in doc.Cues)
{
bool found = false;
foreach (var marker in markerManager.MediaMarkers)
{
if (marker.Begin == cue.Begin && marker.End == cue.End) // assume its the same one if begin and end match.
{
found = true;
break;
}
}
if (!found)
{
markerManager.MediaMarkers.Add(new MediaMarker() { Begin = cue.Begin, End = cue.End, Content = cue });
}
}
}
private async Task LoadWebVTT(string webvtt)
{
var doc = await LoadWebVTTDocument(webvtt, TimeSpan.Zero, TimeSpan.MaxValue);
foreach (var cue in doc.Cues)
{
markerManager.MediaMarkers.Add(new MediaMarker() { Begin = cue.Begin, End = cue.End, Content = cue });
}
}
private async void RefreshCaption(Caption caption)
{
if (caption != null)
{
string result = null;
if (caption.Payload is Uri)
{
try
{
result = await ((Uri)caption.Payload).LoadToString();
}
catch
{
// TODO: expose event to log errors
return;
}
}
else if (caption.Payload is byte[])
{
var byteArray = (byte[])caption.Payload;
#if SILVERLIGHT && !WINDOWS_PHONE || WINDOWS_PHONE7
result = await TaskEx.Run(() => System.Text.Encoding.UTF8.GetString(byteArray, 0, byteArray.Length));
#else
result = await Task.Run(() => Encoding.UTF8.GetString(byteArray, 0, byteArray.Length));
#endif
//result = System.Text.Encoding.UTF8.GetString(byteArray, 0, byteArray.Length);
}
else if (caption.Payload is string)
{
result = (string)caption.Payload;
}
if (result != null)
{
allTasks = EnqueueTask(() => LoadWebVTT(result), allTasks);
await allTasks;
IsSourceLoaded = true;
// refresh the caption based on the current position. Fixes issue where caption is changed while paused.
if (IsLoaded) // make sure we didn't get unloaded by the time this completed.
{
captionsPanel.UpdatePosition(MediaPlayer.Position);
}
}
}
}
Task allTasks;
static async Task EnqueueTask(Func<Task> newTask, Task taskQueue)
{
if (taskQueue != null) await taskQueue;
await newTask();
}
}
}
@Hesamom
Copy link

Hesamom commented Apr 18, 2016

thanks for your great helpful library, but unfortunately it has two major problems:

1- static subtitle lines(the same start and end time) will cause the player not to show any subtitle!

2- the text of Last subtitle line is duplicated after a line break!

I could fix the first one by validating the subtitle lines, but i couldn't find any where for second problem to start fixing from!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment