/// <summary> /// Utility method to write a single block into a buffer. /// </summary> /// <param name="block">Block to write.</param> /// <param name="builder">String builder buffer to write into.</param> private static void WriteBlock( BaseBlock block, StringBuilder builder) { Cue cue = block as Cue; if (cue != null) { WebVttSerializer.WriteCue(cue, builder); return; } RegionDefinition region = block as RegionDefinition; if (region != null) { WebVttSerializer.WriteRegion(region, builder); return; } Style style = block as Style; if (style != null) { WebVttSerializer.WrteStyle(style, builder); return; } Comment comment = block as Comment; if (comment != null) { WebVttSerializer.WriteComment(comment, builder); return; } throw new ArgumentOutOfRangeException( string.Format("Unknown block type '{0}'.", block.GetType().FullName)); }
/// <summary> /// Writes captions into a text in WebVTT format. /// </summary> /// <param name="captions">Caption blocks to serialize.</param> /// <param name="writer">Text writer to write into.</param> /// <remarks>See http://www.w3.org/TR/webvtt1/ for more details.</remarks> public static async Task SerializeAsync( IEnumerable <BaseBlock> captions, TextWriter writer) { if (writer == null) { throw new ArgumentNullException("writer"); } StringBuilder builder = new StringBuilder(1024); builder.AppendLine(Constants.WebVttHeaderToken); if (captions != null) { Cue lastSeenCue = null; foreach (var block in captions) { if (block == null) { throw new ArgumentException("Caption block cannot be null.", "captions"); } Cue cue = block as Cue; if (lastSeenCue != null) { if (block is RegionDefinition || block is Style) { throw new ArgumentException( string.Format("{0} is not allowed after Cue.", block.GetType().Name), "captions"); } if (cue != null && cue.Start < lastSeenCue.Start) { throw new ArgumentException( string.Format("Cue start time '{0}' must be greater than or equal to previous cue start time '{1}'.", cue.Start.ToString("g"), lastSeenCue.Start.ToString("g")), "captions"); } } if (cue != null) { lastSeenCue = cue; } WebVttSerializer.WriteBlock(block, builder); if (builder.Length >= WebVttSerializer.MaxBufferSize) { await writer.WriteAsync(builder.ToString()) .ConfigureAwait(false); builder.Clear(); } } } if (builder.Length > 0) { await writer.WriteAsync(builder.ToString()) .ConfigureAwait(false); } await writer.FlushAsync() .ConfigureAwait(false); }
/// <summary> /// Utility method to write a caption cue into a buffer. /// </summary> /// <param name="cue">Cue to write.</param> /// <param name="builder">String builder buffer to write into.</param> private static void WriteCue( Cue cue, StringBuilder builder) { if (cue.End <= cue.Start) { throw new ArgumentException(string.Format("Cue start time '{0}' must be less than cue end time '{1}'.", cue.Start, cue.End)); } builder.AppendLine(); if (false == string.IsNullOrWhiteSpace(cue.Id)) { builder.AppendLine(cue.Id); } WebVttSerializer.WriteTimeSpanValue(cue.Start, builder); builder .Append(' ') .Append(Constants.ArrowToken) .Append(' '); WebVttSerializer.WriteTimeSpanValue(cue.End, builder); if (false == string.IsNullOrWhiteSpace(cue.Region)) { if (WebVttSerializer.HasWhiteSpace(cue.Region)) { throw new ArgumentException("White space characters are not allowed in cue region id."); } builder .Append(' ') .Append(Constants.RegionName).Append(':').Append(cue.Region); } if (cue.Alignment.HasValue) { builder .Append(' ') .Append(Constants.AlignName).Append(':').Append(WebVttSerializer.GetAlignmentValue(cue.Alignment.Value)); } if (cue.Line.HasValue) { builder .Append(' ') .Append(Constants.LineName).Append(':'); if (cue.Line.Value.Percent.HasValue) { builder.Append(WebVttSerializer.GetPercentValue(cue.Line.Value.Percent.Value)); } else if (cue.Line.Value.LineNumber.HasValue) { builder.Append(cue.Line.Value.LineNumber.Value); } else { throw new ArgumentException("Cue line setting must specify either percent or line number value."); } if (cue.Line.Value.Alignment.HasValue) { builder.Append(',').Append(WebVttSerializer.GetLineAlignmentValue(cue.Line.Value.Alignment.Value)); } } if (cue.Position.HasValue) { builder .Append(' ') .Append(Constants.PositionName).Append(':') .Append(WebVttSerializer.GetPercentValue( cue.Position.Value.PositionPercent.HasValue ? cue.Position.Value.PositionPercent.Value : 0.0)); if (cue.Position.Value.Alignment.HasValue) { builder .Append(',') .Append(WebVttSerializer.GetPositionAlignmentValue(cue.Position.Value.Alignment.Value)); } } if (cue.SizePercent.HasValue) { builder .Append(' ') .Append(Constants.SizeName) .Append(":") .Append(WebVttSerializer.GetPercentValue(cue.SizePercent.Value)); } if (cue.Vertical.HasValue) { builder .Append(' ') .Append(Constants.VerticalName) .Append(":") .Append(WebVttSerializer.GetVerticalAlignmentValue(cue.Vertical.Value)); } builder.AppendLine(); if (cue.Content != null && cue.Content.Length > 0) { Span previousSpan = null; foreach (var span in cue.Content) { if (span == null) { throw new ArgumentException("Cue content cannot be null."); } if (WebVttSerializer.NeedNewLine(previousSpan, span)) { builder.AppendLine(); } WebVttSerializer.WriteSpan(span, builder); previousSpan = span; } builder.AppendLine(); } }
/// <summary> /// Utility method to return all parts of media captions are a sequence of blocks. /// </summary> /// <param name="captions">Captions to process.</param> /// <returns>Sequence of blocks.</returns> private static IEnumerable <BaseBlock> GetMediaCaptionBlocks( MediaCaptions captions) { if (captions != null) { if (captions.Styles != null && captions.Styles.Length > 0) { foreach (var style in captions.Styles) { if (style != null) { yield return(style); } } } if (captions.Regions != null && captions.Regions.Length > 0) { foreach (var region in captions.Regions) { if (region != null) { yield return(region); } } } if (captions.Cues != null && captions.Cues.Length > 0) { bool needSort = false; Cue previous = null; foreach (var cue in captions.Cues) { if (cue != null) { needSort = previous != null && cue.Start < previous.Start; previous = cue; } if (needSort) { break; } } if (needSort) { var tmp = new List <Cue>(captions.Cues.Length); foreach (var cue in captions.Cues) { if (cue != null) { tmp.Add(cue); } } if (tmp.Count > 0) { tmp.Sort((c1, c2) => c1.Start < c2.Start ? -1 : (c1.Start > c2.Start ? 1 : c1.End.CompareTo(c2.End))); foreach (var cue in tmp) { yield return(cue); } } } else { foreach (var cue in captions.Cues) { yield return(cue); } } } } }
/// <summary> /// Reads WebVTT media captions from a given <see cref="TextReader"/>. /// </summary> /// <param name="reader">Text reader to read the captions.</param> /// <returns>A task that represents the asynchronous read operation.</returns> public static async Task <MediaCaptions> ReadMediaCaptionsAsync( TextReader reader) { if (reader == null) { throw new ArgumentNullException("reader"); } // Process stream header line var line = await reader.ReadLineAsync() .ConfigureAwait(false); if (string.IsNullOrWhiteSpace(line) || line.Length < 6 || false == line.StartsWith(Constants.WebVttHeaderToken) || line.Length >= 7 && line[6] != '\t' && line[6] != ' ') { throw new InvalidDataException("The stream does not start with the correct WebVTT file signature."); } var result = new MediaCaptions(); // Process (skip over) optional headers from the stream while (false == string.IsNullOrEmpty(line)) { line = await reader.ReadLineAsync() .ConfigureAwait(false); } // Process media caption blocks. List <RegionDefinition> regions = new List <RegionDefinition>(); List <Style> styles = new List <Style>(); List <Cue> cues = new List <Cue>(); BaseBlock block; do { block = await WebVttParser.ReadBlockAsync(reader) .ConfigureAwait(false); RegionDefinition region = block as RegionDefinition; Style style = block as Style; Cue cue = block as Cue; if (cues.Count == 0) { if (region != null) { regions.Add(region); } else if (style != null) { styles.Add(style); } } else if (region != null || style != null) { throw new InvalidDataException("Region or style blocks cannot be mixed with cue blocks."); } if (cue != null) { cues.Add(cue); } }while (block != null); if (regions.Count > 0) { result.Regions = regions.ToArray(); } if (styles.Count > 0) { result.Styles = styles.ToArray(); } if (cues.Count > 0) { result.Cues = cues.ToArray(); } return(result); }
/// <summary> /// Reads WebVTT Cue block from the reader. /// </summary> /// <param name="firstLine">First line of the block read from the reader.</param> /// <param name="reader">Text reader to read the style block.</param> /// <returns>A task that represents the asynchronous read operation.</returns> private static async Task <Cue> ReadCueAsync( string firstLine, TextReader reader) { string line = firstLine; var content = new StringBuilder(100); Cue result = new Cue(); if (false == line.Contains(Constants.ArrowToken)) { result.Id = line; line = await reader.ReadLineAsync() .ConfigureAwait(false); } int position = 0; result.Start = ParseTimeSpan(line, ref position); while (position < line.Length && char.IsWhiteSpace(line, position)) { position++; } if (0 != string.CompareOrdinal(line, position, Constants.ArrowToken, 0, Constants.ArrowToken.Length)) { throw new InvalidDataException(string.Format("Invalid characters found in cue timings '{0}' at position {1}.", line, position)); } position += Constants.ArrowToken.Length; while (position < line.Length && char.IsWhiteSpace(line, position)) { position++; } result.End = ParseTimeSpan(line, ref position); if (result.End < result.Start) { throw new InvalidDataException(string.Format("Cue start time is greater than end time in '{0}'.", line)); } if (position < line.Length) { var settings = line.Substring(position).TrimStart('\t', ' ').TrimEnd('\t', ' '); if (false == string.IsNullOrWhiteSpace(settings)) { result.RawSettings = settings; var parsedSettings = WebVttParser.ParseSettings(settings); if (parsedSettings != null) { VerticalTextLayout vertical; if (WebVttParser.TryGetVerticalTextLayoutSetting(Constants.VerticalName, parsedSettings, out vertical)) { result.Vertical = vertical; } LineSettings lineSetting; if (WebVttParser.TryGetLineSetting(Constants.LineName, parsedSettings, out lineSetting)) { result.Line = lineSetting; } PositionSettings positionSetting; if (WebVttParser.TryGetPositionSetting(Constants.PositionName, parsedSettings, out positionSetting)) { result.Position = positionSetting; } double percent; if (WebVttParser.TryGetPercentSetting(Constants.SizeName, parsedSettings, out percent)) { result.SizePercent = percent; } TextAlignment alignment; if (WebVttParser.TryGetAlignmentSetting(Constants.AlignName, parsedSettings, out alignment)) { result.Alignment = alignment; } string name; if (WebVttParser.TryGetStringSetting(Constants.RegionName, parsedSettings, out name)) { result.Region = name; } } } } while (false == string.IsNullOrEmpty(line)) { line = await reader.ReadLineAsync() .ConfigureAwait(false); if (false == string.IsNullOrEmpty(line)) { if (line.Contains(Constants.ArrowToken)) { throw new InvalidDataException(string.Format("Cue must not contain '{0}'.", Constants.ArrowToken)); } content.SafeAppendLine(line); } } if (content.Length > 0) { result.RawContent = content.ToString(); List <Span> spans; position = 0; if (WebVttParser.TryParseSpans(result.RawContent, ref position, out spans) && spans != null && spans.Count > 0) { result.Content = spans.ToArray(); } } return(result); }