├── .gitattributes ├── .gitignore ├── DashTools.Samples ├── App.config ├── DashTools.Samples.csproj ├── Program.cs ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── DashTools.Tests ├── DashTools.Tests.csproj ├── MediaPresentationDescriptionTests.cs ├── MpdDownloaderTests.cs ├── MpdFixture.cs ├── Properties │ └── AssemblyInfo.cs ├── envivio.mpd └── packages.config ├── DashTools ├── ArrayExtensions.cs ├── AspectRatio.cs ├── DashTools.csproj ├── FrameRate.cs ├── MediaPresentationDescription.cs ├── Mp4File.cs ├── Mp4InitFile.cs ├── MpdAdaptationSet.cs ├── MpdDownloader.cs ├── MpdElement.cs ├── MpdInitialization.cs ├── MpdPeriod.cs ├── MpdRepresentation.cs ├── MpdSegmentList.cs ├── MpdSegmentTemplate.cs ├── MpdSegmentUrl.cs ├── MpdWalker.cs ├── MultipleSegmentBase.cs ├── Properties │ └── AssemblyInfo.cs ├── SegmentBase.cs ├── Track.cs ├── TrackContentType.cs ├── TrackRepresentation.cs ├── TrackRepresentationSegment.cs └── XmlAttributeParseHelper.cs ├── LICENSE ├── NuGet └── DashTools.csproj.nuspec ├── README.md ├── SharpDashTools.sln └── build.ps1 /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | NuGet/NuGet.exe 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | build/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | 88 | # TFS 2012 Local Workspace 89 | $tf/ 90 | 91 | # Guidance Automation Toolkit 92 | *.gpState 93 | 94 | # ReSharper is a .NET coding add-in 95 | _ReSharper*/ 96 | *.[Rr]e[Ss]harper 97 | *.DotSettings.user 98 | 99 | # JustCode is a .NET coding add-in 100 | .JustCode 101 | 102 | # TeamCity is a build add-in 103 | _TeamCity* 104 | 105 | # DotCover is a Code Coverage Tool 106 | *.dotCover 107 | 108 | # NCrunch 109 | _NCrunch_* 110 | .*crunch*.local.xml 111 | 112 | # MightyMoose 113 | *.mm.* 114 | AutoTest.Net/ 115 | 116 | # Web workbench (sass) 117 | .sass-cache/ 118 | 119 | # Installshield output folder 120 | [Ee]xpress/ 121 | 122 | # DocProject is a documentation generator add-in 123 | DocProject/buildhelp/ 124 | DocProject/Help/*.HxT 125 | DocProject/Help/*.HxC 126 | DocProject/Help/*.hhc 127 | DocProject/Help/*.hhk 128 | DocProject/Help/*.hhp 129 | DocProject/Help/Html2 130 | DocProject/Help/html 131 | 132 | # Click-Once directory 133 | publish/ 134 | 135 | # Publish Web Output 136 | *.[Pp]ublish.xml 137 | *.azurePubxml 138 | ## TODO: Comment the next line if you want to checkin your 139 | ## web deploy settings but do note that will include unencrypted 140 | ## passwords 141 | #*.pubxml 142 | 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | 154 | # Windows Azure Build Output 155 | csx/ 156 | *.build.csdef 157 | 158 | # Windows Store app package directory 159 | AppPackages/ 160 | 161 | # Visual Studio cache files 162 | # files ending in .cache can be ignored 163 | *.[Cc]ache 164 | # but keep track of directories ending in .cache 165 | !*.[Cc]ache/ 166 | 167 | # Others 168 | ClientBin/ 169 | [Ss]tyle[Cc]op.* 170 | ~$* 171 | *~ 172 | *.dbmdl 173 | *.dbproj.schemaview 174 | *.pfx 175 | *.publishsettings 176 | node_modules/ 177 | orleans.codegen.cs 178 | 179 | # RIA/Silverlight projects 180 | Generated_Code/ 181 | 182 | # Backup & report files from converting an old project file 183 | # to a newer Visual Studio version. Backup files are not needed, 184 | # because we have git ;-) 185 | _UpgradeReport_Files/ 186 | Backup*/ 187 | UpgradeLog*.XML 188 | UpgradeLog*.htm 189 | 190 | # SQL Server files 191 | *.mdf 192 | *.ldf 193 | 194 | # Business Intelligence projects 195 | *.rdl.data 196 | *.bim.layout 197 | *.bim_*.settings 198 | 199 | # Microsoft Fakes 200 | FakesAssemblies/ 201 | 202 | # Node.js Tools for Visual Studio 203 | .ntvs_analysis.dat 204 | 205 | # Visual Studio 6 build log 206 | *.plg 207 | 208 | # Visual Studio 6 workspace options file 209 | *.opt 210 | 211 | # LightSwitch generated files 212 | GeneratedArtifacts/ 213 | _Pvt_Extensions/ 214 | ModelManifest.xml 215 | -------------------------------------------------------------------------------- /DashTools.Samples/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DashTools.Samples/DashTools.Samples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {19125071-F9C9-4F10-AA66-621DD65F3ABE} 8 | Exe 9 | Properties 10 | Qoollo.MpegDash.Samples 11 | DashTools.Samples 12 | v4.5.2 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\NReco.VideoConverter.1.0.8.0\lib\net20\NReco.VideoConverter.dll 38 | True 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {d517b927-7afc-42ad-a410-27354f3217c2} 60 | DashTools 61 | 62 | 63 | 64 | 71 | -------------------------------------------------------------------------------- /DashTools.Samples/Program.cs: -------------------------------------------------------------------------------- 1 | using NReco.VideoConverter; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Qoollo.MpegDash.Samples 11 | { 12 | class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | Task.Run(async () => 17 | { 18 | await MainAsync(args); 19 | }).Wait(); 20 | } 21 | 22 | static async Task MainAsync(string[] args) 23 | { 24 | string dir = "envivio"; 25 | string mpdUrl = "http://10.5.5.7/q/p/userapi/streams/32/mpd"; 26 | //"http://10.5.7.207/userapi/streams/30/mpd"; 27 | //"http://10.5.7.207/userapi/streams/11/mpd?start_time=1458816642&stop_time=1458819642"; 28 | //"http://dash.edgesuite.net/envivio/EnvivioDash3/manifest.mpd"; 29 | //"http://dash.edgesuite.net/dash264/TestCases/1a/netflix/exMPD_BIP_TC1.mpd"; 30 | var stopwatch = Stopwatch.StartNew(); 31 | 32 | var downloader = new MpdDownloader(new Uri(mpdUrl), dir); 33 | var trackRepresentation = downloader.GetTracksFor(TrackContentType.Video).First().TrackRepresentations.OrderByDescending(r => r.Bandwidth).First(); 34 | var prepareTime = stopwatch.Elapsed; 35 | 36 | var chunks = 37 | //Directory.GetFiles(@"C:\Users\Alexander\Work\Github\qoollo\SharpDashTools\DashTools.Samples\bin\Debug\envivio", "*.mp4") 38 | //.Select(f => Path.GetFileName(f) == "make_chunk_path_longer_32.mp4" 39 | // ? new Mp4InitFile(Path.Combine("envivio", Path.GetFileName(f))) 40 | // : new Mp4File(Path.Combine("envivio", Path.GetFileName(f)))) 41 | // .ToArray(); 42 | await downloader.Download(trackRepresentation, TimeSpan.FromMinutes(60), TimeSpan.FromMinutes(60 + 60 * 6 / 6)); 43 | var downloadTime = stopwatch.Elapsed - prepareTime; 44 | 45 | var ffmpeg = new FFMpegConverter(); 46 | ffmpeg.LogReceived += (s, e) => Console.WriteLine(e.Data); 47 | var combined = downloader.CombineChunksFast(chunks, s => ffmpeg.Invoke(s)); 48 | //downloader.CombineChunks(chunks, s => ffmpeg.Invoke(s)); 49 | var combineTime = stopwatch.Elapsed - prepareTime - downloadTime; 50 | 51 | if (!ffmpeg.Stop()) 52 | ffmpeg.Abort(); 53 | 54 | Console.WriteLine(); 55 | Console.WriteLine(); 56 | Console.WriteLine("================================================================"); 57 | Console.WriteLine("Prepared in {0} s", prepareTime.TotalSeconds); 58 | Console.WriteLine("Downloaded {0} chunks in {1} s", chunks.Count(), downloadTime.TotalSeconds); 59 | Console.WriteLine("Combined in {0} s", combineTime.TotalSeconds); 60 | Console.WriteLine("Total: {0} s", (prepareTime + downloadTime + combineTime).TotalSeconds); 61 | Console.ReadLine(); 62 | 63 | return; 64 | 65 | string mpdFilePath = //@"C:\Users\Alexander\AppData\Local\Temp\note_5_video_5_source_2-index_00000\manifest.mpd"; 66 | await DownloadMpdStreams(mpdUrl, dir); 67 | await ConcatStreams(mpdFilePath, Path.Combine(Path.GetDirectoryName(mpdFilePath), "output.mp4")); 68 | } 69 | 70 | private static async Task DownloadMpdStreams(string url, string destinationDir) 71 | { 72 | var dir = new DirectoryInfo(destinationDir); 73 | if (dir.Exists) 74 | dir.Delete(recursive: true); 75 | var downloader = new MpdDownloader(new Uri(url), dir.FullName); 76 | 77 | return await downloader.Download(); 78 | } 79 | 80 | private static async Task ConcatStreams(string mpdFilePath, string outputFile) 81 | { 82 | var mpd = new MediaPresentationDescription(mpdFilePath); 83 | var walker = new MpdWalker(mpd); 84 | var track = walker.GetTracksFor(TrackContentType.Video).First(); 85 | var trackRepresentation = track.TrackRepresentations.OrderByDescending(r => r.Bandwidth).First(); 86 | 87 | using (var stream = File.OpenWrite(outputFile)) 88 | using (var writer = new BinaryWriter(stream)) 89 | { 90 | string fragmentPath = Path.Combine(Path.GetDirectoryName(mpdFilePath), trackRepresentation.InitFragmentPath); 91 | writer.Write(File.ReadAllBytes(fragmentPath)); 92 | 93 | foreach (var path in trackRepresentation.FragmentsPaths) 94 | { 95 | fragmentPath = Path.Combine(Path.GetDirectoryName(mpdFilePath), path); 96 | if (!File.Exists(fragmentPath)) 97 | break; 98 | writer.Write(File.ReadAllBytes(fragmentPath)); 99 | } 100 | } 101 | 102 | string[] files = new[] 103 | { 104 | "v1_257-Header.m4s", 105 | "v1_257-270146-i-1.m4s", 106 | "v1_257-270146-i-2.m4s", 107 | "v1_257-270146-i-3.m4s", 108 | "v1_257-270146-i-4.m4s", 109 | "v1_257-270146-i-5.m4s", 110 | "v1_257-270146-i-6.m4s", 111 | "v1_257-270146-i-7.m4s", 112 | "v1_257-270146-i-8.m4s", 113 | "v1_257-270146-i-9.m4s", 114 | "v1_257-270146-i-10.m4s", 115 | "v1_257-270146-i-11.m4s", 116 | "v1_257-270146-i-12.m4s", 117 | "v1_257-270146-i-13.m4s", 118 | "v1_257-270146-i-14.m4s", 119 | "v1_257-270146-i-15.m4s", 120 | "v1_257-270146-i-16.m4s", 121 | "v1_257-270146-i-17.m4s", 122 | "v1_257-270146-i-18.m4s", 123 | "v1_257-270146-i-19.m4s", 124 | "v1_257-270146-i-20.m4s", 125 | "v1_257-270146-i-21.m4s", 126 | "v1_257-270146-i-22.m4s", 127 | "v1_257-270146-i-23.m4s", 128 | "v1_257-270146-i-24.m4s", 129 | "v1_257-270146-i-25.m4s", 130 | "v1_257-270146-i-26.m4s", 131 | "v1_257-270146-i-27.m4s", 132 | "v1_257-270146-i-28.m4s", 133 | "v1_257-270146-i-29.m4s", 134 | "v1_257-270146-i-30.m4s", 135 | "v1_257-270146-i-31.m4s", 136 | "v1_257-270146-i-32.m4s", 137 | "v1_257-270146-i-33.m4s", 138 | "v1_257-270146-i-34.m4s", 139 | "v1_257-270146-i-35.m4s", 140 | "v1_257-270146-i-36.m4s", 141 | "v1_257-270146-i-37.m4s", 142 | "v1_257-270146-i-38.m4s", 143 | "v1_257-270146-i-39.m4s", 144 | "v1_257-270146-i-40.m4s", 145 | "v1_257-270146-i-41.m4s", 146 | "v1_257-270146-i-42.m4s", 147 | "v1_257-270146-i-43.m4s", 148 | "v1_257-270146-i-44.m4s", 149 | "v1_257-270146-i-45.m4s", 150 | "v1_257-270146-i-46.m4s", 151 | "v1_257-270146-i-47.m4s", 152 | "v1_257-270146-i-48.m4s", 153 | "v1_257-270146-i-49.m4s", 154 | "v1_257-270146-i-50.m4s", 155 | "v1_257-270146-i-51.m4s", 156 | "v1_257-270146-i-52.m4s", 157 | "v1_257-270146-i-53.m4s", 158 | "v1_257-270146-i-54.m4s", 159 | "v1_257-270146-i-55.m4s", 160 | "v1_257-270146-i-56.m4s", 161 | "v1_257-270146-i-57.m4s", 162 | "v1_257-270146-i-58.m4s", 163 | "v1_257-270146-i-59.m4s", 164 | "v1_257-270146-i-60.m4s", 165 | "v1_257-270146-i-61.m4s", 166 | "v1_257-270146-i-62.m4s", 167 | "v1_257-270146-i-63.m4s", 168 | "v1_257-270146-i-64.m4s", 169 | "v1_257-270146-i-65.m4s", 170 | "v1_257-270146-i-66.m4s", 171 | "v1_257-270146-i-67.m4s", 172 | "v1_257-270146-i-68.m4s", 173 | "v1_257-270146-i-69.m4s", 174 | "v1_257-270146-i-70.m4s", 175 | "v1_257-270146-i-71.m4s", 176 | "v1_257-270146-i-72.m4s", 177 | "v1_257-270146-i-73.m4s", 178 | "v1_257-270146-i-74.m4s", 179 | "v1_257-270146-i-75.m4s", 180 | "v1_257-270146-i-76.m4s", 181 | "v1_257-270146-i-77.m4s", 182 | "v1_257-270146-i-78.m4s", 183 | "v1_257-270146-i-79.m4s", 184 | "v1_257-270146-i-80.m4s", 185 | "v1_257-270146-i-81.m4s", 186 | "v1_257-270146-i-82.m4s", 187 | "v1_257-270146-i-83.m4s", 188 | "v1_257-270146-i-84.m4s", 189 | "v1_257-270146-i-85.m4s", 190 | "v1_257-270146-i-86.m4s", 191 | "v1_257-270146-i-87.m4s", 192 | "v1_257-270146-i-88.m4s", 193 | "v1_257-270146-i-89.m4s", 194 | "v1_257-270146-i-90.m4s", 195 | "v1_257-270146-i-91.m4s", 196 | "v1_257-270146-i-92.m4s", 197 | "v1_257-270146-i-93.m4s", 198 | "v1_257-270146-i-94.m4s", 199 | "v1_257-270146-i-95.m4s", 200 | "v1_257-270146-i-96.m4s", 201 | "v1_257-270146-i-97.m4s", 202 | }; 203 | //string outputFile = Path.Combine(Path.GetDirectoryName(mpdFilePath), "output.mp4"); 204 | //using (var stream = File.OpenWrite(outputFile)) 205 | //using (var writer = new BinaryWriter(stream)) 206 | //{ 207 | // files.Select(f => Path.Combine(Path.GetDirectoryName(mpdFilePath), f)) 208 | // .ToList() 209 | // .ForEach(f => writer.Write(File.ReadAllBytes(f))); 210 | //} 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /DashTools.Samples/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DashTools.Samples")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DashTools.Samples")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("19125071-f9c9-4f10-aa66-621dd65f3abe")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /DashTools.Samples/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /DashTools.Tests/DashTools.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {17D61727-C15B-4329-AC4A-381D174E76EB} 8 | Library 9 | Properties 10 | Qoollo.MpegDash.Tests 11 | DashTools.Tests 12 | v4.5.2 13 | 512 14 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 10.0 16 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 17 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 18 | False 19 | UnitTest 20 | 21 | 22 | 23 | 24 | 25 | true 26 | full 27 | false 28 | bin\Debug\ 29 | DEBUG;TRACE 30 | prompt 31 | 4 32 | false 33 | 34 | 35 | pdbonly 36 | true 37 | bin\Release\ 38 | TRACE 39 | prompt 40 | 4 41 | false 42 | 43 | 44 | 45 | 46 | 3.5 47 | 48 | 49 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 50 | True 51 | 52 | 53 | ..\packages\xunit.assert.2.2.0-beta1-build3239\lib\dotnet\xunit.assert.dll 54 | True 55 | 56 | 57 | ..\packages\xunit.extensibility.core.2.2.0-beta1-build3239\lib\dotnet\xunit.core.dll 58 | True 59 | 60 | 61 | ..\packages\xunit.extensibility.execution.2.2.0-beta1-build3239\lib\net45\xunit.execution.desktop.dll 62 | True 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | PreserveNewest 86 | 87 | 88 | 89 | 90 | 91 | {d517b927-7afc-42ad-a410-27354f3217c2} 92 | DashTools 93 | 94 | 95 | 96 | 97 | 98 | 99 | False 100 | 101 | 102 | False 103 | 104 | 105 | False 106 | 107 | 108 | False 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 118 | 119 | 120 | 121 | 128 | -------------------------------------------------------------------------------- /DashTools.Tests/MediaPresentationDescriptionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace Qoollo.MpegDash.Tests 6 | { 7 | public class MediaPresentationDescriptionTests : IClassFixture 8 | { 9 | private MediaPresentationDescription mpd; 10 | 11 | public MediaPresentationDescriptionTests(MpdFixture mpdFixture) 12 | { 13 | this.mpd = mpdFixture.Mpd; 14 | } 15 | 16 | [Fact] 17 | public void Id() 18 | { 19 | Assert.Null(mpd.Id); 20 | } 21 | 22 | [Fact] 23 | public void Profiles() 24 | { 25 | Assert.Equal("urn:mpeg:dash:profile:isoff-live:2011", mpd.Profiles); 26 | } 27 | 28 | [Fact] 29 | public void Type() 30 | { 31 | Assert.Equal("static", mpd.Type); 32 | } 33 | 34 | [Fact] 35 | public void AvailabilityStartTime() 36 | { 37 | Assert.Equal(new DateTimeOffset(2016, 1, 20, 21, 10, 2, TimeSpan.Zero), mpd.AvailabilityStartTime); 38 | } 39 | 40 | [Fact] 41 | public void PublishTime() 42 | { 43 | Assert.Null(mpd.PublishTime); 44 | } 45 | 46 | [Fact] 47 | public void AvailabilityEndTime() 48 | { 49 | Assert.Null(mpd.AvailabilityEndTime); 50 | } 51 | 52 | [Fact] 53 | public void MediaPresentationDuration() 54 | { 55 | Assert.Equal(TimeSpan.FromSeconds(193.680), mpd.MediaPresentationDuration); 56 | } 57 | 58 | [Fact] 59 | public void MinimumUpdatePeriod() 60 | { 61 | Assert.Null(mpd.MinimumUpdatePeriod); 62 | } 63 | 64 | [Fact] 65 | public void MinBufferTime() 66 | { 67 | Assert.Equal(TimeSpan.FromSeconds(5), mpd.MinBufferTime); 68 | } 69 | 70 | [Fact] 71 | public void TimeShiftBufferDepth() 72 | { 73 | Assert.Null(mpd.TimeShiftBufferDepth); 74 | } 75 | 76 | [Fact] 77 | public void SuggestedPresentationDelay() 78 | { 79 | Assert.Null(mpd.SuggestedPresentationDelay); 80 | } 81 | 82 | [Fact] 83 | public void MaxSegmentDuration() 84 | { 85 | Assert.Equal(TimeSpan.FromMilliseconds(2005), mpd.MaxSegmentDuration); 86 | } 87 | 88 | [Fact] 89 | public void MaxSubsegmentDuration() 90 | { 91 | Assert.Null(mpd.MaxSubsegmentDuration); 92 | } 93 | 94 | [Fact] 95 | public void Periods_Count() 96 | { 97 | Assert.Equal(1, mpd.Periods.Count()); 98 | } 99 | 100 | [Fact] 101 | public void Period_Id() 102 | { 103 | Assert.Equal("period0", mpd.Periods.First().Id); 104 | } 105 | 106 | [Fact] 107 | public void Period_Start() 108 | { 109 | Assert.Null(mpd.Periods.First().Start); 110 | } 111 | 112 | [Fact] 113 | public void Period_Duration() 114 | { 115 | Assert.Null(mpd.Periods.First().Duration); 116 | } 117 | 118 | [Fact] 119 | public void Period_BitstreamSwitching() 120 | { 121 | Assert.False(mpd.Periods.First().BitstreamSwitching); 122 | } 123 | 124 | [Fact] 125 | public void Period_AdaptationSets_Count() 126 | { 127 | Assert.Equal(2, mpd.Periods.First().AdaptationSets.Count()); 128 | } 129 | 130 | [Fact] 131 | public void Period_AdaptationSets_0_Id() 132 | { 133 | Assert.Null(mpd.Periods.First().AdaptationSets.First().Id); 134 | } 135 | 136 | [Fact] 137 | public void Period_AdaptationSets_0_Group() 138 | { 139 | Assert.Null(mpd.Periods.First().AdaptationSets.First().Group); 140 | } 141 | 142 | [Fact] 143 | public void Period_AdaptationSets_0_Lang() 144 | { 145 | Assert.Null(mpd.Periods.First().AdaptationSets.First().Lang); 146 | } 147 | 148 | [Fact] 149 | public void Period_AdaptationSets_0_ContentType() 150 | { 151 | Assert.Equal("video/mp4", mpd.Periods.First().AdaptationSets.First().ContentType); 152 | } 153 | 154 | [Fact] 155 | public void Period_AdaptationSets_0_Par() 156 | { 157 | Assert.Equal("1:1", mpd.Periods.First().AdaptationSets.First().Par.RawValue); 158 | } 159 | 160 | [Fact] 161 | public void Period_AdaptationSets_0_MinBandwidth() 162 | { 163 | Assert.Null(mpd.Periods.First().AdaptationSets.First().MinBandwidth); 164 | } 165 | 166 | [Fact] 167 | public void Period_AdaptationSets_0_MaxBandwidth() 168 | { 169 | Assert.Null(mpd.Periods.First().AdaptationSets.First().MaxBandwidth); 170 | } 171 | 172 | [Fact] 173 | public void Period_AdaptationSets_0_MinWidth() 174 | { 175 | Assert.Null(mpd.Periods.First().AdaptationSets.First().MinWidth); 176 | } 177 | 178 | [Fact] 179 | public void Period_AdaptationSets_0_MaxWidth() 180 | { 181 | Assert.Equal(1920u, mpd.Periods.First().AdaptationSets.First().MaxWidth); 182 | } 183 | 184 | [Fact] 185 | public void Period_AdaptationSets_0_MinHeight() 186 | { 187 | Assert.Null(mpd.Periods.First().AdaptationSets.First().MinHeight); 188 | } 189 | 190 | [Fact] 191 | public void Period_AdaptationSets_0_MaxHeight() 192 | { 193 | Assert.Equal(1080u, mpd.Periods.First().AdaptationSets.First().MaxHeight); 194 | } 195 | 196 | [Fact] 197 | public void Period_AdaptationSets_0_MinFrameRate() 198 | { 199 | Assert.Null(mpd.Periods.First().AdaptationSets.First().MinFrameRate); 200 | } 201 | 202 | [Fact] 203 | public void Period_AdaptationSets_0_MaxFrameRate() 204 | { 205 | Assert.Equal("30000/1001", mpd.Periods.First().AdaptationSets.First().MaxFrameRate.RawValue); 206 | } 207 | 208 | [Fact] 209 | public void Period_AdaptationSets_0_SegmentAlignment() 210 | { 211 | Assert.True(mpd.Periods.First().AdaptationSets.First().SegmentAlignment); 212 | } 213 | 214 | [Fact] 215 | public void Period_AdaptationSets_0_BitstreamSwitching() 216 | { 217 | Assert.False(mpd.Periods.First().AdaptationSets.First().BitstreamSwitching); 218 | } 219 | 220 | [Fact] 221 | public void Period_AdaptationSets_0_SubsegmentAlignment() 222 | { 223 | Assert.False(mpd.Periods.First().AdaptationSets.First().SubsegmentAlignment); 224 | } 225 | 226 | [Fact] 227 | public void Period_AdaptationSets_0_SubsegmentStartsWithSAP() 228 | { 229 | Assert.Equal(1u, mpd.Periods.First().AdaptationSets.First().SubsegmentStartsWithSAP); 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /DashTools.Tests/MpdDownloaderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace Qoollo.MpegDash.Tests 7 | { 8 | public class MpdDownloaderTests : IClassFixture 9 | { 10 | private MediaPresentationDescription mpd; 11 | 12 | public MpdDownloaderTests(MpdFixture mpdFixture) 13 | { 14 | this.mpd = mpdFixture.Mpd; 15 | } 16 | 17 | [Fact] 18 | public void Download() 19 | { 20 | // arrange 21 | //var dir = new DirectoryInfo("envivio"); 22 | //if (dir.Exists) 23 | // dir.Delete(recursive: true); 24 | //var downloader = new MpdDownloader(new Uri("http://dash.edgesuite.net/envivio/EnvivioDash3/manifest.mpd"), dir.FullName); 25 | 26 | //// act 27 | //var task = downloader.Download(); 28 | //task.Wait(); 29 | //var actual = task.Result; 30 | 31 | // assert 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DashTools.Tests/MpdFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Qoollo.MpegDash.Tests 9 | { 10 | public class MpdFixture : IDisposable 11 | { 12 | public MpdFixture() 13 | { 14 | Stream = File.OpenRead("envivio.mpd"); 15 | Mpd = new MediaPresentationDescription(Stream); 16 | } 17 | 18 | public void Dispose() 19 | { 20 | Stream.Dispose(); 21 | } 22 | 23 | public Stream Stream { get; private set; } 24 | 25 | public MediaPresentationDescription Mpd { get; private set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DashTools.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DashTools.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DashTools.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("17d61727-c15b-4329-ac4a-381d174e76eb")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /DashTools.Tests/envivio.mpd: -------------------------------------------------------------------------------- 1 |  2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /DashTools.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /DashTools/ArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | internal static class ArrayExtensions 9 | { 10 | /// 11 | /// Checks whether first array starts with second array. 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static bool StartsWith(this T[] first, T[] second) 18 | where T: IComparable 19 | { 20 | bool res = true; 21 | for (int i = 0; i < second.Length && res; i++) 22 | { 23 | res = first[i].CompareTo(second[i]) == 0; 24 | } 25 | return res; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DashTools/AspectRatio.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Qoollo.MpegDash 4 | { 5 | public class AspectRatio 6 | { 7 | public AspectRatio(string value) 8 | { 9 | if (string.IsNullOrWhiteSpace(value)) 10 | throw new ArgumentNullException(nameof(value)); 11 | 12 | RawValue = value; 13 | X = double.Parse(value.Split(':')[0]); 14 | Y = double.Parse(value.Split(':')[1]); 15 | } 16 | 17 | public double X { get; } 18 | 19 | public double Y { get; } 20 | 21 | public string RawValue { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /DashTools/DashTools.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {D517B927-7AFC-42AD-A410-27354F3217C2} 8 | Library 9 | Properties 10 | Qoollo.MpegDash 11 | DashTools 12 | v4.0 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | -------------------------------------------------------------------------------- /DashTools/FrameRate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | public class FrameRate 9 | { 10 | public FrameRate(string value) 11 | { 12 | if (string.IsNullOrWhiteSpace(value)) 13 | throw new ArgumentNullException(nameof(value)); 14 | 15 | RawValue = value; 16 | } 17 | 18 | public string RawValue { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DashTools/MediaPresentationDescription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Xml; 7 | using System.Xml.Linq; 8 | 9 | namespace Qoollo.MpegDash 10 | { 11 | public class MediaPresentationDescription : IDisposable 12 | { 13 | private readonly Stream stream; 14 | 15 | private readonly bool streamIsOwned; 16 | 17 | private readonly Lazy mpdTag; 18 | 19 | private readonly Lazy helper; 20 | 21 | public MediaPresentationDescription(Stream mpdStream) 22 | : this(mpdStream, false) 23 | { 24 | this.baseURL = new Lazy(ParseBaseURL); 25 | } 26 | 27 | public MediaPresentationDescription(string mpdFilePath) 28 | : this(File.OpenRead(mpdFilePath), true) 29 | { 30 | this.baseURL = new Lazy(ParseBaseURL); 31 | } 32 | 33 | private MediaPresentationDescription(Stream mpdStream, bool streamIsOwned) 34 | { 35 | stream = mpdStream; 36 | this.streamIsOwned = streamIsOwned; 37 | 38 | mpdTag = new Lazy(ReadMpdTag); 39 | fetchTime = new Lazy(() => { var v = mpdTag.Value; return DateTimeOffset.Now; }); 40 | helper = new Lazy(() => new XmlAttributeParseHelper(mpdTag.Value)); 41 | periods = new Lazy>(ParsePeriods); 42 | } 43 | 44 | public static MediaPresentationDescription FromUrl(Uri url, string saveFilePath = null) 45 | { 46 | if (saveFilePath == null) 47 | saveFilePath = Path.GetTempFileName(); 48 | using (var client = new WebClient()) 49 | { 50 | client.DownloadFile(url, saveFilePath); 51 | return new MediaPresentationDescription(saveFilePath); 52 | } 53 | } 54 | 55 | public DateTimeOffset FetchTime 56 | { 57 | get { return fetchTime.Value; } 58 | } 59 | private readonly Lazy fetchTime; 60 | 61 | public string Id 62 | { 63 | get { return mpdTag.Value.Attribute("id")?.Value; } 64 | } 65 | 66 | public string Type 67 | { 68 | get { return mpdTag.Value.Attribute("type")?.Value ?? "static"; } 69 | } 70 | 71 | public string Profiles 72 | { 73 | get { return mpdTag.Value.Attribute("profiles").Value; } 74 | } 75 | 76 | public DateTimeOffset? AvailabilityStartTime 77 | { 78 | get { return helper.Value.ParseDateTimeOffset("availabilityStartTime", Type == "dynamic"); } 79 | } 80 | 81 | public DateTimeOffset? PublishTime 82 | { 83 | get { return helper.Value.ParseOptionalDateTimeOffset("publishTime"); } 84 | } 85 | 86 | public DateTimeOffset? AvailabilityEndTime 87 | { 88 | get { return helper.Value.ParseOptionalDateTimeOffset("availabilityEndTime"); } 89 | } 90 | 91 | public TimeSpan? MediaPresentationDuration 92 | { 93 | get { return helper.Value.ParseOptionalTimeSpan("mediaPresentationDuration"); } 94 | } 95 | 96 | public TimeSpan? MinimumUpdatePeriod 97 | { 98 | get { return helper.Value.ParseOptionalTimeSpan("minimumUpdatePeriod"); } 99 | } 100 | 101 | public TimeSpan MinBufferTime 102 | { 103 | get { return XmlConvert.ToTimeSpan(mpdTag.Value.Attribute("minBufferTime").Value); } 104 | } 105 | 106 | public TimeSpan? TimeShiftBufferDepth 107 | { 108 | get { return helper.Value.ParseOptionalTimeSpan("timeShiftBufferDepth"); } 109 | } 110 | 111 | public TimeSpan? SuggestedPresentationDelay 112 | { 113 | get { return helper.Value.ParseOptionalTimeSpan("suggestedPresentationDelay"); } 114 | } 115 | 116 | public TimeSpan? MaxSegmentDuration 117 | { 118 | get { return helper.Value.ParseOptionalTimeSpan("maxSegmentDuration"); } 119 | } 120 | 121 | public TimeSpan? MaxSubsegmentDuration 122 | { 123 | get { return helper.Value.ParseOptionalTimeSpan("maxSubsegmentDuration"); } 124 | } 125 | 126 | public string BaseURL 127 | { 128 | get { return baseURL.Value; } 129 | } 130 | private readonly Lazy baseURL; 131 | 132 | private string ParseBaseURL() 133 | { 134 | return mpdTag.Value.Elements() 135 | .Where(n => n.Name.LocalName == "BaseURL") 136 | .Select(n => n.Value) 137 | .FirstOrDefault(); 138 | } 139 | 140 | public IEnumerable Periods 141 | { 142 | get { return periods.Value; } 143 | } 144 | private readonly Lazy> periods; 145 | 146 | public void Save(string filename) 147 | { 148 | using (var fileStream = File.OpenWrite(filename)) 149 | { 150 | stream.CopyTo(fileStream); 151 | stream.Seek(0, SeekOrigin.Begin); 152 | } 153 | } 154 | 155 | private XElement ReadMpdTag() 156 | { 157 | using (var reader = XmlReader.Create(stream)) 158 | { 159 | stream.Seek(0, SeekOrigin.Begin); 160 | reader.ReadToFollowing("MPD"); 161 | return XNode.ReadFrom(reader) as XElement; 162 | } 163 | } 164 | 165 | private IEnumerable ParsePeriods() 166 | { 167 | return mpdTag.Value.Elements() 168 | .Where(n => n.Name.LocalName == "Period") 169 | .Select(n => new MpdPeriod(n)); 170 | } 171 | 172 | #region IDisposable 173 | 174 | public void Dispose() 175 | { 176 | if (streamIsOwned) 177 | stream.Dispose(); 178 | } 179 | 180 | #endregion 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /DashTools/Mp4File.cs: -------------------------------------------------------------------------------- 1 | namespace Qoollo.MpegDash 2 | { 3 | public class Mp4File 4 | { 5 | public Mp4File(string path) 6 | { 7 | this.path = path; 8 | } 9 | 10 | public string Path 11 | { 12 | get { return path; } 13 | } 14 | private readonly string path; 15 | } 16 | } -------------------------------------------------------------------------------- /DashTools/Mp4InitFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | public class Mp4InitFile : Mp4File 9 | { 10 | public Mp4InitFile(string path) 11 | :base(path) 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DashTools/MpdAdaptationSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | public class MpdAdaptationSet : MpdElement 9 | { 10 | internal MpdAdaptationSet(XElement node) 11 | : base(node) 12 | { 13 | this.segmentTemplate = new Lazy(ParseSegmentTemplate); 14 | this.representations = new Lazy>(ParseRepresentations); 15 | } 16 | 17 | public uint? Id 18 | { 19 | get { return helper.ParseOptionalUint("id"); } 20 | } 21 | 22 | public uint? Group 23 | { 24 | get { return helper.ParseOptionalUint("group"); } 25 | } 26 | 27 | public string Lang 28 | { 29 | get { return node.Attribute("lang")?.Value; } 30 | } 31 | 32 | public string ContentType 33 | { 34 | get 35 | { 36 | var attr = node.Attribute("contentType") ?? node.Attribute("mimeType"); 37 | return attr?.Value; } 38 | } 39 | 40 | public AspectRatio Par 41 | { 42 | get { return helper.ParseOptionalAspectRatio("par"); } 43 | } 44 | 45 | public uint? MinBandwidth 46 | { 47 | get { return helper.ParseOptionalUint("minBandwidth"); } 48 | } 49 | 50 | public uint? MaxBandwidth 51 | { 52 | get { return helper.ParseOptionalUint("maxBandwidth"); } 53 | } 54 | 55 | public uint? MinWidth 56 | { 57 | get { return helper.ParseOptionalUint("minWidth"); } 58 | } 59 | 60 | public uint? MaxWidth 61 | { 62 | get { return helper.ParseOptionalUint("maxWidth"); } 63 | } 64 | 65 | public uint? MinHeight 66 | { 67 | get { return helper.ParseOptionalUint("minHeight"); } 68 | } 69 | 70 | public uint? MaxHeight 71 | { 72 | get { return helper.ParseOptionalUint("maxHeight"); } 73 | } 74 | 75 | public FrameRate MinFrameRate 76 | { 77 | get { return helper.ParseOptionalFrameRate("minFrameRate"); } 78 | } 79 | 80 | public FrameRate MaxFrameRate 81 | { 82 | get { return helper.ParseOptionalFrameRate("maxFrameRate"); } 83 | } 84 | 85 | public bool SegmentAlignment 86 | { 87 | get { return helper.ParseOptionalBool("segmentAlignment", false); } 88 | } 89 | 90 | public bool BitstreamSwitching 91 | { 92 | get { return helper.ParseOptionalBool("bitstreamSwitching", false); } 93 | } 94 | 95 | public bool SubsegmentAlignment 96 | { 97 | get { return helper.ParseOptionalBool("subsegmentAlignment", false); } 98 | } 99 | 100 | public uint SubsegmentStartsWithSAP 101 | { 102 | get 103 | { 104 | var value = helper.ParseOptionalUint("subsegmentStartsWithSAP", null) 105 | ?? helper.ParseOptionalUint("startWithSAP", null); 106 | return value.Value; 107 | } 108 | } 109 | 110 | public MpdSegmentTemplate SegmentTemplate 111 | { 112 | get { return segmentTemplate.Value; } 113 | } 114 | private readonly Lazy segmentTemplate; 115 | 116 | public IEnumerable Representations 117 | { 118 | get { return representations.Value; } 119 | } 120 | private readonly Lazy> representations; 121 | 122 | private MpdSegmentTemplate ParseSegmentTemplate() 123 | { 124 | return node.Elements() 125 | .Where(n => n.Name.LocalName == "SegmentTemplate") 126 | .Select(n => new MpdSegmentTemplate(n)) 127 | .FirstOrDefault(); 128 | } 129 | 130 | private IEnumerable ParseRepresentations() 131 | { 132 | return node.Elements() 133 | .Where(n => n.Name.LocalName == "Representation") 134 | .Select(n => new MpdRepresentation(n)); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /DashTools/MpdDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Qoollo.MpegDash 11 | { 12 | public class MpdDownloader 13 | { 14 | private readonly Uri mpdUrl; 15 | 16 | private readonly string destinationDir; 17 | 18 | private readonly Lazy mpdFileName; 19 | 20 | private readonly Lazy mpd; 21 | 22 | private readonly Lazy walker; 23 | 24 | private readonly int downloadConcurrency; 25 | 26 | public MpdDownloader(Uri mpdUrl, string destinationDir, int downloadConcurrency = 2) 27 | { 28 | if (downloadConcurrency < 1) 29 | throw new ArgumentException("downloadConcurrency cannot be less than 1.", "downloadConcurrency"); 30 | 31 | this.mpdUrl = mpdUrl; 32 | this.destinationDir = destinationDir; 33 | this.downloadConcurrency = downloadConcurrency; 34 | 35 | mpdFileName = new Lazy(GetMpdFileName); 36 | mpd = new Lazy(DownloadMpd); 37 | walker = new Lazy(CreateMpdWalker); 38 | } 39 | 40 | public IEnumerable GetTracksFor(TrackContentType type) 41 | { 42 | return walker.Value.GetTracksFor(type); 43 | } 44 | 45 | public Task> Download(TrackRepresentation trackRepresentation) 46 | { 47 | return Task.Factory.StartNew(() => DownloadTrackRepresentation(trackRepresentation, TimeSpan.Zero, TimeSpan.MaxValue)); 48 | } 49 | 50 | public Task> Download(TrackRepresentation trackRepresentation, TimeSpan from, TimeSpan to) 51 | { 52 | return Task.Factory.StartNew(() => DownloadTrackRepresentation(trackRepresentation, from, to)); 53 | } 54 | 55 | public FileInfo CombineChunksFast(IEnumerable chunks, Action ffmpegRunner) 56 | { 57 | string concatFile = Path.Combine(Path.GetDirectoryName(chunks.First().Path), string.Format("{0:yyyyMMddHHmmssfffffff}_concat.mp4", DateTime.Now)); 58 | string outFile = concatFile.Replace("_concat.mp4", "_video.mp4"); 59 | 60 | if (File.Exists(concatFile)) 61 | File.Delete(concatFile); 62 | if (File.Exists(outFile)) 63 | File.Delete(outFile); 64 | 65 | var initFile = chunks.OfType().First(); 66 | var files = chunks.ToList(); 67 | files.Remove(initFile); 68 | files.Insert(0, initFile); 69 | 70 | ConcatFiles(files.Select(f => f.Path), concatFile); 71 | 72 | ffmpegRunner(string.Format("-i \"concat:{0}\" -c copy {1}", ConvertPathForFfmpeg(concatFile), ConvertPathForFfmpeg(outFile))); 73 | 74 | return new FileInfo(outFile); 75 | } 76 | 77 | private void ConcatFiles(IEnumerable files, string outFile) 78 | { 79 | using (var stream = File.OpenWrite(outFile)) 80 | { 81 | foreach (var f in files) 82 | { 83 | var bytes = File.ReadAllBytes(f); 84 | stream.Write(bytes, 0, bytes.Length); 85 | } 86 | } 87 | } 88 | 89 | public FileInfo CombineChunksFastOld(IEnumerable chunks, Action ffmpegRunner, int maxCmdLength = 32672) 90 | { 91 | var cmdBuilder = new StringBuilder(maxCmdLength); 92 | var initFile = chunks.OfType().First(); 93 | var files = chunks.Except(new[] { initFile }).ToList(); 94 | 95 | cmdBuilder.AppendFormat("-i \"concat:{0}", ConvertPathForFfmpeg(initFile.Path)); 96 | string outputFile = Path.Combine(Path.GetDirectoryName(chunks.First().Path), string.Format("{0:yyyyMMddHHmmssfffffff}_combined.mp4", DateTime.Now)); 97 | if (File.Exists(outputFile)) 98 | File.Delete(outputFile); 99 | string cmdEnd = "\" -c copy " + ConvertPathForFfmpeg(outputFile); 100 | 101 | bool overflow = false; 102 | while (!overflow && files.Any()) 103 | { 104 | string toAppend = "|" + ConvertPathForFfmpeg(files[0].Path); 105 | if (cmdBuilder.Length + toAppend.Length + cmdEnd.Length > maxCmdLength) 106 | overflow = true; 107 | else 108 | { 109 | cmdBuilder.Append(toAppend); 110 | files.RemoveAt(0); 111 | } 112 | } 113 | cmdBuilder.Append(cmdEnd); 114 | 115 | ffmpegRunner(cmdBuilder.ToString()); 116 | cmdBuilder.Clear(); 117 | 118 | FileInfo res; 119 | if (files.Any()) 120 | { 121 | files.Insert(0, new Mp4InitFile(outputFile)); 122 | res = CombineChunksFastOld(files, ffmpegRunner, maxCmdLength); 123 | } 124 | else 125 | res = new FileInfo(outputFile); 126 | 127 | return res; 128 | } 129 | 130 | public FileInfo CombineChunks(IEnumerable chunks, Action ffmpegRunner) 131 | { 132 | chunks = ProcessChunks(chunks); 133 | 134 | string tempFile = Path.Combine(Path.GetDirectoryName(chunks.First().Path), string.Format("{0:yyyyMMddHHmmss}_temp.mp4", DateTime.Now)); 135 | foreach (var c in chunks) 136 | { 137 | ffmpegRunner(string.Format( 138 | @"-i ""{0}"" -filter:v ""setpts=PTS-STARTPTS"" -f mp4 ""{1}""", 139 | ConvertPathForFfmpeg(c.Path), 140 | ConvertPathForFfmpeg(tempFile))); 141 | File.Delete(c.Path); 142 | File.Move(tempFile, c.Path); 143 | } 144 | File.Delete(tempFile); 145 | 146 | string filesListFile = Path.Combine(Path.GetDirectoryName(chunks.First().Path), string.Format("{0:yyyyMMddHHmmss}_list.txt", DateTime.Now)); 147 | File.WriteAllText(filesListFile, string.Join("", chunks.Select(c => string.Format("file '{0}'\r\n", Path.GetFileName(c.Path))))); 148 | string outFile = Path.Combine(Path.GetDirectoryName(chunks.First().Path), string.Format("{0:yyyyMMddHHmmss}_combined.mp4", DateTime.Now)); 149 | if (File.Exists(outFile)) 150 | File.Delete(outFile); 151 | ffmpegRunner(string.Format( 152 | @"-f concat -i ""{0}"" -c copy ""{1}""", 153 | ConvertPathForFfmpeg(filesListFile), 154 | ConvertPathForFfmpeg(outFile))); 155 | File.Delete(filesListFile); 156 | 157 | return new FileInfo(outFile); 158 | } 159 | 160 | private string ConvertPathForFfmpeg(string path) 161 | { 162 | return path.Replace("\\", "/"); 163 | } 164 | 165 | private IEnumerable DownloadTrackRepresentation(TrackRepresentation trackRepresentation, TimeSpan from, TimeSpan to, int concurrency = 2) 166 | { 167 | string initFile; 168 | var files = new List(); 169 | 170 | var task = DownloadFragment(trackRepresentation.InitFragmentPath); 171 | task.Wait(TimeSpan.FromMinutes(5)); 172 | if (DownloadTaskSucceded(task)) 173 | { 174 | initFile = task.Result; 175 | 176 | bool complete = false; 177 | int downloadedCount = 0; 178 | while (!complete) 179 | { 180 | var tasks = trackRepresentation.GetFragmentsPaths(from, to) 181 | .Skip(downloadedCount) 182 | .Take(concurrency) 183 | .Select(p => DownloadFragment(p)) 184 | .ToList(); 185 | tasks.ForEach(t => t.Wait(TimeSpan.FromMinutes(5))); 186 | var succeeded = tasks.Where(t => DownloadTaskSucceded(t)).ToList(); 187 | succeeded.ForEach(t => files.Add(t.Result)); 188 | 189 | downloadedCount += succeeded.Count; 190 | complete = !tasks.Any() || succeeded.Count != tasks.Count; 191 | } 192 | 193 | //var chunks = ProcessChunks(initFile, files); 194 | var chunks = files.Select(f => new Mp4File(f)).ToList(); 195 | chunks.Insert(0, new Mp4InitFile(initFile)); 196 | 197 | //DeleteAllFilesExcept(outputFile, destinationDir); 198 | 199 | return chunks; 200 | } 201 | else 202 | throw new Exception("Failed to download init file"); 203 | } 204 | 205 | private IEnumerable ProcessChunks(IEnumerable files) 206 | { 207 | List res; 208 | 209 | var initFile = files.OfType().FirstOrDefault(); 210 | if (initFile != null) 211 | { 212 | res = new List(); 213 | 214 | var initFileBytes = File.ReadAllBytes(initFile.Path); 215 | foreach (var f in files) 216 | { 217 | var bytes = File.ReadAllBytes(f.Path); 218 | if (!bytes.StartsWith(initFileBytes)) 219 | { 220 | bytes = initFileBytes.Concat(bytes).ToArray(); 221 | File.WriteAllBytes(f.Path, bytes); 222 | res.Add(f); 223 | } 224 | } 225 | } 226 | else 227 | res = new List(files); 228 | 229 | return res; 230 | 231 | //string outputFile = Path.Combine(destinationDir, DateTime.Now.ToString("yyyyMMddHHmmss") + "_video.mp4"); 232 | //using (var stream = File.OpenWrite(outputFile)) 233 | //using (var writer = new BinaryWriter(stream)) 234 | //{ 235 | //foreach (var f in files.Skip(1)) 236 | //{ 237 | // var bytes = File.ReadAllBytes(f); 238 | // var mdatBytes = Encoding.ASCII.GetBytes("mdat"); 239 | // int offset = FindAtomOffset(bytes, mdatBytes); 240 | // //if (offset >= 0) 241 | // // writer.Write(bytes, offset - mdatBytes.Length, bytes.Length - offset + mdatBytes.Length); 242 | // writer.Write(bytes); 243 | //} 244 | //} 245 | } 246 | 247 | private int FindAtomOffset(byte[] chunkBytes, byte[] atomBytes) 248 | { 249 | int mdatOffset = -1; 250 | for (int i = 0; i < chunkBytes.Length - atomBytes.Length && mdatOffset < 0; i++) 251 | { 252 | int matchCount = 0; 253 | for (int j = 0; j < atomBytes.Length; j++) 254 | { 255 | if (chunkBytes[i + j] == atomBytes[j]) 256 | matchCount++; 257 | } 258 | if (matchCount == atomBytes.Length) 259 | mdatOffset = i; 260 | } 261 | return mdatOffset; 262 | } 263 | 264 | private void DeleteAllFilesExcept(string outputFile, string destinationDir) 265 | { 266 | outputFile = Path.GetFullPath(outputFile); 267 | var files = Directory.GetFiles(destinationDir); 268 | mpd.Value.Dispose(); 269 | foreach (var f in files) 270 | { 271 | string file = Path.GetFullPath(f); 272 | if (outputFile != file) 273 | File.Delete(file); 274 | } 275 | } 276 | 277 | public Task Download() 278 | { 279 | var tasks = new List(); 280 | 281 | foreach (var period in mpd.Value.Periods) 282 | { 283 | foreach (var adaptationSet in period.AdaptationSets) 284 | { 285 | foreach (var representation in adaptationSet.Representations) 286 | { 287 | tasks.Add(DownloadAllFragments(adaptationSet, representation)); 288 | } 289 | } 290 | } 291 | 292 | return Task.Factory.ContinueWhenAll( 293 | tasks.ToArray(), 294 | completed => CombineFragments(mpd.Value, mpdFileName.Value, Path.Combine(Path.GetDirectoryName(mpdFileName.Value), "video.mp4"))); 295 | } 296 | 297 | private Task DownloadAllFragments(MpdAdaptationSet adaptationSet, MpdRepresentation representation) 298 | { 299 | return Task.Factory.StartNew(() => DownloadFragmentsUntilFirstFailure(adaptationSet, representation)); 300 | } 301 | 302 | private string CombineFragments(MediaPresentationDescription mpd, string mpdFilePath, string outputFilePath) 303 | { 304 | var walker = new MpdWalker(mpd); 305 | var track = walker.GetTracksFor(TrackContentType.Video).First(); 306 | var trackRepresentation = track.TrackRepresentations.OrderByDescending(r => r.Bandwidth).First(); 307 | 308 | using (var stream = File.OpenWrite(outputFilePath)) 309 | using (var writer = new BinaryWriter(stream)) 310 | { 311 | string fragmentPath = Path.Combine(Path.GetDirectoryName(mpdFilePath), trackRepresentation.InitFragmentPath); 312 | writer.Write(File.ReadAllBytes(fragmentPath)); 313 | 314 | foreach (var path in trackRepresentation.FragmentsPaths) 315 | { 316 | fragmentPath = Path.Combine(Path.GetDirectoryName(mpdFilePath), path); 317 | if (!File.Exists(fragmentPath)) 318 | break; 319 | writer.Write(File.ReadAllBytes(fragmentPath)); 320 | } 321 | } 322 | 323 | return outputFilePath; 324 | } 325 | 326 | private void DownloadFragmentsUntilFirstFailure(MpdAdaptationSet adaptationSet, MpdRepresentation representation) 327 | { 328 | var task = DownloadRepresentationInitFragment(adaptationSet, representation); 329 | 330 | if (DownloadTaskSucceded(task)) 331 | { 332 | int i = 1; 333 | do 334 | { 335 | task = DownloadRepresentationFragment(adaptationSet, representation, i); 336 | i++; 337 | } 338 | while (DownloadTaskSucceded(task)); 339 | } 340 | } 341 | 342 | private Task DownloadRepresentationInitFragment(MpdAdaptationSet adaptationSet, MpdRepresentation representation) 343 | { 344 | string initUrl = adaptationSet.SegmentTemplate.Initialization 345 | .Replace("$RepresentationID$", representation.Id); 346 | var task = DownloadFragment(initUrl); 347 | task.Wait(TimeSpan.FromMinutes(5)); 348 | 349 | return task; 350 | } 351 | 352 | private Task DownloadRepresentationFragment(MpdAdaptationSet adaptationSet, MpdRepresentation representation, int index) 353 | { 354 | string fragmentUrl = adaptationSet.SegmentTemplate.Media 355 | .Replace("$RepresentationID$", representation.Id) 356 | .Replace("$Number$", index.ToString()); 357 | 358 | var task = DownloadFragment(fragmentUrl); 359 | task.Wait(TimeSpan.FromMinutes(5)); 360 | 361 | return task; 362 | } 363 | 364 | private Task DownloadFragment(string fragmentUrl) 365 | { 366 | using (var client = new WebClient()) 367 | { 368 | var url = IsAbsoluteUrl(fragmentUrl) 369 | ? new Uri(fragmentUrl) 370 | : mpd.Value.BaseURL != null 371 | ? new Uri(mpd.Value.BaseURL + fragmentUrl) 372 | : new Uri(mpdUrl, fragmentUrl); 373 | 374 | string destPath = Path.Combine(destinationDir, GetLastPartOfPath(fragmentUrl)); 375 | if (string.IsNullOrWhiteSpace(Path.GetExtension(destPath))) 376 | destPath = Path.ChangeExtension(destPath, "mp4"); 377 | if (File.Exists(destPath)) 378 | destPath = Path.Combine(Path.GetDirectoryName(destPath), Path.ChangeExtension((Path.GetFileNameWithoutExtension(destPath) + "_1"), Path.GetExtension(destPath))); 379 | 380 | // create directory recursive 381 | Directory.CreateDirectory(Path.GetDirectoryName(destPath)); 382 | 383 | return Task.Factory.StartNew(() => 384 | { 385 | try 386 | { 387 | client.DownloadFile(url, destPath); 388 | return destPath; 389 | } 390 | catch 391 | { 392 | return null; 393 | } 394 | }); 395 | } 396 | } 397 | 398 | private bool DownloadTaskSucceded(Task task) 399 | { 400 | return task.IsCompleted && !task.IsFaulted && !string.IsNullOrWhiteSpace(task.Result); 401 | } 402 | 403 | private string GetMpdFileName() 404 | { 405 | string mpdFileName = mpdUrl.AbsolutePath; 406 | if (mpdFileName.Contains("/")) 407 | mpdFileName = mpdFileName.Substring(mpdFileName.LastIndexOf("/") + 1); 408 | string mpdPath = Path.Combine(destinationDir, mpdFileName); 409 | 410 | return mpdPath; 411 | } 412 | 413 | private MediaPresentationDescription DownloadMpd() 414 | { 415 | if (!Directory.Exists(destinationDir)) 416 | Directory.CreateDirectory(destinationDir); 417 | else 418 | Directory.GetFiles(destinationDir).ToList().ForEach(f => File.Delete(f)); 419 | 420 | return MediaPresentationDescription.FromUrl(mpdUrl, mpdFileName.Value); 421 | } 422 | 423 | private MpdWalker CreateMpdWalker() 424 | { 425 | return new MpdWalker(mpd.Value); 426 | } 427 | 428 | private string GetLastPartOfPath(string url) 429 | { 430 | string fileName = url; 431 | if (IsAbsoluteUrl(url)) 432 | { 433 | fileName = new Uri(url).AbsolutePath; 434 | if (fileName.Contains("/")) 435 | fileName = fileName.Substring(fileName.LastIndexOf("/") + 1); 436 | } 437 | return fileName; 438 | } 439 | 440 | bool IsAbsoluteUrl(string url) 441 | { 442 | Uri result; 443 | return Uri.TryCreate(url, UriKind.Absolute, out result); 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /DashTools/MpdElement.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace Qoollo.MpegDash 4 | { 5 | public abstract class MpdElement 6 | { 7 | protected readonly XElement node; 8 | 9 | protected readonly XmlAttributeParseHelper helper; 10 | 11 | internal MpdElement(XElement node) 12 | { 13 | this.node = node; 14 | this.helper = new XmlAttributeParseHelper(node); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DashTools/MpdInitialization.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace Qoollo.MpegDash 4 | { 5 | public class MpdInitialization : MpdElement 6 | { 7 | internal MpdInitialization(XElement node) 8 | : base(node) 9 | { 10 | } 11 | 12 | public string SourceUrl 13 | { 14 | get { return node.Attribute("sourceURL")?.Value; } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DashTools/MpdPeriod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Xml.Linq; 6 | 7 | namespace Qoollo.MpegDash 8 | { 9 | public class MpdPeriod : MpdElement 10 | { 11 | internal MpdPeriod(XElement node) 12 | : base(node) 13 | { 14 | this.adaptationSets = new Lazy>(ParseAdaptationSets); 15 | } 16 | 17 | public string Id 18 | { 19 | get { return node.Attribute("id")?.Value; } 20 | } 21 | 22 | public TimeSpan? Start 23 | { 24 | get { return helper.ParseOptionalTimeSpan("start"); } 25 | } 26 | 27 | public TimeSpan? Duration 28 | { 29 | get { return helper.ParseOptionalTimeSpan("duration"); } 30 | } 31 | 32 | public bool BitstreamSwitching 33 | { 34 | get { return helper.ParseOptionalBool("bitstreamSwitching", false); } 35 | } 36 | 37 | public IEnumerable AdaptationSets 38 | { 39 | get { return adaptationSets.Value; } 40 | } 41 | private readonly Lazy> adaptationSets; 42 | 43 | private IEnumerable ParseAdaptationSets() 44 | { 45 | return node.Elements() 46 | .Where(n => n.Name.LocalName == "AdaptationSet") 47 | .Select(n => new MpdAdaptationSet(n)); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /DashTools/MpdRepresentation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Xml.Linq; 4 | 5 | namespace Qoollo.MpegDash 6 | { 7 | public class MpdRepresentation : MpdElement 8 | { 9 | internal MpdRepresentation(XElement node) 10 | : base(node) 11 | { 12 | segmentList = new Lazy(ParseSegmentList); 13 | this.segmentTemplate = new Lazy(ParseSegmentTemplate); 14 | this.baseURL = new Lazy(ParseBaseURL); 15 | } 16 | 17 | public string Id 18 | { 19 | get { return node.Attribute("id").Value; } 20 | } 21 | 22 | public uint Bandwidth 23 | { 24 | get { return helper.ParseUint("bandwidth"); } 25 | } 26 | 27 | public uint? QualityRanking 28 | { 29 | get { return helper.ParseOptionalUint("qualityRanking"); } 30 | } 31 | 32 | public string DependencyId 33 | { 34 | get { return node.Attribute("dependencyId").Value; } 35 | } 36 | 37 | public string MediaStreamStructureId 38 | { 39 | get { return node.Attribute("mediaStreamStructureId").Value; } 40 | } 41 | 42 | public MpdSegmentList SegmentList 43 | { 44 | get { return segmentList.Value; } 45 | } 46 | private readonly Lazy segmentList; 47 | 48 | public MpdSegmentTemplate SegmentTemplate 49 | { 50 | get { return segmentTemplate.Value; } 51 | } 52 | private readonly Lazy segmentTemplate; 53 | 54 | public string BaseURL 55 | { 56 | get { return baseURL.Value; } 57 | } 58 | private readonly Lazy baseURL; 59 | 60 | private MpdSegmentList ParseSegmentList() 61 | { 62 | return node.Elements() 63 | .Where(n => n.Name.LocalName == "SegmentList") 64 | .Select(n => new MpdSegmentList(n)) 65 | .FirstOrDefault(); 66 | } 67 | 68 | private MpdSegmentTemplate ParseSegmentTemplate() 69 | { 70 | return node.Elements() 71 | .Where(n => n.Name.LocalName == "SegmentTemplate") 72 | .Select(n => new MpdSegmentTemplate(n)) 73 | .FirstOrDefault(); 74 | } 75 | private string ParseBaseURL() 76 | { 77 | return node.Elements() 78 | .Where(n => n.Name.LocalName == "BaseURL") 79 | .Select(n => n.Value) 80 | .FirstOrDefault(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /DashTools/MpdSegmentList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | public class MpdSegmentList : MpdElement 9 | { 10 | internal MpdSegmentList(XElement node) 11 | : base(node) 12 | { 13 | initialization = new Lazy(ParseInitialization); 14 | segmentUrls = new Lazy>(ParseSegmentUrls); 15 | } 16 | 17 | public uint? Timescale 18 | { 19 | get { return helper.ParseOptionalUint("timescale"); } 20 | } 21 | 22 | public uint? Duration 23 | { 24 | get { return helper.ParseOptionalUint("duration"); } 25 | } 26 | 27 | public MpdInitialization Initialization 28 | { 29 | get { return initialization.Value; } 30 | } 31 | private readonly Lazy initialization; 32 | 33 | public IEnumerable SegmentUrls 34 | { 35 | get { return segmentUrls.Value; } 36 | } 37 | private readonly Lazy> segmentUrls; 38 | 39 | private MpdInitialization ParseInitialization() 40 | { 41 | return node.Elements() 42 | .Where(n => n.Name.LocalName == "Initialization") 43 | .Select(n => new MpdInitialization(n)) 44 | .FirstOrDefault(); 45 | } 46 | 47 | private IEnumerable ParseSegmentUrls() 48 | { 49 | return node.Elements() 50 | .Where(n => n.Name.LocalName == "SegmentURL") 51 | .Select(n => new MpdSegmentUrl(n)); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /DashTools/MpdSegmentTemplate.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace Qoollo.MpegDash 4 | { 5 | /// 6 | /// Specifies Segment template information. 7 | /// 8 | public class MpdSegmentTemplate : MultipleSegmentBase 9 | { 10 | internal MpdSegmentTemplate(XElement node) 11 | : base(node) 12 | { 13 | } 14 | 15 | /// 16 | /// Specifies the template to create the Media Segment List. 17 | /// 18 | public string Media 19 | { 20 | get { return node.Attribute("media")?.Value; } 21 | } 22 | 23 | /// 24 | /// Specifies the template to create the Index Segment List. 25 | /// If neither the $Number$ nor the $Time$ identifier is included, 26 | /// this provides the URL to a Representation Index. 27 | /// 28 | public string Index 29 | { 30 | get { return node.Attribute("index")?.Value; } 31 | } 32 | 33 | /// 34 | /// Specifies the template to create the Initialization Segment. 35 | /// Neither $Number$ nor the $Time$ identifier shall be included. 36 | /// 37 | public string Initialization 38 | { 39 | get { return node.Attribute("initialization")?.Value; } 40 | } 41 | 42 | /// 43 | /// Specifies the template to create the Bitstream Switching Segment. 44 | /// Neither $Number$ nor the $Time$ identifier shall be included. 45 | /// 46 | public bool BitstreamSwitching 47 | { 48 | get { return helper.ParseOptionalBool("bitstreamSwitching", false); } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /DashTools/MpdSegmentUrl.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace Qoollo.MpegDash 4 | { 5 | public class MpdSegmentUrl : MpdElement 6 | { 7 | internal MpdSegmentUrl(XElement node) 8 | : base(node) 9 | { 10 | } 11 | 12 | public int Index 13 | { 14 | get { return int.Parse(node.Attribute("index").Value); } 15 | } 16 | 17 | public string Media 18 | { 19 | get { return node.Attribute("media")?.Value; } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /DashTools/MpdWalker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | public class MpdWalker 9 | { 10 | private readonly MediaPresentationDescription mpd; 11 | 12 | public MpdWalker(MediaPresentationDescription mpd) 13 | { 14 | this.mpd = mpd; 15 | } 16 | 17 | public IEnumerable GetTracksFor(TrackContentType type) 18 | { 19 | string typeText; 20 | switch (type) 21 | { 22 | case TrackContentType.Video: 23 | typeText = "video"; 24 | break; 25 | case TrackContentType.Audio: 26 | typeText = "video"; 27 | break; 28 | case TrackContentType.Text: 29 | typeText = "video"; 30 | break; 31 | default: 32 | throw new ArgumentException($"Unexpected TrackContentType: {type}."); 33 | } 34 | 35 | return mpd.Periods 36 | .SelectMany(p => p.AdaptationSets.Where(a => a.ContentType.Contains(typeText)).Select(a => new Track(a))); 37 | } 38 | 39 | public IEnumerable GetAllFragmentsUrls() 40 | { 41 | var res = new List(); 42 | 43 | //mpd.Periods 44 | // .SelectMany(p => p.AdaptationSets) 45 | // .SelectMany(a => a. 46 | 47 | return res; 48 | } 49 | 50 | /// 51 | /// https://github.com/Dash-Industry-Forum/dash.js/blob/ea24350b2df53be20d566603a401fb59bdfae416/src/dash/models/DashManifestModel.js 52 | /// 53 | /// 54 | //private IEnumerable GetRegularPeriods() 55 | //{ 56 | // var periods = mpd.Periods 57 | // .Select(p => new Period { Id = p.Id, Start = p.Start.Value, Duration = p.Duration.Value }) 58 | // .ToList(); 59 | // Period vo1 = null, vo = null, p1 = null; 60 | 61 | // for (int i = 0; i < periods.Count; i++) 62 | // { 63 | // var p = periods[i]; 64 | 65 | // // If the attribute @start is present in the Period, then the 66 | // // Period is a regular Period and the PeriodStart is equal 67 | // // to the value of this attribute. 68 | // if (p.Start.HasValue) 69 | // { 70 | // vo = new Period { Start = p.Start.Value }; 71 | // } 72 | // // If the @start attribute is absent, but the previous Period 73 | // // element contains a @duration attribute then then this new 74 | // // Period is also a regular Period. The start time of the new 75 | // // Period PeriodStart is the sum of the start time of the previous 76 | // // Period PeriodStart and the value of the attribute @duration 77 | // // of the previous Period. 78 | // else if (p1 != null && p.Duration.HasValue && vo1 != null) 79 | // { 80 | // vo = new Period(); 81 | // vo.Start = vo1.Start + vo1.Duration; 82 | // vo.Duration = p.Duration.Value; 83 | // } 84 | // // If (i) @start attribute is absent, and (ii) the Period element 85 | // // is the first in the MPD, and (iii) the MPD@type is 'static', 86 | // // then the PeriodStart time shall be set to zero. 87 | // else if (i == 0 && mpd.Type != "dynamic") 88 | // { 89 | // vo = new Period(); 90 | // vo.Start = TimeSpan.Zero; 91 | // } 92 | 93 | // // The Period extends until the PeriodStart of the next Period. 94 | // // The difference between the PeriodStart time of a Period and 95 | // // the PeriodStart time of the following Period. 96 | // if (vo1 != null && vo1.Duration == default(TimeSpan)) 97 | // { 98 | // vo1.Duration = vo.Start - vo1.Start; 99 | // } 100 | 101 | // if (vo != null) 102 | // { 103 | // vo.Id = GetPeriodId(p); 104 | // } 105 | 106 | // if (vo != null && p.Duration.HasValue) 107 | // { 108 | // vo.Duration = p.Duration.Value; 109 | // } 110 | 111 | // if (vo != null) 112 | // { 113 | // vo.index = i; 114 | // vo.mpd = mpd; 115 | // periods.Add(vo); 116 | // p1 = p; 117 | // vo1 = vo; 118 | // } 119 | 120 | // p = null; 121 | // vo = null; 122 | // } 123 | 124 | // if (periods.Any() && vo1 != null && !vo1.Duration.HasValue) 125 | // vo1.Duration = GetEndTimeForLastPeriod(vo1) - vo1.Start.Value; 126 | 127 | // return periods; 128 | //} 129 | 130 | //private TimeSpan GetEndTimeForLastPeriod(Period period) 131 | //{ 132 | // var periodEnd; 133 | // var checkTime = GetCheckTime(period); 134 | 135 | // // if the MPD@mediaPresentationDuration attribute is present, then PeriodEndTime is defined as the end time of the Media Presentation. 136 | // // if the MPD@mediaPresentationDuration attribute is not present, then PeriodEndTime is defined as FetchTime + MPD@minimumUpdatePeriod 137 | 138 | // if (manifest.mediaPresentationDuration) 139 | // { 140 | // periodEnd = manifest.mediaPresentationDuration; 141 | // } 142 | // else if (!isNaN(checkTime)) 143 | // { 144 | // // in this case the Period End Time should match CheckTime 145 | // periodEnd = checkTime; 146 | // } 147 | // else { 148 | // throw new Error('Must have @mediaPresentationDuration or @minimumUpdatePeriod on MPD or an explicit @duration on the last period.'); 149 | // } 150 | 151 | // return periodEnd; 152 | //} 153 | 154 | //private object GetCheckTime(Period period) 155 | //{ 156 | // var checkTime = NaN; 157 | // var fetchTime; 158 | 159 | // // If the MPD@minimumUpdatePeriod attribute in the client is provided, then the check time is defined as the 160 | // // sum of the fetch time of this operating MPD and the value of this attribute, 161 | // // i.e. CheckTime = FetchTime + MPD@minimumUpdatePeriod. 162 | // if (mpd.MinimumUpdatePeriod.HasValue) 163 | // { 164 | // fetchTime = GetFetchTime(period); 165 | // checkTime = fetchTime + manifest.minimumUpdatePeriod; 166 | // } 167 | // // TODO If the MPD@minimumUpdatePeriod attribute in the client is not provided, external means are used to 168 | // // determine CheckTime, such as a priori knowledge, or HTTP cache headers, etc. 169 | 170 | // return checkTime; 171 | //} 172 | 173 | //private object GetFetchTime(Period period) 174 | //{ 175 | // // FetchTime is defined as the time at which the server processes the request for the MPD from the client. 176 | // // TODO The client typically should not use the time at which it actually successfully received the MPD, but should 177 | // // take into account delay due to MPD delivery and processing. The fetch is considered successful fetching 178 | // // either if the client obtains an updated MPD or the client verifies that the MPD has not been updated since the previous fetching. 179 | 180 | // return timelineConverter.calcPresentationTimeFromWallTime(mpd.loadedTime, period); 181 | //} 182 | 183 | private string GetPeriodId(Period period) 184 | { 185 | if (period == null) 186 | throw new ArgumentNullException(nameof(period)); 187 | 188 | var res = Period.DEFAULT_ID; 189 | 190 | if (!string.IsNullOrWhiteSpace(period.Id)) 191 | { 192 | res = period.Id; 193 | } 194 | 195 | return res; 196 | } 197 | 198 | class Period 199 | { 200 | public const string DEFAULT_ID = "defaultId"; 201 | 202 | public string Id { get; set; } 203 | 204 | public TimeSpan? Start { get; set; } 205 | 206 | public TimeSpan? Duration { get; set; } 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /DashTools/MultipleSegmentBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Xml.Linq; 3 | 4 | namespace Qoollo.MpegDash 5 | { 6 | /// 7 | /// Specifies multiple Segment base information. 8 | /// 9 | public abstract class MultipleSegmentBase : SegmentBase 10 | { 11 | internal MultipleSegmentBase(XElement node) 12 | : base(node) 13 | { 14 | } 15 | 16 | public uint? Duration 17 | { 18 | get { return helper.ParseOptionalUint("duration"); } 19 | } 20 | 21 | public uint? StartNumber 22 | { 23 | get { return helper.ParseOptionalUint("startNumber"); } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /DashTools/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DashTools")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DashTools")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("d517b927-7afc-42ad-a410-27354f3217c2")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion(Qoollo.AssemblySettings.AssemblyVersion)] 36 | [assembly: AssemblyFileVersion(Qoollo.AssemblySettings.AssemblyVersion)] 37 | 38 | namespace Qoollo 39 | { 40 | internal static class AssemblySettings 41 | { 42 | public const string AssemblyVersion = "0.1.12"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DashTools/SegmentBase.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Linq; 2 | 3 | namespace Qoollo.MpegDash 4 | { 5 | public class SegmentBase : MpdElement 6 | { 7 | internal SegmentBase(XElement node) 8 | : base(node) 9 | { 10 | } 11 | 12 | public uint? Timescale 13 | { 14 | get { return helper.ParseOptionalUint("timescale"); } 15 | } 16 | 17 | public ulong? PresentationTimeOffset 18 | { 19 | get { return helper.ParseOptionalUlong("presentationTimeOffset"); } 20 | } 21 | 22 | public string IndexRange 23 | { 24 | get { return node.Attribute("indexRange")?.Value; } 25 | } 26 | 27 | public bool IndexRangeExact 28 | { 29 | get { return helper.ParseOptionalBool("indexRangeExact", false); } 30 | } 31 | 32 | public double? AvailabilityTimeOffset 33 | { 34 | get { return helper.ParseOptionalDouble("availabilityTimeOffset"); } 35 | } 36 | 37 | public bool AvailabilityTimeComplete 38 | { 39 | get { return helper.ParseOptionalBool("availabilityTimeComplete", false); } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /DashTools/Track.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Mime; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | public class Track 9 | { 10 | private readonly MpdAdaptationSet adaptationSet; 11 | 12 | public Track(MpdAdaptationSet adaptationSet) 13 | { 14 | this.adaptationSet = adaptationSet; 15 | 16 | trackRepresentations = new Lazy>(GetTrackRepresentations); 17 | } 18 | 19 | public ContentType ContentType 20 | { 21 | get { return new ContentType(adaptationSet.ContentType); } 22 | } 23 | 24 | public IEnumerable TrackRepresentations 25 | { 26 | get { return trackRepresentations.Value; } 27 | } 28 | private readonly Lazy> trackRepresentations; 29 | 30 | private IEnumerable GetTrackRepresentations() 31 | { 32 | return adaptationSet.Representations.Select(r => new TrackRepresentation(adaptationSet, r)); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /DashTools/TrackContentType.cs: -------------------------------------------------------------------------------- 1 | namespace Qoollo.MpegDash 2 | { 3 | public enum TrackContentType 4 | { 5 | Video, 6 | Audio, 7 | Text 8 | } 9 | } -------------------------------------------------------------------------------- /DashTools/TrackRepresentation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Qoollo.MpegDash 6 | { 7 | public class TrackRepresentation 8 | { 9 | private readonly MpdAdaptationSet adaptationSet; 10 | private readonly MpdRepresentation representation; 11 | 12 | public TrackRepresentation(MpdAdaptationSet adaptationSet, MpdRepresentation representation) 13 | { 14 | this.adaptationSet = adaptationSet; 15 | this.representation = representation; 16 | 17 | initFragmentPath = new Lazy(GetInitFragmentPath); 18 | fragmentsPaths = new Lazy>(GetFragmentsPaths); 19 | } 20 | 21 | public string InitFragmentPath 22 | { 23 | get { return initFragmentPath.Value; } 24 | } 25 | private readonly Lazy initFragmentPath; 26 | 27 | public IEnumerable FragmentsPaths 28 | { 29 | get { return fragmentsPaths.Value; } 30 | } 31 | private readonly Lazy> fragmentsPaths; 32 | 33 | public uint Bandwidth 34 | { 35 | get { return representation.Bandwidth; } 36 | } 37 | 38 | private string GetInitFragmentPath() 39 | { 40 | string res; 41 | 42 | var segmentTemplate = adaptationSet.SegmentTemplate ?? representation.SegmentTemplate; 43 | if (segmentTemplate != null) 44 | res = segmentTemplate.Initialization 45 | .Replace("$RepresentationID$", representation.Id); 46 | else if (representation.SegmentList != null) 47 | res = representation.SegmentList.Initialization.SourceUrl; 48 | else if (representation.BaseURL != null) 49 | res = representation.BaseURL; 50 | else 51 | throw new Exception("Failed to determine InitFragmentPath"); 52 | 53 | return res; 54 | } 55 | 56 | private IEnumerable GetFragmentsPaths() 57 | { 58 | var segmentTemplate = adaptationSet.SegmentTemplate ?? representation.SegmentTemplate; 59 | if (segmentTemplate != null) 60 | { 61 | int i = 1; 62 | while (true) 63 | { 64 | yield return segmentTemplate.Media 65 | .Replace("$RepresentationID$", representation.Id) 66 | .Replace("$Number$", i.ToString()); 67 | i++; 68 | } 69 | } 70 | else if (representation.SegmentList != null) 71 | { 72 | foreach (var segmentUrl in representation.SegmentList.SegmentUrls.OrderBy(s => s.Index)) 73 | { 74 | yield return segmentUrl.Media; 75 | } 76 | } 77 | else 78 | throw new Exception("Failed to determine FragmentPath"); 79 | } 80 | 81 | private IEnumerable GetSegments() 82 | { 83 | var segmentTemplate = adaptationSet.SegmentTemplate ?? representation.SegmentTemplate; 84 | if (segmentTemplate != null) 85 | { 86 | int i = 1; 87 | while (true) 88 | { 89 | yield return new TrackRepresentationSegment 90 | { 91 | Path = segmentTemplate.Media 92 | .Replace("$RepresentationID$", representation.Id) 93 | .Replace("$Number$", i.ToString()), 94 | Duration = TimeSpan.FromMilliseconds(segmentTemplate.Duration.Value) 95 | }; 96 | i++; 97 | } 98 | } 99 | else if (representation.SegmentList != null) 100 | { 101 | foreach (var segmentUrl in representation.SegmentList.SegmentUrls.OrderBy(s => s.Index)) 102 | { 103 | yield return new TrackRepresentationSegment 104 | { 105 | Path = segmentUrl.Media, 106 | Duration = TimeSpan.FromMilliseconds(representation.SegmentList.Duration.Value) 107 | }; 108 | } 109 | } 110 | else 111 | // no segments (it's possible) 112 | yield break; 113 | } 114 | 115 | internal IEnumerable GetFragmentsPaths(TimeSpan from, TimeSpan to) 116 | { 117 | var span = TimeSpan.Zero; 118 | foreach (var segment in GetSegments()) 119 | { 120 | if (span >= from && span + segment.Duration <= to) 121 | yield return segment.Path; 122 | 123 | span += segment.Duration; 124 | 125 | if (span > to) 126 | break; 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /DashTools/TrackRepresentationSegment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Qoollo.MpegDash 7 | { 8 | class TrackRepresentationSegment 9 | { 10 | public TimeSpan Duration { get; set; } 11 | 12 | public string Path { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DashTools/XmlAttributeParseHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Xml; 6 | using System.Xml.Linq; 7 | 8 | namespace Qoollo.MpegDash 9 | { 10 | public class XmlAttributeParseHelper 11 | { 12 | private readonly XElement node; 13 | 14 | public XmlAttributeParseHelper(XElement node) 15 | { 16 | this.node = node; 17 | } 18 | 19 | public DateTimeOffset? ParseDateTimeOffset(string attributeName, bool mandatoryCondition) 20 | { 21 | if (!mandatoryCondition && node.Attribute(attributeName) == null) 22 | throw new Exception($"MPD attribute @{attributeName} should be present."); 23 | return ParseOptionalDateTimeOffset(attributeName); 24 | } 25 | 26 | public uint ParseUint(string attributeName) 27 | { 28 | return uint.Parse(node.Attribute("bandwidth").Value); 29 | } 30 | 31 | public DateTimeOffset? ParseOptionalDateTimeOffset(string attributeName, DateTimeOffset? defaultValue = null) 32 | { 33 | var attr = node.Attribute(attributeName); 34 | return attr == null 35 | ? defaultValue 36 | : DateTimeOffset.Parse(attr.Value); 37 | } 38 | 39 | public TimeSpan? ParseOptionalTimeSpan(string attributeName, TimeSpan? defaultValue = null) 40 | { 41 | var attr = node.Attribute(attributeName); 42 | return attr == null 43 | ? defaultValue 44 | : XmlConvert.ToTimeSpan(attr.Value); 45 | } 46 | 47 | public bool ParseOptionalBool(string attributeName, bool defaultValue) 48 | { 49 | var attr = node.Attribute(attributeName); 50 | return attr == null 51 | ? defaultValue 52 | : bool.Parse(attr.Value); 53 | } 54 | 55 | public uint? ParseOptionalUint(string attributeName, uint? defaultValue = null) 56 | { 57 | var attr = node.Attribute(attributeName); 58 | return attr == null 59 | ? defaultValue 60 | : uint.Parse(attr.Value); 61 | } 62 | 63 | public ulong? ParseOptionalUlong(string attributeName, ulong? defaultValue = null) 64 | { 65 | var attr = node.Attribute(attributeName); 66 | return attr == null 67 | ? defaultValue 68 | : ulong.Parse(attr.Value); 69 | } 70 | 71 | public double? ParseOptionalDouble(string attributeName, double? defaultValue = null) 72 | { 73 | var attr = node.Attribute(attributeName); 74 | return attr == null 75 | ? defaultValue 76 | : double.Parse(attr.Value); 77 | } 78 | 79 | public AspectRatio ParseOptionalAspectRatio(string attributeName, AspectRatio defaultValue = null) 80 | { 81 | var attr = node.Attribute(attributeName); 82 | return attr == null 83 | ? defaultValue 84 | : new AspectRatio(attr.Value); 85 | } 86 | 87 | public FrameRate ParseOptionalFrameRate(string attributeName, FrameRate defaultValue = null) 88 | { 89 | var attr = node.Attribute(attributeName); 90 | return attr == null 91 | ? defaultValue 92 | : new FrameRate(attr.Value); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Qoollo 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 | -------------------------------------------------------------------------------- /NuGet/DashTools.csproj.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Qoollo.MpegDash 5 | MPEG-DASH tools. MPD file parser, downloader. 6 | $version$ 7 | Qoollo 8 | Qoollo 9 | https://raw.githubusercontent.com/qoollo/SharpDashTools/master/LICENSE 10 | https://github.com/qoollo/SharpDashTools 11 | https://avatars3.githubusercontent.com/u/8374105?s=64 12 | false 13 | Initial version. 14 | Copyright 2016 15 | mpeg dash download parse mpd 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SharpDashTools 2 | 3 | MPEG-DASH tools in C#. 4 | -------------------------------------------------------------------------------- /SharpDashTools.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.24720.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashTools", "DashTools\DashTools.csproj", "{D517B927-7AFC-42AD-A410-27354F3217C2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashTools.Tests", "DashTools.Tests\DashTools.Tests.csproj", "{17D61727-C15B-4329-AC4A-381D174E76EB}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuGet", "NuGet", "{AA1B4699-9D5B-4FC4-BB4C-3B3CE5ADC180}" 11 | ProjectSection(SolutionItems) = preProject 12 | NuGet\DashTools.csproj.nuspec = NuGet\DashTools.csproj.nuspec 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DashTools.Samples", "DashTools.Samples\DashTools.Samples.csproj", "{19125071-F9C9-4F10-AA66-621DD65F3ABE}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {D517B927-7AFC-42AD-A410-27354F3217C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {D517B927-7AFC-42AD-A410-27354F3217C2}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {D517B927-7AFC-42AD-A410-27354F3217C2}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {D517B927-7AFC-42AD-A410-27354F3217C2}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {17D61727-C15B-4329-AC4A-381D174E76EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {17D61727-C15B-4329-AC4A-381D174E76EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {17D61727-C15B-4329-AC4A-381D174E76EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {17D61727-C15B-4329-AC4A-381D174E76EB}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {19125071-F9C9-4F10-AA66-621DD65F3ABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {19125071-F9C9-4F10-AA66-621DD65F3ABE}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {19125071-F9C9-4F10-AA66-621DD65F3ABE}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {19125071-F9C9-4F10-AA66-621DD65F3ABE}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | EndGlobal 40 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [ValidateSet("vs2013", "vs2015")] 3 | [Parameter(Position = 0)] 4 | [string] $Target = "vs2015", 5 | [Parameter(Position = 1)] 6 | [string] $Version = "0.1.12", 7 | [Parameter(Position = 2)] 8 | [string] $AssemblyVersion = "0.1.12" 9 | ) 10 | 11 | function Write-Diagnostic 12 | { 13 | param( 14 | [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] 15 | [string] $Message 16 | ) 17 | 18 | Write-Host 19 | Write-Host $Message -ForegroundColor Green 20 | Write-Host 21 | } 22 | 23 | # https://github.com/jbake/Powershell_scripts/blob/master/Invoke-BatchFile.ps1 24 | function Invoke-BatchFile 25 | { 26 | param( 27 | [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] 28 | [string]$Path, 29 | [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true)] 30 | [string]$Parameters 31 | ) 32 | 33 | $tempFile = [IO.Path]::GetTempFileName() 34 | 35 | cmd.exe /c " `"$Path`" $Parameters && set > `"$tempFile`" " 36 | 37 | Get-Content $tempFile | Foreach-Object { 38 | if ($_ -match "^(.*?)=(.*)$") 39 | { 40 | Set-Content "env:\$($matches[1])" $matches[2] 41 | } 42 | } 43 | 44 | Remove-Item $tempFile 45 | } 46 | 47 | function Die 48 | { 49 | param( 50 | [Parameter(Position = 0, ValueFromPipeline = $true)] 51 | [string] $Message 52 | ) 53 | 54 | Write-Host 55 | Write-Error $Message 56 | exit 1 57 | } 58 | 59 | function Warn 60 | { 61 | param( 62 | [Parameter(Position = 0, ValueFromPipeline = $true)] 63 | [string] $Message 64 | ) 65 | 66 | Write-Host 67 | Write-Host $Message -ForegroundColor Yellow 68 | Write-Host 69 | } 70 | 71 | function TernaryReturn 72 | { 73 | param( 74 | [Parameter(Position = 0, ValueFromPipeline = $true)] 75 | [bool] $Yes, 76 | [Parameter(Position = 1, ValueFromPipeline = $true)] 77 | $Value, 78 | [Parameter(Position = 2, ValueFromPipeline = $true)] 79 | $Value2 80 | ) 81 | 82 | if($Yes) { 83 | return $Value 84 | } 85 | 86 | $Value2 87 | } 88 | 89 | function Msvs 90 | { 91 | param( 92 | [ValidateSet('v120', 'v140')] 93 | [Parameter(Position = 0, ValueFromPipeline = $true)] 94 | [string] $Toolchain, 95 | 96 | [Parameter(Position = 1, ValueFromPipeline = $true)] 97 | [ValidateSet('Debug', 'Release')] 98 | [string] $Configuration, 99 | 100 | [Parameter(Position = 2, ValueFromPipeline = $true)] 101 | [ValidateSet('x86', 'x64', '"Mixed Platforms"', '"Any CPU"')] 102 | [string] $Platform 103 | ) 104 | 105 | Write-Diagnostic "Targeting $Toolchain using configuration $Configuration on platform $Platform" 106 | 107 | $VisualStudioVersion = $null 108 | $VXXCommonTools = $null 109 | 110 | switch -Exact ($Toolchain) { 111 | 'v120' { 112 | $MSBuildExe = join-path -path (Get-ItemProperty "HKLM:\software\Microsoft\MSBuild\ToolsVersions\12.0").MSBuildToolsPath -childpath "msbuild.exe" 113 | $MSBuildExe = $MSBuildExe -replace "Framework64", "Framework" 114 | $VisualStudioVersion = '12.0' 115 | $VXXCommonTools = Join-Path $env:VS120COMNTOOLS '..\..\vc' 116 | } 117 | 'v140' { 118 | $MSBuildExe = join-path -path (Get-ItemProperty "HKLM:\software\Microsoft\MSBuild\ToolsVersions\14.0").MSBuildToolsPath -childpath "msbuild.exe" 119 | $MSBuildExe = $MSBuildExe -replace "Framework64", "Framework" 120 | $VisualStudioVersion = '14.0' 121 | $VXXCommonTools = Join-Path $env:VS140COMNTOOLS '..\..\vc' 122 | } 123 | } 124 | 125 | if ($VXXCommonTools -eq $null -or (-not (Test-Path($VXXCommonTools)))) { 126 | Die 'Error unable to find any visual studio environment' 127 | } 128 | 129 | $VCVarsAll = Join-Path $VXXCommonTools vcvarsall.bat 130 | if (-not (Test-Path $VCVarsAll)) { 131 | Die "Unable to find $VCVarsAll" 132 | } 133 | 134 | # Only configure build environment once 135 | if($env:CEFSHARP_BUILD_IS_BOOTSTRAPPED -eq $null) { 136 | Invoke-BatchFile $VCVarsAll $Platform 137 | $env:CEFSHARP_BUILD_IS_BOOTSTRAPPED = $true 138 | } 139 | 140 | $Arguments = @( 141 | "$slnFile", 142 | "/t:rebuild", 143 | "/p:VisualStudioVersion=$VisualStudioVersion", 144 | "/p:Configuration=$Configuration", 145 | "/p:Platform=$Platform", 146 | "/verbosity:normal" 147 | ) 148 | 149 | $StartInfo = New-Object System.Diagnostics.ProcessStartInfo 150 | $StartInfo.FileName = $MSBuildExe 151 | $StartInfo.Arguments = $Arguments 152 | 153 | $StartInfo.EnvironmentVariables.Clear() 154 | 155 | Get-ChildItem -Path env:* | ForEach-Object { 156 | $StartInfo.EnvironmentVariables.Add($_.Name, $_.Value) 157 | } 158 | 159 | $StartInfo.UseShellExecute = $false 160 | $StartInfo.CreateNoWindow = $false 161 | $StartInfo.RedirectStandardError = $true 162 | $StartInfo.RedirectStandardOutput = $true 163 | 164 | $Process = New-Object System.Diagnostics.Process 165 | $Process.StartInfo = $startInfo 166 | $Process.Start() 167 | 168 | $stdout = $Process.StandardOutput.ReadToEnd() 169 | $stderr = $Process.StandardError.ReadToEnd() 170 | 171 | $Process.WaitForExit() 172 | 173 | if($Process.ExitCode -ne 0) 174 | { 175 | Write-Host "stdout: $stdout" 176 | Write-Host "stderr: $stderr" 177 | Die "Build failed" 178 | } 179 | } 180 | 181 | function VSX 182 | { 183 | param( 184 | [ValidateSet('v120', 'v140')] 185 | [Parameter(Position = 0, ValueFromPipeline = $true)] 186 | [string] $Toolchain 187 | ) 188 | 189 | if($Toolchain -eq 'v120' -and $env:VS120COMNTOOLS -eq $null) { 190 | Warn "Toolchain $Toolchain is not installed on your development machine, skipping build." 191 | Return 192 | } 193 | 194 | if($Toolchain -eq 'v140' -and $env:VS140COMNTOOLS -eq $null) { 195 | Warn "Toolchain $Toolchain is not installed on your development machine, skipping build." 196 | Return 197 | } 198 | 199 | Write-Diagnostic "Starting to build targeting toolchain $Toolchain" 200 | 201 | Msvs "$Toolchain" 'Release' '"Any CPU"' 202 | 203 | Write-Diagnostic "Finished build targeting toolchain $Toolchain" 204 | } 205 | 206 | function NugetPackageRestore 207 | { 208 | $nuget = [IO.Path]::Combine($WorkingDir, 'NuGet', 'NuGet.exe') 209 | if(-not (Test-Path $nuget)) { 210 | Die "Please install nuget. More information available at: http://docs.nuget.org/docs/start-here/installing-nuget" 211 | } 212 | 213 | Write-Diagnostic "Restore Nuget Packages" 214 | 215 | # Restore packages 216 | . $nuget restore $slnFile 217 | } 218 | 219 | function Nupkg 220 | { 221 | if (Test-Path Env:\APPVEYOR_PULL_REQUEST_NUMBER) 222 | { 223 | Write-Diagnostic "Pr Number: $env:APPVEYOR_PULL_REQUEST_NUMBER" 224 | Write-Diagnostic "Skipping Nupkg" 225 | return 226 | } 227 | 228 | $nuget = Join-Path $WorkingDir .\NuGet\NuGet.exe 229 | if(-not (Test-Path $nuget)) { 230 | Die "Please install nuget. More information available at: http://docs.nuget.org/docs/start-here/installing-nuget" 231 | } 232 | 233 | Write-Diagnostic "Building nuget package" 234 | 235 | # Build packages 236 | $nuspec = [IO.Path]::Combine($WorkingDir, 'NuGet', 'DashTools.csproj.nuspec') 237 | . $nuget pack "$nuspec" -NoPackageAnalysis -Symbol -Version $Version -OutputDirectory ([IO.Path]::Combine($WorkingDir, 'NuGet')) 238 | 239 | # Invoke `AfterBuild` script if available (ie. upload packages to myget) 240 | if(-not (Test-Path $WorkingDir\AfterBuild.ps1)) { 241 | return 242 | } 243 | 244 | . $WorkingDir\AfterBuild.ps1 -Version $Version 245 | } 246 | 247 | function DownloadNuget() 248 | { 249 | $nugetDir = [IO.Path]::Combine($WorkingDir, 'NuGet') 250 | $nuget = [IO.Path]::Combine($nugetDir, 'NuGet.exe') 251 | if(-not (Test-Path $nuget)) 252 | { 253 | if (-not (Test-Path $nugetDir)) 254 | { 255 | New-Item -Path $nugetDir -ItemType Directory 256 | } 257 | $client = New-Object System.Net.WebClient; 258 | $client.DownloadFile('http://nuget.org/nuget.exe', $nuget); 259 | } 260 | } 261 | 262 | function WriteAssemblyVersion 263 | { 264 | param() 265 | 266 | $Filename = [IO.Path]::Combine($WorkingDir, 'DashTools', 'Properties', 'AssemblyInfo.cs') 267 | $Regex = 'public const string AssemblyVersion = "(.*)"'; 268 | 269 | $AssemblyInfo = Get-Content $Filename 270 | $NewString = $AssemblyInfo -replace $Regex, "public const string AssemblyVersion = ""$AssemblyVersion""" 271 | 272 | $NewString | Set-Content $Filename -Encoding UTF8 273 | } 274 | 275 | $WorkingDir = split-path -parent $MyInvocation.MyCommand.Definition 276 | 277 | $slnFile = [IO.Path]::Combine($WorkingDir, 'SharpDashTools.sln') 278 | 279 | DownloadNuget 280 | 281 | NugetPackageRestore 282 | 283 | WriteAssemblyVersion 284 | 285 | switch -Exact ($Target) 286 | { 287 | "nupkg-only" 288 | { 289 | Nupkg 290 | } 291 | "vs2013" 292 | { 293 | VSX v120 294 | Nupkg 295 | } 296 | "vs2015" 297 | { 298 | VSX v140 299 | Nupkg 300 | } 301 | } --------------------------------------------------------------------------------