├── WebMParser ├── Images │ └── icon-128.png ├── BinaryElement.cs ├── DateElement.cs ├── UnknownElement.cs ├── SimpleBlockElement.cs ├── StringElement.cs ├── ByteSegment.cs ├── StreamSegment.cs ├── UintElement.cs ├── IntElement.cs ├── SpawnDev.WebMParser.csproj ├── TrackEntryElement.cs ├── EnumerableExtensions.cs ├── FloatElement.cs ├── SegmentSource.cs ├── StreamExtensions.cs ├── ElementId.cs ├── WebMParser.cs ├── ContainerElement.cs └── WebMElement.cs ├── WebMParserDemo ├── WebMParserDemo.csproj └── Program.cs ├── LICENSE.txt ├── WebMParser.sln ├── .gitattributes ├── README.md └── .gitignore /WebMParser/Images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/WebMParser/main/WebMParser/Images/icon-128.png -------------------------------------------------------------------------------- /WebMParser/BinaryElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class BinaryElement : WebMElement 4 | { 5 | public BinaryElement(ElementId id) : base(id) { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /WebMParser/DateElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class DateElement : WebMElement 4 | { 5 | public DateElement(ElementId id) : base(id) { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /WebMParser/UnknownElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class UnknownElement : WebMElement 4 | { 5 | public UnknownElement(ElementId id) : base(id) { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /WebMParserDemo/WebMParserDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /WebMParser/SimpleBlockElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class SimpleBlockElement : BinaryElement 4 | { 5 | public SimpleBlockElement(ElementId id) : base(id) { } 6 | public byte TrackId => (byte)(Stream!.ReadByteOrThrow(0) & ~0x80); 7 | public uint Timecode => BitConverter.ToUInt16(Stream!.ReadBytes(1, 2).Reverse().ToArray()); 8 | public override string ToString() => $"{Index} {Id} - IdChain: [ {string.Join(" ", IdChain.ToArray())} ] Type: {this.GetType().Name} Length: {Length} bytes TrackId: {TrackId} Timecode: {Timecode}"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /WebMParser/StringElement.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace SpawnDev.WebMParser 4 | { 5 | public class StringElement : WebMElement 6 | { 7 | public static explicit operator string?(StringElement? element) => element == null ? null : element.Data; 8 | public StringElement(ElementId id) : base(id) { } 9 | public Encoding Encoding { get; set; } = Encoding.UTF8; 10 | public StringElement(ElementId id, string value) : base(id) 11 | { 12 | Data = value; 13 | } 14 | public override void UpdateBySource() 15 | { 16 | Data = Encoding.GetString(Stream!.ReadBytes()); 17 | } 18 | public override void UpdateByData() 19 | { 20 | Stream = new ByteSegment(Encoding.GetBytes(Data)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WebMParser/ByteSegment.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class ByteSegment : SegmentSource 4 | { 5 | #region Constructors 6 | public ByteSegment(byte[] source, long offset, long size, bool ownsSource = false) : base(source, offset, size, ownsSource) 7 | { 8 | } 9 | public ByteSegment(byte[] source, long size, bool ownsSource = false) : base(source, 0, size, ownsSource) 10 | { 11 | } 12 | public ByteSegment(byte[] source, bool ownsSource = false) : base(source, 0, source.Length, ownsSource) 13 | { 14 | } 15 | #endregion 16 | public override int Read(byte[] buffer, int offset, int count) 17 | { 18 | var bytesLeftInSegment = Length - Position; 19 | count = (int)Math.Min(count, bytesLeftInSegment); 20 | if (count <= 0) return 0; 21 | Array.Copy(Source, SourcePosition, buffer, offset, count); 22 | SourcePosition += count; 23 | return count; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WebMParser/StreamSegment.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class StreamSegment : SegmentSource 4 | { 5 | #region Constructors 6 | public StreamSegment(Stream source, long offset, long size, bool ownsSource = false) : base(source, offset, size, ownsSource) 7 | { 8 | } 9 | public StreamSegment(Stream source, long size, bool ownsSource = false) : base(source, source.Position, size, ownsSource) 10 | { 11 | } 12 | public StreamSegment(Stream source, bool ownsSource = false) : base(source, source.Position, source.Length - source.Position, ownsSource) 13 | { 14 | } 15 | #endregion 16 | protected override long SourcePosition { get => Source.Position; set => Source.Position = value; } 17 | public override int Read(byte[] buffer, int offset, int count) 18 | { 19 | var bytesLeftInSegment = Length - Position; 20 | count = (int)Math.Min(count, bytesLeftInSegment); 21 | if (count <= 0) return 0; 22 | return Source.Read(buffer, offset, count); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /WebMParser/UintElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class UintElement : WebMElement 4 | { 5 | public static explicit operator ulong(UintElement? element) => element == null ? 0 : element.Data; 6 | public static explicit operator ulong?(UintElement? element) => element == null ? default : element.Data; 7 | public UintElement(ElementId id) : base(id) { } 8 | public UintElement(ElementId id, ulong value) : base(id) 9 | { 10 | Data = value; 11 | } 12 | public override void UpdateBySource() 13 | { 14 | // switch endianness and pad to 64bit 15 | var bytes = Stream!.ReadBytes().Reverse().ToList(); 16 | while(bytes.Count < 8) bytes.Add(0); 17 | Data = BitConverter.ToUInt64(bytes.ToArray()); 18 | } 19 | public override void UpdateByData() 20 | { 21 | // switch endianness and remove preceding 0 bytes 22 | var bytes = BitConverter.GetBytes(Data).Reverse().ToList(); 23 | while(bytes.Count > 1 && bytes[0] == 0) bytes.RemoveAt(0); 24 | Stream = new ByteSegment(bytes.ToArray()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /WebMParser/IntElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class IntElement : WebMElement 4 | { 5 | public static explicit operator long(IntElement? element) => element == null ? 0 : element.Data; 6 | public static explicit operator long?(IntElement? element) => element == null ? null : element.Data; 7 | int DataSize = 4; 8 | public IntElement(ElementId id) : base(id) { } 9 | public IntElement(ElementId id, long value) : base(id) 10 | { 11 | Data = value; 12 | } 13 | public override void UpdateBySource() 14 | { 15 | // switch endianness and pad to 64bit 16 | var bytes = Stream!.ReadBytes().Reverse().ToList(); 17 | while (bytes.Count < 8) bytes.Add(0); 18 | Data = BitConverter.ToInt64(bytes.ToArray()); 19 | } 20 | public override void UpdateByData() 21 | { 22 | // switch endianness and remove preceding 0 bytes 23 | var bytes = BitConverter.GetBytes(Data).Reverse().ToList(); 24 | while (bytes.Count > 1 && bytes[0] == 0) bytes.RemoveAt(0); 25 | Stream = new ByteSegment(bytes.ToArray()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WebMParser/SpawnDev.WebMParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 1.0.1 8 | True 9 | true 10 | true 11 | Embedded 12 | SpawnDev.WebMParser 13 | LostBeard 14 | .Net WebM parser and editor. Supports duration fixing. 15 | https://github.com/LostBeard/WebMParser 16 | README.md 17 | LICENSE.txt 18 | icon-128.png 19 | https://github.com/LostBeard/WebMParser.git 20 | git 21 | WebM;DotNet 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | True 31 | \ 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /WebMParser/TrackEntryElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SpawnDev.WebMParser 8 | { 9 | public enum TrackType : byte 10 | { 11 | Video = 1, 12 | Audio = 2, 13 | Complex = 3, 14 | Logo = 0x10, 15 | Subtitle = 0x11, 16 | Buttons = 0x12, 17 | Control = 0x20, 18 | } 19 | public class TrackEntryElement : ContainerElement 20 | { 21 | public TrackEntryElement(ElementId id) : base(id) 22 | { 23 | } 24 | public byte TrackNumber 25 | { 26 | get => (byte)(ulong)GetElement(ElementId.TrackNumber); 27 | } 28 | public byte TrackUID 29 | { 30 | get => (byte)(ulong)GetElement(ElementId.TrackUID); 31 | } 32 | public TrackType TrackType 33 | { 34 | get => (TrackType)(byte)(ulong)GetElement(ElementId.TrackType); 35 | } 36 | public string CodecID 37 | { 38 | get => (string)GetElement(ElementId.CodecID)!; 39 | } 40 | public string Language 41 | { 42 | get => (string)GetElement(ElementId.Language)!; 43 | } 44 | public ulong DefaultDuration 45 | { 46 | get => (ulong)GetElement(ElementId.DefaultDuration); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WebMParser/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SpawnDev.WebMParser 8 | { 9 | public static class EnumerableExtensions 10 | { 11 | /// 12 | /// Returns true if the second collection is has at least 1 element and the source collection ends with the second collection 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static bool SequenceEndsWith(this IEnumerable first, IEnumerable second) 19 | { 20 | if (first == null || second == null) return false; 21 | var firstCount = first.Count(); 22 | var secondCount = second.Count(); 23 | if (secondCount == 0 || firstCount == 0 || secondCount > firstCount) return false; 24 | var startIndex = firstCount - secondCount; 25 | for (var i = 0; i < secondCount; i++) 26 | { 27 | var firstValue = first.ElementAt(i + startIndex); 28 | var secondValue = second.ElementAt(i); 29 | var isEqual = EqualityComparer.Default.Equals(firstValue, secondValue); 30 | if (!isEqual) return false; 31 | } 32 | return true; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WebMParser/FloatElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class FloatElement : WebMElement 4 | { 5 | public static explicit operator double(FloatElement? element) => element == null ? 0 : element.Data; 6 | public static explicit operator double?(FloatElement? element) => element == null ? null : element.Data; 7 | int DataSize = 4; 8 | public FloatElement(ElementId id) : base(id) { } 9 | public FloatElement(ElementId id, float value) : base(id) 10 | { 11 | DataSize = 4; 12 | Data = value; 13 | } 14 | public FloatElement(ElementId id, double value) : base(id) 15 | { 16 | DataSize = 8; 17 | Data = value; 18 | } 19 | public override void UpdateBySource() 20 | { 21 | DataSize = (int)Stream!.Length; 22 | var source = Stream!.ReadBytes().Reverse().ToArray(); 23 | if (DataSize == 4) 24 | { 25 | Data = BitConverter.ToSingle(source); 26 | } 27 | else if (DataSize == 8) 28 | { 29 | Data = BitConverter.ToDouble(source); 30 | } 31 | } 32 | public override void UpdateByData() 33 | { 34 | if (DataSize == 4) 35 | { 36 | var bytes = BitConverter.GetBytes((float)Data).Reverse().ToArray(); 37 | Stream = new ByteSegment(bytes); 38 | } 39 | else if (DataSize == 8) 40 | { 41 | var bytes = BitConverter.GetBytes(Data).Reverse().ToArray(); 42 | Stream = new ByteSegment(bytes); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /WebMParser.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34407.89 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebMParserDemo", "WebMParserDemo\WebMParserDemo.csproj", "{02F07EB6-1441-4290-A5BE-3136BF22A9F0}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A7588CB-12DD-4585-B8BA-C2667020FFFC}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SpawnDev.WebMParser", "WebMParser\SpawnDev.WebMParser.csproj", "{D2AED413-57A0-47CC-A617-C89EBF8AC291}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {02F07EB6-1441-4290-A5BE-3136BF22A9F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {02F07EB6-1441-4290-A5BE-3136BF22A9F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {02F07EB6-1441-4290-A5BE-3136BF22A9F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {02F07EB6-1441-4290-A5BE-3136BF22A9F0}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {D2AED413-57A0-47CC-A617-C89EBF8AC291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {D2AED413-57A0-47CC-A617-C89EBF8AC291}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {D2AED413-57A0-47CC-A617-C89EBF8AC291}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {D2AED413-57A0-47CC-A617-C89EBF8AC291}.Release|Any CPU.Build.0 = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ExtensibilityGlobals) = postSolution 31 | SolutionGuid = {B3E20B88-03C4-4F8F-A43A-84F7EDDC0184} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /WebMParser/SegmentSource.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public abstract class SegmentSource : Stream 4 | { 5 | /// 6 | /// The underlying source of the segment 7 | /// 8 | public virtual object SourceObject { get; protected set; } 9 | /// 10 | /// Segment start position in Source 11 | /// 12 | public virtual long Offset { get; init; } 13 | /// 14 | /// Whether this SegmentSource owns the underlying source object 15 | /// 16 | public virtual bool OwnsSource { get; init; } 17 | /// 18 | /// Segment size in bytes. 19 | /// 20 | protected virtual long Size { get; init; } 21 | protected virtual long SourcePosition { get; set; } 22 | // Stream 23 | public override long Length => Size; 24 | public override bool CanRead => SourceObject != null; 25 | public override bool CanSeek => SourceObject != null; 26 | public override bool CanWrite => false; 27 | public override bool CanTimeout => false; 28 | public override long Position { get => SourcePosition - Offset; set => SourcePosition = value + Offset; } 29 | public override void Flush() { } 30 | public override void SetLength(long value) => throw new NotImplementedException(); 31 | public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); 32 | public SegmentSource(long offset, long size, bool ownsSource) 33 | { 34 | Offset = offset; 35 | Size = size; 36 | OwnsSource = ownsSource; 37 | } 38 | public override long Seek(long offset, SeekOrigin origin) 39 | { 40 | switch (origin) 41 | { 42 | case SeekOrigin.Begin: 43 | Position = offset; 44 | break; 45 | case SeekOrigin.End: 46 | Position = Length + offset; 47 | break; 48 | case SeekOrigin.Current: 49 | Position = Position + offset; 50 | break; 51 | } 52 | return Position; 53 | } 54 | public virtual SegmentSource Slice(long offset, long size) 55 | { 56 | var slice = (SegmentSource)Activator.CreateInstance(this.GetType(), SourceObject, offset, size, OwnsSource)!; 57 | return slice; 58 | } 59 | public SegmentSource Slice(long size) => Slice(SourcePosition, size); 60 | public override void CopyTo(Stream destination, int bufferSize) 61 | { 62 | base.CopyTo(destination, bufferSize); 63 | } 64 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) 65 | { 66 | return base.CopyToAsync(destination, bufferSize, cancellationToken); 67 | } 68 | } 69 | public abstract class SegmentSource : SegmentSource 70 | { 71 | public T Source { get; private set; } 72 | public SegmentSource(T source, long offset, long size, bool ownsSource) : base(offset, size, ownsSource) 73 | { 74 | Source = source; 75 | SourceObject = source!; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpawnDev.WebMParser 2 | 3 | | Name | Package | Description | 4 | |---------|-------------|-------------| 5 | |**[SpawnDev.WebMParser](#webmparser)**|[![NuGet version](https://badge.fury.io/nu/SpawnDev.WebMParser.svg)](https://www.nuget.org/packages/SpawnDev.WebMParser)| .Net WebM parser and editor | 6 | 7 | WebMParser is a .Net WebM parser written in C#. 8 | 9 | The initial goal of this library is to allow adding a duration to WebM video files recorded using [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) in a web browser. I am using this library in a browser based Blazor WebAssembly video messaging application. 10 | 11 | The demo project included with the library is a .Net core 8 console app that currently just allows testing the library. 12 | 13 | To fix the duration in a WebM file, WebM parser reads the Timecode information from Clusters and SimpleBlocks and adds a Segment > Info > Duration element with the new duration. 14 | 15 | Example of how to add Duration info if not found in a the WebM stream. 16 | ```cs 17 | using var inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read); 18 | 19 | var webm = new WebMStreamParser(inputStream); 20 | 21 | // FixDuration returns true if the WebM was modified 22 | var modified = webm.FixDuration(); 23 | // webm.Modified will also be true if the WebM is modified 24 | if (modified) 25 | { 26 | var outFile = Path.Combine(Path.GetDirectoryName(inputFile)!, Path.GetFileNameWithoutExtension(inputFile) + ".fixed" + Path.GetExtension(inputFile)); 27 | using var outputStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None); 28 | webm.CopyTo(outputStream); 29 | } 30 | ``` 31 | 32 | Example that prints out basic info for every element found in the WebM stream 33 | ```cs 34 | var elements = webm.Descendants; 35 | foreach (var element in elements) 36 | { 37 | var indent = new string('-', element.IdChain.Length - 1); 38 | Console.WriteLine($"{indent}{element}"); 39 | } 40 | ``` 41 | 42 | 43 | 44 | 45 | Example of how to get an element 46 | ```cs 47 | var durationElement = webm.GetElement(ElementId.Segment, ElementId.Info, ElementId.Duration); 48 | var duration = durationElement?.Data ?? 0; 49 | ``` 50 | 51 | Example of how to get all elements of a type 52 | ```cs 53 | var segments = webm.GetElements(ElementId.Segment); 54 | ``` 55 | 56 | Example of how to use ElementIds to walk the data tree and access information 57 | ```cs 58 | var segments = webm.GetContainers(ElementId.Segment); 59 | foreach (var segment in segments) 60 | { 61 | var clusters = segment.GetContainers(ElementId.Cluster); 62 | foreach (var cluster in clusters) 63 | { 64 | var timecode = cluster.GetElement(ElementId.Timecode); 65 | if (timecode != null) 66 | { 67 | duration = timecode.Data; 68 | }; 69 | var simpleBlocks = cluster.GetElements(ElementId.SimpleBlock); 70 | var simpleBlockLast = simpleBlocks.LastOrDefault(); 71 | if (simpleBlockLast != null) 72 | { 73 | duration += simpleBlockLast.Timecode; 74 | } 75 | } 76 | } 77 | ``` 78 | 79 | Example of how to add an element 80 | All parent containers are automatically marked Modified if any children are added, removed, or changed. 81 | ```cs 82 | var info = GetContainer(ElementId.Segment, ElementId.Info); 83 | info!.Add(ElementId.Duration, 100000); 84 | ``` -------------------------------------------------------------------------------- /WebMParserDemo/Program.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.WebMParser; 2 | 3 | namespace WebMParserDemo 4 | { 5 | internal class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | var inputFile = ""; 10 | var verbose = 1; 11 | var fixDuration = true; 12 | #if DEBUG 13 | var videoFolder = @"C:\Users\TJ\.SpawnDev.AccountsServer\messages"; 14 | inputFile = Path.Combine(videoFolder, "test.webm"); 15 | //inputFile = Path.Combine(videoFolder, "56135218-d984-4b18-96a8-f81e830da98f.webm"); 16 | inputFile = Path.Combine(videoFolder, "Big_Buck_Bunny_4K.webm.480p.vp9.webm"); 17 | #endif 18 | // 19 | var inputFileBaseName = Path.GetFileName(inputFile); 20 | Console.WriteLine($"Input: {inputFileBaseName}"); 21 | // 22 | using var inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read); 23 | var webm = new WebMStreamParser(inputStream); 24 | var tracks = webm.GetElements(ElementId.TrackEntry); 25 | // 26 | if (verbose >= 1) 27 | { 28 | Console.WriteLine($"Duration: {webm.Duration.ToString() ?? "-"}"); 29 | if (webm.HasAudio) 30 | { 31 | Console.WriteLine($"-- Audio --"); 32 | Console.WriteLine($"AudioCodecID: {webm.AudioCodecID}"); 33 | Console.WriteLine($"AudioSamplingFrequency: {webm.AudioSamplingFrequency}"); 34 | Console.WriteLine($"AudioBitDepth: {webm.AudioBitDepth}"); 35 | Console.WriteLine($"AudioChannels: {webm.AudioChannels}"); 36 | } 37 | else 38 | { 39 | Console.WriteLine($"-- Audio --"); 40 | } 41 | if (webm.HasVideo) 42 | { 43 | Console.WriteLine($"-- Video --"); 44 | Console.WriteLine($"VideoCodecID: {webm.VideoCodecID}"); 45 | Console.WriteLine($"VideoSize: {webm.VideoPixelWidth}x{webm.VideoPixelHeight}"); 46 | } 47 | else 48 | { 49 | Console.WriteLine($"-- Video --"); 50 | } 51 | } 52 | if (verbose >= 2) 53 | { 54 | // Display verbose element output 55 | var elements = webm.Descendants; 56 | foreach (var element in elements) 57 | { 58 | var indent = new string('-', element.IdChain.Length - 1); 59 | var elementStr = element.ToString(); 60 | if (elementStr.Contains("SimpleBlock")) continue; 61 | Console.WriteLine($"{indent}{elementStr}"); 62 | } 63 | } 64 | // 65 | if (fixDuration) 66 | { 67 | // Fix duration if not present 68 | var modified = webm.FixDuration(); 69 | // webm.DataChanged will also be true if the WebM is modified 70 | if (modified) 71 | { 72 | var outFile = Path.Combine(Path.GetDirectoryName(inputFile)!, Path.GetFileNameWithoutExtension(inputFile) + ".fixed" + Path.GetExtension(inputFile)); 73 | using var outputStream = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None); 74 | webm.CopyTo(outputStream); 75 | } 76 | } 77 | #if DEBUG 78 | Console.ReadLine(); 79 | #endif 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /WebMParser/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public static class StreamExtensions 4 | { 5 | /// 6 | /// Returns the maximum number of bytes that can be read starting from the given offset
7 | /// If the offset >= length or offset < 0 or !CanRead, 0 is returned 8 | ///
9 | /// 10 | /// 11 | /// 12 | public static long MaxReadableCount(this Stream _this, long offset) 13 | { 14 | if (!_this.CanRead || offset < 0 || offset >= _this.Length || _this.Length == 0) return 0; 15 | return _this.Length - offset; 16 | } 17 | public static long MaxReadableCount(this Stream _this) => _this.MaxReadableCount(_this.Position); 18 | 19 | public static long GetReadableCount(this Stream _this, long maxCount) 20 | { 21 | return _this.GetReadableCount(_this.Position, maxCount); 22 | } 23 | public static long GetReadableCount(this Stream _this, long offset, long maxCount) 24 | { 25 | if (maxCount <= 0) return 0; 26 | var bytesLeft = _this.MaxReadableCount(offset); 27 | return Math.Max(bytesLeft, maxCount); 28 | } 29 | public static byte[] ReadBytes(this Stream _this) 30 | { 31 | var readCount = _this.MaxReadableCount(); 32 | var bytes = new byte[readCount]; 33 | _this.Read(bytes, 0, bytes.Length); 34 | return bytes; 35 | } 36 | public static byte[] ReadBytes(this Stream _this, long count, bool requireCountExact = false) 37 | { 38 | var readCount = _this.GetReadableCount(count); 39 | if (readCount != count && requireCountExact) throw new Exception("Not available"); 40 | var bytes = new byte[readCount]; 41 | if (readCount == 0) return bytes; 42 | _this.Read(bytes, 0, bytes.Length); 43 | return bytes; 44 | } 45 | public static byte[] ReadBytes(this Stream _this, long offset, long count, bool requireCountExact = false) 46 | { 47 | var origPosition = _this.Position; 48 | _this.Position = offset; 49 | try 50 | { 51 | var readCount = _this.GetReadableCount(offset, count); 52 | if (readCount != count && requireCountExact) throw new Exception("Not available"); 53 | var bytes = new byte[readCount]; 54 | if (readCount == 0) return bytes; 55 | _this.Read(bytes, 0, bytes.Length); 56 | return bytes; 57 | } 58 | finally 59 | { 60 | _this.Position = origPosition; 61 | } 62 | } 63 | public static int ReadByte(this Stream _this, long offset) 64 | { 65 | var origPosition = _this.Position; 66 | _this.Position = offset; 67 | try 68 | { 69 | var ret = _this.ReadByte(); 70 | return ret; 71 | } 72 | finally 73 | { 74 | _this.Position = origPosition; 75 | } 76 | } 77 | public static int ReadByteOrThrow(this Stream _this, long offset) 78 | { 79 | var origPosition = _this.Position; 80 | _this.Position = offset; 81 | try 82 | { 83 | var ret = _this.ReadByte(); 84 | if (ret == -1) throw new EndOfStreamException(); 85 | return ret; 86 | } 87 | finally 88 | { 89 | _this.Position = origPosition; 90 | } 91 | } 92 | public static int ReadByteOrThrow(this Stream _this) 93 | { 94 | var ret = _this.ReadByte(); 95 | if (ret == -1) throw new EndOfStreamException(); 96 | return ret; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /WebMParser/ElementId.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public enum ElementId : uint 4 | { 5 | File = uint.MaxValue, 6 | EBML = 0xa45dfa3, 7 | EBMLVersion = 0x286, 8 | EBMLReadVersion = 0x2f7, 9 | EBMLMaxIDLength = 0x2f2, 10 | EBMLMaxSizeLength = 0x2f3, 11 | DocType = 0x282, 12 | DocTypeVersion = 0x287, 13 | DocTypeReadVersion = 0x285, 14 | Void = 0x6c, 15 | CRC32 = 0x3f, 16 | SignatureSlot = 0xb538667, 17 | SignatureAlgo = 0x3e8a, 18 | SignatureHash = 0x3e9a, 19 | SignaturePublicKey = 0x3ea5, 20 | Signature = 0x3eb5, 21 | SignatureElements = 0x3e5b, 22 | SignatureElementList = 0x3e7b, 23 | SignedElement = 0x2532, 24 | Segment = 0x8538067, 25 | SeekHead = 0x14d9b74, 26 | Seek = 0xdbb, 27 | SeekID = 0x13ab, 28 | SeekPosition = 0x13ac, 29 | Info = 0x549a966, 30 | SegmentUID = 0x33a4, 31 | SegmentFilename = 0x3384, 32 | PrevUID = 0x1cb923, 33 | PrevFilename = 0x1c83ab, 34 | NextUID = 0x1eb923, 35 | NextFilename = 0x1e83bb, 36 | SegmentFamily = 0x444, 37 | ChapterTranslate = 0x2924, 38 | ChapterTranslateEditionUID = 0x29fc, 39 | ChapterTranslateCodec = 0x29bf, 40 | ChapterTranslateID = 0x29a5, 41 | TimecodeScale = 0xad7b1, 42 | Duration = 0x489, 43 | DateUTC = 0x461, 44 | Title = 0x3ba9, 45 | MuxingApp = 0xd80, 46 | WritingApp = 0x1741, 47 | Cluster = 0xf43b675, 48 | Timecode = 0x67, 49 | SilentTracks = 0x1854, 50 | SilentTrackNumber = 0x18d7, 51 | Position = 0x27, 52 | PrevSize = 0x2b, 53 | SimpleBlock = 0x23, 54 | BlockGroup = 0x20, 55 | Block = 0x21, 56 | BlockVirtual = 0x22, 57 | BlockAdditions = 0x35a1, 58 | BlockMore = 0x26, 59 | BlockAddID = 0x6e, 60 | BlockAdditional = 0x25, 61 | BlockDuration = 0x1b, 62 | ReferencePriority = 0x7a, 63 | ReferenceBlock = 0x7b, 64 | ReferenceVirtual = 0x7d, 65 | CodecState = 0x24, 66 | DiscardPadding = 0x35a2, 67 | Slices = 0xe, 68 | TimeSlice = 0x68, 69 | LaceNumber = 0x4c, 70 | FrameNumber = 0x4d, 71 | BlockAdditionID = 0x4b, 72 | Delay = 0x4e, 73 | SliceDuration = 0x4f, 74 | ReferenceFrame = 0x48, 75 | ReferenceOffset = 0x49, 76 | ReferenceTimeCode = 0x4a, 77 | EncryptedBlock = 0x2f, 78 | Tracks = 0x654ae6b, 79 | TrackEntry = 0x2e, 80 | TrackNumber = 0x57, 81 | TrackUID = 0x33c5, 82 | TrackType = 0x3, 83 | FlagEnabled = 0x39, 84 | FlagDefault = 0x8, 85 | FlagForced = 0x15aa, 86 | FlagLacing = 0x1c, 87 | MinCache = 0x2de7, 88 | MaxCache = 0x2df8, 89 | DefaultDuration = 0x3e383, 90 | DefaultDecodedFieldDuration = 0x34e7a, 91 | TrackTimecodeScale = 0x3314f, 92 | TrackOffset = 0x137f, 93 | MaxBlockAdditionID = 0x15ee, 94 | Name = 0x136e, 95 | Language = 0x2b59c, 96 | CodecID = 0x6, 97 | CodecPrivate = 0x23a2, 98 | CodecName = 0x58688, 99 | AttachmentLink = 0x3446, 100 | CodecSettings = 0x1a9697, 101 | CodecInfoURL = 0x1b4040, 102 | CodecDownloadURL = 0x6b240, 103 | CodecDecodeAll = 0x2a, 104 | TrackOverlay = 0x2fab, 105 | CodecDelay = 0x16aa, 106 | SeekPreRoll = 0x16bb, 107 | TrackTranslate = 0x2624, 108 | TrackTranslateEditionUID = 0x26fc, 109 | TrackTranslateCodec = 0x26bf, 110 | TrackTranslateTrackID = 0x26a5, 111 | Video = 0x60, 112 | FlagInterlaced = 0x1a, 113 | StereoMode = 0x13b8, 114 | AlphaMode = 0x13c0, 115 | OldStereoMode = 0x13b9, 116 | PixelWidth = 0x30, 117 | PixelHeight = 0x3a, 118 | PixelCropBottom = 0x14aa, 119 | PixelCropTop = 0x14bb, 120 | PixelCropLeft = 0x14cc, 121 | PixelCropRight = 0x14dd, 122 | DisplayWidth = 0x14b0, 123 | DisplayHeight = 0x14ba, 124 | DisplayUnit = 0x14b2, 125 | AspectRatioType = 0x14b3, 126 | ColourSpace = 0xeb524, 127 | GammaValue = 0xfb523, 128 | FrameRate = 0x383e3, 129 | Audio = 0x61, 130 | SamplingFrequency = 0x35, 131 | OutputSamplingFrequency = 0x38b5, 132 | Channels = 0x1f, 133 | ChannelPositions = 0x3d7b, 134 | BitDepth = 0x2264, 135 | TrackOperation = 0x62, 136 | TrackCombinePlanes = 0x63, 137 | TrackPlane = 0x64, 138 | TrackPlaneUID = 0x65, 139 | TrackPlaneType = 0x66, 140 | TrackJoinBlocks = 0x69, 141 | TrackJoinUID = 0x6d, 142 | TrickTrackUID = 0x40, 143 | TrickTrackSegmentUID = 0x41, 144 | TrickTrackFlag = 0x46, 145 | TrickMasterTrackUID = 0x47, 146 | TrickMasterTrackSegmentUID = 0x44, 147 | ContentEncodings = 0x2d80, 148 | ContentEncoding = 0x2240, 149 | ContentEncodingOrder = 0x1031, 150 | ContentEncodingScope = 0x1032, 151 | ContentEncodingType = 0x1033, 152 | ContentCompression = 0x1034, 153 | ContentCompAlgo = 0x254, 154 | ContentCompSettings = 0x255, 155 | ContentEncryption = 0x1035, 156 | ContentEncAlgo = 0x7e1, 157 | ContentEncKeyID = 0x7e2, 158 | ContentSignature = 0x7e3, 159 | ContentSigKeyID = 0x7e4, 160 | ContentSigAlgo = 0x7e5, 161 | ContentSigHashAlgo = 0x7e6, 162 | Cues = 0xc53bb6b, 163 | CuePoint = 0x3b, 164 | CueTime = 0x33, 165 | CueTrackPositions = 0x37, 166 | CueTrack = 0x77, 167 | CueClusterPosition = 0x71, 168 | CueRelativePosition = 0x70, 169 | CueDuration = 0x32, 170 | CueBlockNumber = 0x1378, 171 | CueCodecState = 0x6a, 172 | CueReference = 0x5b, 173 | CueRefTime = 0x16, 174 | CueRefCluster = 0x17, 175 | CueRefNumber = 0x135f, 176 | CueRefCodecState = 0x6b, 177 | Attachments = 0x941a469, 178 | AttachedFile = 0x21a7, 179 | FileDescription = 0x67e, 180 | FileName = 0x66e, 181 | FileMimeType = 0x660, 182 | FileData = 0x65c, 183 | FileUID = 0x6ae, 184 | FileReferral = 0x675, 185 | FileUsedStartTime = 0x661, 186 | FileUsedEndTime = 0x662, 187 | Chapters = 0x43a770, 188 | EditionEntry = 0x5b9, 189 | EditionUID = 0x5bc, 190 | EditionFlagHidden = 0x5bd, 191 | EditionFlagDefault = 0x5db, 192 | EditionFlagOrdered = 0x5dd, 193 | ChapterAtom = 0x36, 194 | ChapterUID = 0x33c4, 195 | ChapterStringUID = 0x1654, 196 | ChapterTimeStart = 0x11, 197 | ChapterTimeEnd = 0x12, 198 | ChapterFlagHidden = 0x18, 199 | ChapterFlagEnabled = 0x598, 200 | ChapterSegmentUID = 0x2e67, 201 | ChapterSegmentEditionUID = 0x2ebc, 202 | ChapterPhysicalEquiv = 0x23c3, 203 | ChapterTrack = 0xf, 204 | ChapterTrackNumber = 0x9, 205 | ChapterDisplay = 0x0, 206 | ChapString = 0x5, 207 | ChapLanguage = 0x37c, 208 | ChapCountry = 0x37e, 209 | ChapProcess = 0x2944, 210 | ChapProcessCodecID = 0x2955, 211 | ChapProcessPrivate = 0x50d, 212 | ChapProcessCommand = 0x2911, 213 | ChapProcessTime = 0x2922, 214 | ChapProcessData = 0x2933, 215 | Tags = 0x254c367, 216 | Tag = 0x3373, 217 | Targets = 0x23c0, 218 | TargetTypeValue = 0x28ca, 219 | TargetType = 0x23ca, 220 | TagTrackUID = 0x23c5, 221 | TagEditionUID = 0x23c9, 222 | TagChapterUID = 0x23c4, 223 | TagAttachmentUID = 0x23c6, 224 | SimpleTag = 0x27c8, 225 | TagName = 0x5a3, 226 | TagLanguage = 0x47a, 227 | TagDefault = 0x484, 228 | TagString = 0x487, 229 | TagBinary = 0x485, 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /WebMParser/WebMParser.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | public class WebMStreamParser : ContainerElement 4 | { 5 | public WebMStreamParser(Stream? stream = null) : base(ElementId.File) 6 | { 7 | if (stream != null) 8 | { 9 | if (typeof(SegmentSource).IsAssignableFrom(stream.GetType())) 10 | { 11 | SetSource((SegmentSource)stream); 12 | } 13 | else 14 | { 15 | SetSource(new StreamSegment(stream)); 16 | } 17 | } 18 | } 19 | 20 | /// 21 | /// Get and Set for TimecodeScale from the first segment block 22 | /// 23 | public virtual uint? TimecodeScale 24 | { 25 | get 26 | { 27 | var timecodeScale = GetElement(ElementId.Segment, ElementId.Info, ElementId.TimecodeScale); 28 | return timecodeScale != null ? (uint)timecodeScale.Data : null; 29 | } 30 | set 31 | { 32 | var timecodeScale = GetElement(ElementId.Segment, ElementId.Info, ElementId.TimecodeScale); 33 | if (timecodeScale == null) 34 | { 35 | if (value != null) 36 | { 37 | var info = GetContainer(ElementId.Segment, ElementId.Info); 38 | info!.Add(ElementId.TimecodeScale, value.Value); 39 | } 40 | } 41 | else 42 | { 43 | if (value == null) 44 | { 45 | var info = GetContainer(ElementId.Segment, ElementId.Info); 46 | info!.Remove(timecodeScale); 47 | } 48 | else 49 | { 50 | timecodeScale.Data = value.Value; 51 | } 52 | } 53 | } 54 | } 55 | 56 | string? Title 57 | { 58 | get 59 | { 60 | var title = GetElement(ElementId.Segment, ElementId.Info, ElementId.Title); 61 | return (string?)title; 62 | } 63 | set 64 | { 65 | var title = GetElement(ElementId.Segment, ElementId.Info, ElementId.Title); 66 | if (title == null) 67 | { 68 | if (value != null) 69 | { 70 | var info = GetContainer(ElementId.Segment, ElementId.Info); 71 | info!.Add(ElementId.Title, value); 72 | } 73 | } 74 | else 75 | { 76 | if (value == null) 77 | { 78 | title.Remove(); 79 | } 80 | else 81 | { 82 | title.Data = value; 83 | } 84 | } 85 | } 86 | } 87 | 88 | string? MuxingApp 89 | { 90 | get 91 | { 92 | var docType = GetElement(ElementId.Segment, ElementId.Info, ElementId.MuxingApp); 93 | return docType != null ? docType.Data : null; 94 | } 95 | } 96 | 97 | string? WritingApp 98 | { 99 | get 100 | { 101 | var docType = GetElement(ElementId.Segment, ElementId.Info, ElementId.WritingApp); 102 | return docType != null ? docType.Data : null; 103 | } 104 | } 105 | 106 | string? EBMLDocType 107 | { 108 | get 109 | { 110 | var docType = GetElement(ElementId.EBML, ElementId.DocType); 111 | return docType != null ? docType.Data : null; 112 | } 113 | } 114 | 115 | /// 116 | /// Returns true if audio tracks exist 117 | /// 118 | public virtual bool HasAudio => GetElements(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry).Where(o => o.TrackType == TrackType.Audio).Any(); 119 | 120 | public virtual uint? AudioChannels 121 | { 122 | get 123 | { 124 | var channels = GetElement(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry, ElementId.Audio, ElementId.Channels); 125 | return channels != null ? (uint)channels : null; 126 | } 127 | } 128 | public virtual double? AudioSamplingFrequency 129 | { 130 | get 131 | { 132 | var samplingFrequency = GetElement(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry, ElementId.Audio, ElementId.SamplingFrequency); 133 | return samplingFrequency != null ? (double)samplingFrequency : null; 134 | } 135 | } 136 | public virtual uint? AudioBitDepth 137 | { 138 | get 139 | { 140 | var bitDepth = GetElement(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry, ElementId.Audio, ElementId.BitDepth); 141 | return bitDepth != null ? (uint)bitDepth : null; 142 | } 143 | } 144 | 145 | 146 | /// 147 | /// Returns true if video tracks exist 148 | /// 149 | public virtual bool HasVideo => GetElements(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry).Where(o => o.TrackType == TrackType.Video).Any(); 150 | 151 | public virtual string VideoCodecID => GetElements(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry).Where(o => o.TrackType == TrackType.Video).FirstOrDefault()?.CodecID ?? ""; 152 | 153 | public virtual string AudioCodecID => GetElements(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry).Where(o => o.TrackType == TrackType.Audio).FirstOrDefault()?.CodecID ?? ""; 154 | 155 | public virtual uint? VideoPixelWidth 156 | { 157 | get 158 | { 159 | var pixelWidth = GetElement(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry, ElementId.Video, ElementId.PixelWidth); 160 | return pixelWidth != null ? (uint)pixelWidth : null; 161 | } 162 | } 163 | 164 | public virtual uint? VideoPixelHeight 165 | { 166 | get 167 | { 168 | var pixelHeight = GetElement(ElementId.Segment, ElementId.Tracks, ElementId.TrackEntry, ElementId.Video, ElementId.PixelHeight); 169 | return pixelHeight != null ? (uint)pixelHeight : null; 170 | } 171 | } 172 | 173 | /// 174 | /// Get and Set for the first segment block duration 175 | /// 176 | public virtual double? Duration 177 | { 178 | get 179 | { 180 | var duration = GetElement(ElementId.Segment, ElementId.Info, ElementId.Duration); 181 | return duration != null ? duration.Data : null; 182 | } 183 | set 184 | { 185 | var duration = GetElement(ElementId.Segment, ElementId.Info, ElementId.Duration); 186 | if (duration == null) 187 | { 188 | if (value != null) 189 | { 190 | var info = GetContainer(ElementId.Segment, ElementId.Info); 191 | info!.Add(ElementId.Duration, value.Value); 192 | } 193 | } 194 | else 195 | { 196 | if (value == null) 197 | { 198 | var info = GetContainer(ElementId.Segment, ElementId.Info); 199 | info!.Remove(duration); 200 | } 201 | else 202 | { 203 | duration.Data = value.Value; 204 | } 205 | } 206 | } 207 | } 208 | 209 | /// 210 | /// If the Duration is not set in the first segment block, the duration will be calculated using Cluster and SimpleBlock data and written to Duration 211 | /// 212 | /// 213 | public virtual bool FixDuration() 214 | { 215 | if (Duration == null) 216 | { 217 | var durationEstimate = GetDurationEstimate(); 218 | Duration = durationEstimate; 219 | return true; 220 | } 221 | return false; 222 | } 223 | 224 | /// 225 | /// Duration calculated using Cluster and SimpleBlock data and written to Duration 226 | /// 227 | /// 228 | public virtual double GetDurationEstimate() 229 | { 230 | double duration = 0; 231 | var segments = GetContainers(ElementId.Segment); 232 | foreach (var segment in segments) 233 | { 234 | var clusters = segment.GetContainers(ElementId.Cluster); 235 | foreach (var cluster in clusters) 236 | { 237 | var timecode = cluster.GetElement(ElementId.Timecode); 238 | if (timecode != null) 239 | { 240 | duration = timecode.Data; 241 | }; 242 | var simpleBlocks = cluster.GetElements(ElementId.SimpleBlock); 243 | var simpleBlockLast = simpleBlocks.LastOrDefault(); 244 | if (simpleBlockLast != null) 245 | { 246 | duration += simpleBlockLast.Timecode; 247 | } 248 | } 249 | } 250 | return duration; 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /WebMParser/ContainerElement.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Linq; 3 | using System.Xml.Linq; 4 | 5 | namespace SpawnDev.WebMParser 6 | { 7 | public class ContainerElement : WebMElement> 8 | { 9 | private List __Data = new List(); 10 | public override ReadOnlyCollection Data 11 | { 12 | get => __Data.AsReadOnly(); 13 | set => throw new NotImplementedException(); 14 | } 15 | 16 | public override string ToString() => $"{Index} [ {Id} ] - IdChain: [ {string.Join(" ", IdChain.ToArray())} ] Type: {this.GetType().Name} Length: {Length} bytes Entries: {Data.Count}"; 17 | public ContainerElement(ElementId id) : base(id) { } 18 | public ContainerElement? GetContainer(params ElementId[] ids) => GetElement(ids); 19 | public List GetContainers(params ElementId[] ids) => GetElements(ids); 20 | public bool ElementExists(params ElementId[] idChain) => GetElement(idChain) != null; 21 | public WebMElement? GetElement(params ElementId[] idChain) => GetElement(idChain); 22 | public List GetElements(params ElementId[] idChain) => GetElements(idChain); 23 | public T? GetElement(params ElementId[] idChain) where T : WebMElement => (T?)Descendants.FirstOrDefault(o => o.IdChain.SequenceEndsWith(idChain)); 24 | /// 25 | /// Returns all elements with an IdChain that ends with the provided idChain
26 | ///
27 | /// 28 | /// idChain is an array ending in the desired ElementId types to be returned, with optional preceding parent ElementIds 29 | /// 30 | public List GetElements(params ElementId[] idChain) where T : WebMElement => Descendants.Where(o => o.IdChain.SequenceEndsWith(idChain)).Select(o => (T)o).ToList(); 31 | long CalculatedLength = 0; 32 | /// 33 | /// Returns the byte length of this container 34 | /// 35 | public override long Length => Stream != null ? Stream.Length : CalculatedLength; 36 | private long CalculateLength() 37 | { 38 | long ret = 0; 39 | foreach (var element in Data) 40 | { 41 | var len = element.Length; 42 | ret += len; 43 | var idSize = GetElementIdUintSize(element.Id); 44 | var lenSize = GetContainerUintSize((uint)len); 45 | ret += idSize; 46 | ret += lenSize; 47 | } 48 | return ret; 49 | } 50 | /// 51 | /// Copies the container to the specified stream 52 | /// 53 | /// 54 | /// 55 | /// 56 | public override long CopyTo(Stream stream, int? bufferSize = null) 57 | { 58 | if (!DataChanged && Stream != null) 59 | { 60 | Stream.Position = 0; 61 | if (bufferSize != null) 62 | { 63 | Stream.CopyTo(stream, bufferSize.Value); 64 | } 65 | else 66 | { 67 | Stream.CopyTo(stream); 68 | } 69 | return Length; 70 | } 71 | else 72 | { 73 | foreach (var element in Data) 74 | { 75 | var len = element.Length; 76 | var idBytes = GetElementIdUintBytes(element.Id); 77 | var lenBytes = GetContainerUintBytes((uint)len); 78 | stream.Write(idBytes); 79 | stream.Write(lenBytes); 80 | element.CopyTo(stream, bufferSize); 81 | } 82 | return Length; 83 | } 84 | } 85 | public override void UpdateByData() 86 | { 87 | Stream = null; 88 | CalculatedLength = CalculateLength(); 89 | DataChangedInvoke(); 90 | } 91 | public override void UpdateBySource() 92 | { 93 | foreach (var el in __Data) 94 | { 95 | el.OnDataChanged -= Element_DataChanged; 96 | el.SetParent(); 97 | } 98 | __Data.Clear(); 99 | var elements = new List(); 100 | if (Stream == null) 101 | { 102 | return; 103 | } 104 | while (Stream.Position < Stream.Length) 105 | { 106 | var id = ReadElementId(Stream); 107 | var len = ReadContainerUint(Stream); 108 | var sectionBodyPos = Stream.Position; 109 | var elementType = GetElementType(id); 110 | if (len == UnknownElementSize) 111 | { 112 | if (id == ElementId.Segment) 113 | { 114 | len = FindSegmentLength(Stream); 115 | } 116 | else if (id == ElementId.Cluster) 117 | { 118 | len = FindClusterLength(Stream); 119 | } 120 | else 121 | { 122 | throw new Exception("Invalid data"); 123 | } 124 | } 125 | var bytesLeft = Stream.Length - Stream.Position; 126 | if (len > bytesLeft || len == 0 || len == uint.MaxValue) 127 | { 128 | // invalid section length 129 | break; 130 | } 131 | var slice = Stream.Slice(len); 132 | var element = (WebMElement)Activator.CreateInstance(elementType, id)!; 133 | elements.Add(element); 134 | element.SetParent(this); 135 | element.SetSource(slice); 136 | element.OnDataChanged += Element_DataChanged; 137 | Stream.Position = sectionBodyPos + len; 138 | } 139 | __Data = elements; 140 | } 141 | /// 142 | /// A flat list of all the elements contained by this element and their children 143 | /// 144 | public ReadOnlyCollection Descendants 145 | { 146 | get 147 | { 148 | var ret = new List(); 149 | foreach (var el in __Data) 150 | { 151 | ret.Add(el); 152 | if (el is ContainerElement container) 153 | { 154 | ret.AddRange(container.Descendants); 155 | } 156 | } 157 | return ret.AsReadOnly(); 158 | } 159 | } 160 | /// 161 | /// Adds a FloatElement to this container with the given value 162 | /// 163 | /// 164 | /// 165 | /// 166 | public bool Add(ElementId id, double value) => Add(new FloatElement(id, value)); 167 | /// 168 | /// Adds a FloatElement to this container with the given value 169 | /// 170 | /// 171 | /// 172 | /// 173 | public bool Add(ElementId id, float value) => Add(new FloatElement(id, value)); 174 | /// 175 | /// Adds a StringElement to this container with the given value 176 | /// 177 | /// 178 | /// 179 | /// 180 | public bool Add(ElementId id, string value) => Add(new StringElement(id, value)); 181 | /// 182 | /// Adds a UintElement to this container with the given value 183 | /// 184 | /// 185 | /// 186 | /// 187 | public bool Add(ElementId id, ulong value) => Add(new UintElement(id, value)); 188 | /// 189 | /// Adds an IntElement to this container with the given value 190 | /// 191 | /// 192 | /// 193 | /// 194 | public bool Add(ElementId id, long value) => Add(new IntElement(id, value)); 195 | /// 196 | /// Adds a WebMElement to this container with the given value 197 | /// 198 | /// 199 | /// 200 | public bool Add(WebMElement element) 201 | { 202 | if (__Data.Contains(element)) return false; 203 | __Data.Add(element); 204 | element.SetParent(this); 205 | element.OnDataChanged += Element_DataChanged; 206 | UpdateByData(); 207 | return true; 208 | } 209 | private void Element_DataChanged(WebMElement obj) 210 | { 211 | UpdateByData(); 212 | } 213 | /// 214 | /// Removes a WebElement from this container 215 | /// 216 | /// 217 | /// 218 | public bool Remove(WebMElement element) 219 | { 220 | var succ = __Data.Remove(element); 221 | if (!succ) return false; 222 | element.SetParent(); 223 | element.OnDataChanged -= Element_DataChanged; 224 | UpdateByData(); 225 | return succ; 226 | } 227 | static uint FindSegmentLength(Stream stream) 228 | { 229 | long startOffset = stream.Position; 230 | long pos = stream.Position; 231 | while (pos < stream.Length) 232 | { 233 | pos = stream.Position; 234 | try 235 | { 236 | var id = ReadElementId(stream); 237 | var len = ReadContainerUint(stream); 238 | if (len == uint.MaxValue && id == ElementId.Cluster) 239 | { 240 | len = FindClusterLength(stream); 241 | } 242 | if (!SegmentChildIds.Contains(id)) 243 | { 244 | break; 245 | } 246 | stream.Seek(len, SeekOrigin.Current); 247 | } 248 | catch (Exception ex) 249 | { 250 | break; 251 | } 252 | } 253 | stream.Position = startOffset; 254 | return (uint)(pos - startOffset); 255 | } 256 | static uint FindClusterLength(Stream stream) 257 | { 258 | long startOffset = stream.Position; 259 | long pos = stream.Position; 260 | while (pos < stream.Length) 261 | { 262 | pos = stream.Position; 263 | try 264 | { 265 | var id = ReadElementId(stream); 266 | var len = ReadContainerUint(stream); 267 | var sectionInfo = GetElementType(id); 268 | if (!ClusterChildIds.Contains(id)) 269 | { 270 | break; 271 | } 272 | stream.Seek(len, SeekOrigin.Current); 273 | } 274 | catch (Exception ex) 275 | { 276 | break; 277 | } 278 | } 279 | stream.Position = startOffset; 280 | return (uint)(pos - startOffset); 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /WebMParser/WebMElement.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.WebMParser 2 | { 3 | /// 4 | /// Base WebM element class 5 | /// 6 | public abstract class WebMElement 7 | { 8 | /// 9 | /// The 0 based index of this item in the parent container, or 0 if not in a container 10 | /// 11 | public int Index => Parent == null ? 0 : Parent.Data.IndexOf(this); 12 | /// 13 | /// Returns the parent element this element belongs to, or null if it has no parent 14 | /// 15 | public ContainerElement? Parent { get; private set; } 16 | /// 17 | /// Returns true of this element or any descendant has been modified 18 | /// 19 | public bool DataChanged { get; protected set; } 20 | /// 21 | /// Returns the ElementId of this element 22 | /// 23 | public ElementId Id { get; init; } 24 | /// 25 | /// An array of ElementIds ending with this elements id, preceded by this element's parent's id, and so on 26 | /// 27 | public ElementId[] IdChain { get; protected set; } 28 | /// 29 | /// The segment source of this element 30 | /// 31 | public SegmentSource? Stream { get; protected set; } 32 | /// 33 | /// Returns the size in bytes of this element 34 | /// 35 | public virtual long Length => Stream != null ? Stream.Length : 0; 36 | /// 37 | /// Constructs source less instance with the given element id 38 | /// 39 | /// 40 | public WebMElement(ElementId id) 41 | { 42 | Id = id; 43 | IdChain = Id == ElementId.File ? [] : [Id]; 44 | } 45 | /// 46 | /// Remove this element from its parent 47 | /// 48 | /// Returns true if element has a parent and was successfully removed 49 | public bool Remove() => Parent == null ? false : Parent.Remove(this); 50 | internal void SetParent(ContainerElement? parent = null) 51 | { 52 | Parent = parent; 53 | if (parent != null) 54 | { 55 | var idChain = new List(parent.IdChain); 56 | if (Id != ElementId.File) idChain.Add(Id); 57 | IdChain = idChain.ToArray(); 58 | } 59 | else 60 | { 61 | IdChain = Id == ElementId.File ? [] : [Id]; 62 | } 63 | } 64 | /// 65 | /// Copies the element to a stream 66 | /// 67 | /// 68 | /// 69 | /// 70 | public virtual long CopyTo(Stream stream, int? bufferSize = null) 71 | { 72 | if (Stream == null) return 0; 73 | Stream.Position = 0; 74 | if (bufferSize != null) 75 | { 76 | Stream.CopyTo(stream, bufferSize.Value); 77 | } 78 | else 79 | { 80 | Stream.CopyTo(stream); 81 | } 82 | return Length; 83 | } 84 | /// 85 | /// Should be overridden and internally update WebMBase.Data when called.
86 | ///
87 | public virtual void UpdateBySource() 88 | { 89 | // 90 | } 91 | /// 92 | /// Returns true when updating by source 93 | /// 94 | protected bool UpdatingBySource { get; private set; } = false; 95 | /// 96 | /// Loads the element from the given segment source 97 | /// 98 | /// 99 | public void SetSource(SegmentSource stream) 100 | { 101 | Stream = stream; 102 | UpdatingBySource = true; 103 | UpdateBySource(); 104 | UpdatingBySource = false; 105 | } 106 | /// 107 | /// The element ids that a cluster can contain. Used when detecting the end of a cluster. 108 | /// 109 | public static List ClusterChildIds = new List 110 | { 111 | ElementId.Timecode, 112 | ElementId.Position, 113 | ElementId.PrevSize, 114 | ElementId.SimpleBlock, 115 | ElementId.BlockGroup, 116 | }; 117 | /// 118 | /// The element ids that a segment can contain. Used when detecting the end of a segment. 119 | /// 120 | public static List SegmentChildIds = new List 121 | { 122 | ElementId.SeekHead, 123 | ElementId.Info, 124 | ElementId.Tracks, 125 | ElementId.Chapters, 126 | ElementId.Cluster, 127 | ElementId.Cues, 128 | ElementId.Attachments, 129 | ElementId.Tags, 130 | }; 131 | /// 132 | /// Returns the type that can best represent the element data 133 | /// 134 | /// 135 | /// 136 | public static Type GetElementType(ElementId id) => ElementTypeMap.TryGetValue(id, out var ret) ? ret : typeof(UnknownElement); 137 | /// 138 | /// ElementIds mapped to the type that will be used to represent the specified element 139 | /// 140 | public static Dictionary ElementTypeMap { get; } = new Dictionary 141 | { 142 | { ElementId.EBML, typeof(ContainerElement) }, 143 | { ElementId.EBMLVersion, typeof(UintElement) }, 144 | { ElementId.EBMLReadVersion, typeof(UintElement) }, 145 | { ElementId.EBMLMaxIDLength, typeof(UintElement) }, 146 | { ElementId.EBMLMaxSizeLength, typeof(UintElement) }, 147 | { ElementId.DocType, typeof(StringElement) }, 148 | { ElementId.DocTypeVersion, typeof(UintElement) }, 149 | { ElementId.DocTypeReadVersion, typeof(UintElement) }, 150 | { ElementId.Void, typeof(BinaryElement) }, 151 | { ElementId.CRC32, typeof(BinaryElement) }, 152 | { ElementId.SignatureSlot, typeof(ContainerElement) }, 153 | { ElementId.SignatureAlgo, typeof(UintElement) }, 154 | { ElementId.SignatureHash, typeof(UintElement) }, 155 | { ElementId.SignaturePublicKey, typeof(BinaryElement) }, 156 | { ElementId.Signature, typeof(BinaryElement) }, 157 | { ElementId.SignatureElements, typeof(ContainerElement) }, 158 | { ElementId.SignatureElementList, typeof(ContainerElement) }, 159 | { ElementId.SignedElement, typeof(BinaryElement) }, 160 | { ElementId.Segment, typeof(ContainerElement) }, 161 | { ElementId.SeekHead, typeof(ContainerElement) }, 162 | { ElementId.Seek, typeof(ContainerElement) }, 163 | { ElementId.SeekID, typeof(BinaryElement) }, 164 | { ElementId.SeekPosition, typeof(UintElement) }, 165 | { ElementId.Info, typeof(ContainerElement) }, 166 | { ElementId.SegmentUID, typeof(BinaryElement) }, 167 | { ElementId.SegmentFilename, typeof(StringElement) }, 168 | { ElementId.PrevUID, typeof(BinaryElement) }, 169 | { ElementId.PrevFilename, typeof(StringElement) }, 170 | { ElementId.NextUID, typeof(BinaryElement) }, 171 | { ElementId.NextFilename, typeof(StringElement) }, 172 | { ElementId.SegmentFamily, typeof(BinaryElement) }, 173 | { ElementId.ChapterTranslate, typeof(ContainerElement) }, 174 | { ElementId.ChapterTranslateEditionUID, typeof(UintElement) }, 175 | { ElementId.ChapterTranslateCodec, typeof(UintElement) }, 176 | { ElementId.ChapterTranslateID, typeof(BinaryElement) }, 177 | { ElementId.TimecodeScale, typeof(UintElement) }, 178 | { ElementId.Duration, typeof(FloatElement) }, 179 | { ElementId.DateUTC, typeof(DateElement) }, 180 | { ElementId.Title, typeof(StringElement) }, 181 | { ElementId.MuxingApp, typeof(StringElement) }, 182 | { ElementId.WritingApp, typeof(StringElement) }, 183 | { ElementId.Cluster, typeof(ContainerElement) }, 184 | { ElementId.Timecode, typeof(UintElement) }, 185 | { ElementId.SilentTracks, typeof(ContainerElement) }, 186 | { ElementId.SilentTrackNumber, typeof(UintElement) }, 187 | { ElementId.Position, typeof(UintElement) }, 188 | { ElementId.PrevSize, typeof(UintElement) }, 189 | { ElementId.SimpleBlock, typeof(SimpleBlockElement) }, 190 | { ElementId.BlockGroup, typeof(ContainerElement) }, 191 | { ElementId.Block, typeof(BinaryElement) }, 192 | { ElementId.BlockVirtual, typeof(BinaryElement) }, 193 | { ElementId.BlockAdditions, typeof(ContainerElement) }, 194 | { ElementId.BlockMore, typeof(ContainerElement) }, 195 | { ElementId.BlockAddID, typeof(UintElement) }, 196 | { ElementId.BlockAdditional, typeof(BinaryElement) }, 197 | { ElementId.BlockDuration, typeof(UintElement) }, 198 | { ElementId.ReferencePriority, typeof(UintElement) }, 199 | { ElementId.ReferenceBlock, typeof(IntElement) }, 200 | { ElementId.ReferenceVirtual, typeof(IntElement) }, 201 | { ElementId.CodecState, typeof(BinaryElement) }, 202 | { ElementId.DiscardPadding, typeof(IntElement) }, 203 | { ElementId.Slices, typeof(ContainerElement) }, 204 | { ElementId.TimeSlice, typeof(ContainerElement) }, 205 | { ElementId.LaceNumber, typeof(UintElement) }, 206 | { ElementId.FrameNumber, typeof(UintElement) }, 207 | { ElementId.BlockAdditionID, typeof(UintElement) }, 208 | { ElementId.Delay, typeof(UintElement) }, 209 | { ElementId.SliceDuration, typeof(UintElement) }, 210 | { ElementId.ReferenceFrame, typeof(ContainerElement) }, 211 | { ElementId.ReferenceOffset, typeof(UintElement) }, 212 | { ElementId.ReferenceTimeCode, typeof(UintElement) }, 213 | { ElementId.EncryptedBlock, typeof(BinaryElement) }, 214 | { ElementId.Tracks, typeof(ContainerElement) }, 215 | { ElementId.TrackEntry, typeof(TrackEntryElement) }, 216 | { ElementId.TrackNumber, typeof(UintElement) }, 217 | { ElementId.TrackUID, typeof(UintElement) }, 218 | { ElementId.TrackType, typeof(UintElement) }, 219 | { ElementId.FlagEnabled, typeof(UintElement) }, 220 | { ElementId.FlagDefault, typeof(UintElement) }, 221 | { ElementId.FlagForced, typeof(UintElement) }, 222 | { ElementId.FlagLacing, typeof(UintElement) }, 223 | { ElementId.MinCache, typeof(UintElement) }, 224 | { ElementId.MaxCache, typeof(UintElement) }, 225 | { ElementId.DefaultDuration, typeof(UintElement) }, 226 | { ElementId.DefaultDecodedFieldDuration, typeof(UintElement) }, 227 | { ElementId.TrackTimecodeScale, typeof(FloatElement) }, 228 | { ElementId.TrackOffset, typeof(IntElement) }, 229 | { ElementId.MaxBlockAdditionID, typeof(UintElement) }, 230 | { ElementId.Name, typeof(StringElement) }, 231 | { ElementId.Language, typeof(StringElement) }, 232 | { ElementId.CodecID, typeof(StringElement) }, 233 | { ElementId.CodecPrivate, typeof(BinaryElement) }, 234 | { ElementId.CodecName, typeof(StringElement) }, 235 | { ElementId.AttachmentLink, typeof(UintElement) }, 236 | { ElementId.CodecSettings, typeof(StringElement) }, 237 | { ElementId.CodecInfoURL, typeof(StringElement) }, 238 | { ElementId.CodecDownloadURL, typeof(StringElement) }, 239 | { ElementId.CodecDecodeAll, typeof(UintElement) }, 240 | { ElementId.TrackOverlay, typeof(UintElement) }, 241 | { ElementId.CodecDelay, typeof(UintElement) }, 242 | { ElementId.SeekPreRoll, typeof(UintElement) }, 243 | { ElementId.TrackTranslate, typeof(ContainerElement) }, 244 | { ElementId.TrackTranslateEditionUID, typeof(UintElement) }, 245 | { ElementId.TrackTranslateCodec, typeof(UintElement) }, 246 | { ElementId.TrackTranslateTrackID, typeof(BinaryElement) }, 247 | { ElementId.Video, typeof(ContainerElement) }, 248 | { ElementId.FlagInterlaced, typeof(UintElement) }, 249 | { ElementId.StereoMode, typeof(UintElement) }, 250 | { ElementId.AlphaMode, typeof(UintElement) }, 251 | { ElementId.OldStereoMode, typeof(UintElement) }, 252 | { ElementId.PixelWidth, typeof(UintElement) }, 253 | { ElementId.PixelHeight, typeof(UintElement) }, 254 | { ElementId.PixelCropBottom, typeof(UintElement) }, 255 | { ElementId.PixelCropTop, typeof(UintElement) }, 256 | { ElementId.PixelCropLeft, typeof(UintElement) }, 257 | { ElementId.PixelCropRight, typeof(UintElement) }, 258 | { ElementId.DisplayWidth, typeof(UintElement) }, 259 | { ElementId.DisplayHeight, typeof(UintElement) }, 260 | { ElementId.DisplayUnit, typeof(UintElement) }, 261 | { ElementId.AspectRatioType, typeof(UintElement) }, 262 | { ElementId.ColourSpace, typeof(BinaryElement) }, 263 | { ElementId.GammaValue, typeof(FloatElement) }, 264 | { ElementId.FrameRate, typeof(FloatElement) }, 265 | { ElementId.Audio, typeof(ContainerElement) }, 266 | { ElementId.SamplingFrequency, typeof(FloatElement) }, 267 | { ElementId.OutputSamplingFrequency, typeof(FloatElement) }, 268 | { ElementId.Channels, typeof(UintElement) }, 269 | { ElementId.ChannelPositions, typeof(BinaryElement) }, 270 | { ElementId.BitDepth, typeof(UintElement) }, 271 | { ElementId.TrackOperation, typeof(ContainerElement) }, 272 | { ElementId.TrackCombinePlanes, typeof(ContainerElement) }, 273 | { ElementId.TrackPlane, typeof(ContainerElement) }, 274 | { ElementId.TrackPlaneUID, typeof(UintElement) }, 275 | { ElementId.TrackPlaneType, typeof(UintElement) }, 276 | { ElementId.TrackJoinBlocks, typeof(ContainerElement) }, 277 | { ElementId.TrackJoinUID, typeof(UintElement) }, 278 | { ElementId.TrickTrackUID, typeof(UintElement) }, 279 | { ElementId.TrickTrackSegmentUID, typeof(BinaryElement) }, 280 | { ElementId.TrickTrackFlag, typeof(UintElement) }, 281 | { ElementId.TrickMasterTrackUID, typeof(UintElement) }, 282 | { ElementId.TrickMasterTrackSegmentUID, typeof(BinaryElement) }, 283 | { ElementId.ContentEncodings, typeof(ContainerElement) }, 284 | { ElementId.ContentEncoding, typeof(ContainerElement) }, 285 | { ElementId.ContentEncodingOrder, typeof(UintElement) }, 286 | { ElementId.ContentEncodingScope, typeof(UintElement) }, 287 | { ElementId.ContentEncodingType, typeof(UintElement) }, 288 | { ElementId.ContentCompression, typeof(ContainerElement) }, 289 | { ElementId.ContentCompAlgo, typeof(UintElement) }, 290 | { ElementId.ContentCompSettings, typeof(BinaryElement) }, 291 | { ElementId.ContentEncryption, typeof(ContainerElement) }, 292 | { ElementId.ContentEncAlgo, typeof(UintElement) }, 293 | { ElementId.ContentEncKeyID, typeof(BinaryElement) }, 294 | { ElementId.ContentSignature, typeof(BinaryElement) }, 295 | { ElementId.ContentSigKeyID, typeof(BinaryElement) }, 296 | { ElementId.ContentSigAlgo, typeof(UintElement) }, 297 | { ElementId.ContentSigHashAlgo, typeof(UintElement) }, 298 | { ElementId.Cues, typeof(ContainerElement) }, 299 | { ElementId.CuePoint, typeof(ContainerElement) }, 300 | { ElementId.CueTime, typeof(UintElement) }, 301 | { ElementId.CueTrackPositions, typeof(ContainerElement) }, 302 | { ElementId.CueTrack, typeof(UintElement) }, 303 | { ElementId.CueClusterPosition, typeof(UintElement) }, 304 | { ElementId.CueRelativePosition, typeof(UintElement) }, 305 | { ElementId.CueDuration, typeof(UintElement) }, 306 | { ElementId.CueBlockNumber, typeof(UintElement) }, 307 | { ElementId.CueCodecState, typeof(UintElement) }, 308 | { ElementId.CueReference, typeof(ContainerElement) }, 309 | { ElementId.CueRefTime, typeof(UintElement) }, 310 | { ElementId.CueRefCluster, typeof(UintElement) }, 311 | { ElementId.CueRefNumber, typeof(UintElement) }, 312 | { ElementId.CueRefCodecState, typeof(UintElement) }, 313 | { ElementId.Attachments, typeof(ContainerElement) }, 314 | { ElementId.AttachedFile, typeof(ContainerElement) }, 315 | { ElementId.FileDescription, typeof(StringElement) }, 316 | { ElementId.FileName, typeof(StringElement) }, 317 | { ElementId.FileMimeType, typeof(StringElement) }, 318 | { ElementId.FileData, typeof(BinaryElement) }, 319 | { ElementId.FileUID, typeof(UintElement) }, 320 | { ElementId.FileReferral, typeof(BinaryElement) }, 321 | { ElementId.FileUsedStartTime, typeof(UintElement) }, 322 | { ElementId.FileUsedEndTime, typeof(UintElement) }, 323 | { ElementId.Chapters, typeof(ContainerElement) }, 324 | { ElementId.EditionEntry, typeof(ContainerElement) }, 325 | { ElementId.EditionUID, typeof(UintElement) }, 326 | { ElementId.EditionFlagHidden, typeof(UintElement) }, 327 | { ElementId.EditionFlagDefault, typeof(UintElement) }, 328 | { ElementId.EditionFlagOrdered, typeof(UintElement) }, 329 | { ElementId.ChapterAtom, typeof(ContainerElement) }, 330 | { ElementId.ChapterUID, typeof(UintElement) }, 331 | { ElementId.ChapterStringUID, typeof(StringElement) }, 332 | { ElementId.ChapterTimeStart, typeof(UintElement) }, 333 | { ElementId.ChapterTimeEnd, typeof(UintElement) }, 334 | { ElementId.ChapterFlagHidden, typeof(UintElement) }, 335 | { ElementId.ChapterFlagEnabled, typeof(UintElement) }, 336 | { ElementId.ChapterSegmentUID, typeof(BinaryElement) }, 337 | { ElementId.ChapterSegmentEditionUID, typeof(UintElement) }, 338 | { ElementId.ChapterPhysicalEquiv, typeof(UintElement) }, 339 | { ElementId.ChapterTrack, typeof(ContainerElement) }, 340 | { ElementId.ChapterTrackNumber, typeof(UintElement) }, 341 | { ElementId.ChapterDisplay, typeof(ContainerElement) }, 342 | { ElementId.ChapString, typeof(StringElement) }, 343 | { ElementId.ChapLanguage, typeof(StringElement) }, 344 | { ElementId.ChapCountry, typeof(StringElement) }, 345 | { ElementId.ChapProcess, typeof(ContainerElement) }, 346 | { ElementId.ChapProcessCodecID, typeof(UintElement) }, 347 | { ElementId.ChapProcessPrivate, typeof(BinaryElement) }, 348 | { ElementId.ChapProcessCommand, typeof(ContainerElement) }, 349 | { ElementId.ChapProcessTime, typeof(UintElement) }, 350 | { ElementId.ChapProcessData, typeof(BinaryElement) }, 351 | { ElementId.Tags, typeof(ContainerElement) }, 352 | { ElementId.Tag, typeof(ContainerElement) }, 353 | { ElementId.Targets, typeof(ContainerElement) }, 354 | { ElementId.TargetTypeValue, typeof(UintElement) }, 355 | { ElementId.TargetType, typeof(StringElement) }, 356 | { ElementId.TagTrackUID, typeof(UintElement) }, 357 | { ElementId.TagEditionUID, typeof(UintElement) }, 358 | { ElementId.TagChapterUID, typeof(UintElement) }, 359 | { ElementId.TagAttachmentUID, typeof(UintElement) }, 360 | { ElementId.SimpleTag, typeof(ContainerElement) }, 361 | { ElementId.TagName, typeof(StringElement) }, 362 | { ElementId.TagLanguage, typeof(StringElement) }, 363 | { ElementId.TagDefault, typeof(UintElement) }, 364 | { ElementId.TagString, typeof(StringElement) }, 365 | { ElementId.TagBinary, typeof(BinaryElement) }, 366 | }; 367 | /// 368 | /// uint value that is used to represent a Segment or Cluster element of unknown size. Only Segments and CLusters can have an unknown size. 369 | /// 370 | public static uint UnknownElementSize { get; } = uint.MaxValue; 371 | /// 372 | /// Returns a string that gives information about this element 373 | /// 374 | /// 375 | public override string ToString() => $"{Index} {Id} - IdChain: [ {string.Join(" ", IdChain.ToArray())} ] Type: {this.GetType().Name} Length: {Length} bytes"; 376 | /// 377 | /// Called when an elements Data is changed 378 | /// 379 | public event Action OnDataChanged; 380 | /// 381 | /// Should be called when Data is changed 382 | /// 383 | protected void DataChangedInvoke() 384 | { 385 | DataChanged = true; 386 | OnDataChanged?.Invoke(this); 387 | } 388 | public static ElementId ReadElementId(Stream data) => (ElementId)ReadContainerUint(data); 389 | public static uint ReadContainerUint(Stream data) 390 | { 391 | var firstByte = (byte)data.ReadByte(); 392 | var bytes = 8 - Convert.ToString(firstByte, 2).Length; 393 | long value = firstByte - (1 << (7 - bytes)); 394 | for (var i = 0; i < bytes; i++) 395 | { 396 | value *= 256; 397 | value += (byte)data.ReadByte(); 398 | } 399 | return (uint)value; 400 | } 401 | public static int GetElementIdUintSize(ElementId x) => GetContainerUintSize((uint)x); 402 | public static int GetContainerUintSize(uint x) 403 | { 404 | int bytes; 405 | int flag; 406 | for (bytes = 1, flag = 0x80; x >= flag && bytes < 8; bytes++, flag *= 0x80) { } 407 | return bytes; 408 | } 409 | public static byte[] GetElementIdUintBytes(ElementId x) => GetContainerUintBytes((uint)x); 410 | public static byte[] GetContainerUintBytes(uint x) 411 | { 412 | int bytes; 413 | int flag; 414 | for (bytes = 1, flag = 0x80; x >= flag && bytes < 8; bytes++, flag *= 0x80) { } 415 | var ret = new byte[bytes]; 416 | var value = flag + x; 417 | for (var i = bytes - 1; i >= 0; i--) 418 | { 419 | var c = value % 256; 420 | ret[i] = (byte)c; 421 | value = (value - c) / 256; 422 | } 423 | return ret; 424 | } 425 | } 426 | /// 427 | /// A typed WebMElement 428 | /// 429 | /// 430 | public abstract class WebMElement : WebMElement 431 | { 432 | private T _Data = default(T); 433 | /// 434 | /// The data contained in this element 435 | /// 436 | public virtual T Data 437 | { 438 | get => _Data; 439 | set 440 | { 441 | var isEqual = EqualityComparer.Default.Equals(_Data, value); 442 | if (isEqual) return; 443 | _Data = value; 444 | if (!UpdatingBySource) 445 | { 446 | UpdateByData(); 447 | DataChangedInvoke(); 448 | } 449 | } 450 | } 451 | public WebMElement(ElementId id) : base(id){ } 452 | /// 453 | /// Should be overridden and internally update WebMBase.Source when called 454 | /// 455 | public virtual void UpdateByData() 456 | { 457 | throw new NotImplementedException(); 458 | } 459 | /// 460 | /// Should be overridden and internally update WebMBase.Data when called 461 | /// 462 | public override void UpdateBySource() 463 | { 464 | throw new NotImplementedException(); 465 | } 466 | /// 467 | /// Provides information specific to this instance 468 | /// 469 | /// 470 | public override string ToString() => $"{Index} {Id} - IdChain: [ {string.Join(" ", IdChain.ToArray())} ] Type: {this.GetType().Name} Length: {Length} bytes Value: {Data}"; 471 | } 472 | } 473 | --------------------------------------------------------------------------------