├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Cinegy.TsAnalyzer.sln ├── Cinegy.TsAnalyzer ├── AnalysisService.cs ├── Cinegy.TsAnalyzer.csproj ├── Dockerfile ├── Helpers │ └── Product.cs ├── Program.cs ├── Properties │ ├── PublishProfiles │ │ └── FolderProfile.pubxml │ └── launchSettings.json ├── SerializableModels │ └── Settings │ │ ├── AppConfig.cs │ │ └── MetricsSetting.cs ├── appsettings.Development.json └── appsettings.json ├── LICENSE ├── README.md └── appveyor.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/Cinegy.TsAnalyzer/bin/Debug/netcoreapp3.1/TsAnalyzer.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/Cinegy.TsAnalyzer", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | }, 26 | { 27 | "name": ".NET Core Launch (remote console)", 28 | "type": "coreclr", 29 | "request": "launch", 30 | "preLaunchTask": "build", 31 | "program": "/home/pi/tsanalyser", 32 | //"args": ["/home/pi/tsanalyser"], 33 | "cwd": "/home/pi/", 34 | "stopAtEntry": false, 35 | "console": "internalConsole", 36 | "pipeTransport": { 37 | "pipeCwd": "${workspaceFolder}", 38 | "pipeProgram": "${env:ChocolateyInstall}\\bin\\PLINK.EXE", 39 | "pipeArgs": [ 40 | "-pw", 41 | "raspberry", 42 | "root@crowpi.lan" 43 | ], 44 | "debuggerPath": "/home/pi/vsdbg/vsdbg" 45 | } 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | }, 14 | { 15 | "label": "publish", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "publish", 20 | "${workspaceFolder}/Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj" 21 | ], 22 | "windows": { 23 | "command": "${cwd}\\publish.bat" 24 | }, 25 | "problemMatcher": [] 26 | }, 27 | { 28 | "label": "watch", 29 | "command": "dotnet", 30 | "type": "process", 31 | "args": [ 32 | "watch", 33 | "run", 34 | "${workspaceFolder}/Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj" 35 | ], 36 | "problemMatcher": "$tsc" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33502.453 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cinegy.TsAnalyzer", "Cinegy.TsAnalyzer\Cinegy.TsAnalyzer.csproj", "{49AFFA46-0559-4616-89A7-5D7DF7CD4AC7}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {49AFFA46-0559-4616-89A7-5D7DF7CD4AC7}.Debug|Any CPU.ActiveCfg = Debug|x64 15 | {49AFFA46-0559-4616-89A7-5D7DF7CD4AC7}.Debug|Any CPU.Build.0 = Debug|x64 16 | {49AFFA46-0559-4616-89A7-5D7DF7CD4AC7}.Release|Any CPU.ActiveCfg = Release|x64 17 | {49AFFA46-0559-4616-89A7-5D7DF7CD4AC7}.Release|Any CPU.Build.0 = Release|x64 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {2B346C73-5A87-4A39-BFA7-2D3D40C8CD89} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/AnalysisService.cs: -------------------------------------------------------------------------------- 1 | /* Copyright 2022-2023 Cinegy GmbH. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | using System.Diagnostics; 17 | using System.Diagnostics.Metrics; 18 | using System.Net; 19 | using System.Net.Sockets; 20 | using System.Runtime; 21 | using Cinegy.Srt.Wrapper; 22 | using Cinegy.TsAnalysis; 23 | using Cinegy.TsDecoder.Descriptors; 24 | using Cinegy.TsDecoder.TransportStream; 25 | using Microsoft.Extensions.Configuration; 26 | using Microsoft.Extensions.Hosting; 27 | using Microsoft.Extensions.Logging; 28 | using SrtSharp; 29 | using TsAnalyzer.SerializableModels.Settings; 30 | using LogLevel = SrtSharp.LogLevel; 31 | 32 | namespace Cinegy.TsAnalyzer; 33 | 34 | public class AnalysisService : IHost, IHostedService 35 | { 36 | private readonly ILogger _logger; 37 | private readonly AppConfig _appConfig; 38 | private readonly IHostApplicationLifetime _appLifetime; 39 | private CancellationToken _cancellationToken; 40 | private static ISecureReliableTransport _engine; 41 | private readonly Meter _metricsMeter = new($"Cinegy.TsAnalyzer.{nameof(AnalysisService)}"); 42 | 43 | private const string LineBreak = "---------------------------------------------------------------------"; 44 | private static Analyzer _analyzer; 45 | private static bool _receiving; 46 | private static readonly UdpClient UdpClient = new UdpClient { ExclusiveAddressUse = false }; 47 | private static bool _pendingExit; 48 | 49 | private const int WarmUpTime = 500; 50 | private static bool _warmedUp; 51 | 52 | private static readonly TsPacketFactory Factory = new(); 53 | 54 | private static readonly DateTime StartTime = DateTime.UtcNow; 55 | private static readonly List ConsoleLines = new(1024); 56 | 57 | private const int DefaultChunk = 1316; 58 | 59 | public IServiceProvider Services => throw new NotImplementedException(); 60 | 61 | #region Constructor and IHostedService 62 | 63 | public AnalysisService(ILoggerFactory loggerFactory, IConfiguration configuration, IHostApplicationLifetime appLifetime) 64 | { 65 | _logger = loggerFactory.CreateLogger(); 66 | _appConfig = configuration.Get(); 67 | 68 | _appLifetime = appLifetime; 69 | 70 | var bannedMessages = new HashSet 71 | { 72 | ": srt_accept: no pending connection available at the moment" 73 | }; 74 | 75 | var loggerOptions = new LoggerOptions 76 | { 77 | LogFlags = LogFlag.DisableSeverity | 78 | LogFlag.DisableThreadName | 79 | LogFlag.DisableEOL | 80 | LogFlag.DisableTime, 81 | LogMessageAction = (level, message, area, file, line) => 82 | { 83 | if (bannedMessages.Contains(message)) return; 84 | 85 | var msLevel = level switch 86 | { 87 | var x when x == LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, 88 | var x when x == LogLevel.Notice => Microsoft.Extensions.Logging.LogLevel.Information, 89 | var x when x == LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, 90 | var x when x == LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, 91 | var x when x == LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical, 92 | _ => Microsoft.Extensions.Logging.LogLevel.Trace 93 | }; 94 | _logger.Log(msLevel, area + message); 95 | } 96 | }; 97 | 98 | 99 | _engine = SecureReliableTransport.Setup(loggerOptions); 100 | 101 | } 102 | 103 | public Task StartAsync(CancellationToken cancellationToken) 104 | { 105 | _logger.LogInformation("Starting Cinegy TS Analyzer service activity"); 106 | 107 | _cancellationToken = cancellationToken; 108 | 109 | StartWorker(); 110 | 111 | return Task.CompletedTask; 112 | } 113 | 114 | public Task StopAsync(CancellationToken cancellationToken) 115 | { 116 | _logger.LogInformation("Shutting down Cinegy TS Analyzer service activity"); 117 | 118 | _pendingExit = true; 119 | 120 | _logger.LogInformation("Cinegy TS Analyzer service stopped"); 121 | 122 | return Task.CompletedTask; 123 | } 124 | 125 | #endregion 126 | 127 | public void StartWorker() 128 | { 129 | _analyzer = new Analyzer(_logger); 130 | 131 | GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; 132 | _receiving = true; 133 | 134 | _analyzer.Setup(); 135 | 136 | _analyzer.TsDecoder.TableChangeDetected += TsDecoder_TableChangeDetected; 137 | 138 | Uri sourceUri; 139 | 140 | try 141 | { 142 | sourceUri = new Uri(_appConfig.SourceUrl); 143 | } 144 | catch (Exception ex) 145 | { 146 | _logger.LogCritical($"Failed to decode source URL parameter {_appConfig.SourceUrl}: {ex.Message}"); 147 | _appLifetime.StopApplication(); 148 | return; 149 | } 150 | 151 | if (sourceUri.Scheme.Equals("srt",StringComparison.InvariantCultureIgnoreCase)) { 152 | new Thread(new ThreadStart(delegate { VideoSrtWorker(sourceUri); })).Start(); 153 | } 154 | else if (sourceUri.Scheme.Equals("udp", StringComparison.InvariantCultureIgnoreCase)) 155 | { 156 | new Thread(new ThreadStart(delegate { NetworkWorker(sourceUri); })).Start(); 157 | } 158 | else if (sourceUri.Scheme.Equals("rtp", StringComparison.InvariantCultureIgnoreCase)) 159 | { 160 | new Thread(new ThreadStart(delegate { NetworkWorker(sourceUri); })).Start(); 161 | } 162 | else if (sourceUri.Scheme.Equals("file", StringComparison.InvariantCultureIgnoreCase)) 163 | { 164 | new Thread(new ThreadStart(delegate { FileWorker(new FileStream(sourceUri.AbsolutePath, FileMode.Open)); })).Start(); 165 | } 166 | else 167 | { 168 | _logger.LogCritical($"Unsupported URI scheme passed in: {sourceUri.Scheme}"); 169 | _appLifetime.StopApplication(); 170 | return; 171 | } 172 | 173 | if(_appConfig.LiveConsole) Console.Clear(); 174 | 175 | var lastConsoleHeartbeatMinute = -1; 176 | var lastDataProcessed = 0L; 177 | var runtimeFormatString = "{0} hours {1} mins"; 178 | while (!_pendingExit) 179 | { 180 | PrintConsoleFeedback(); 181 | 182 | Thread.Sleep(60); 183 | 184 | if (DateTime.Now.Minute == lastConsoleHeartbeatMinute) continue; 185 | 186 | lastConsoleHeartbeatMinute = DateTime.Now.Minute; 187 | var run = DateTime.Now.Subtract(StartTime); 188 | var runtimeStr = string.Format(runtimeFormatString,Math.Floor(run.TotalHours),run.Minutes); 189 | 190 | _logger.LogInformation($"Running: {runtimeStr}, Data Processed: {(Factory.TotalDataProcessed - lastDataProcessed) / 1048576}MB"); 191 | 192 | 193 | lastDataProcessed = Factory.TotalDataProcessed; 194 | } 195 | 196 | _logger.LogInformation("Logging stopped."); 197 | } 198 | 199 | 200 | private void TsDecoder_TableChangeDetected(object sender, TableChangedEventArgs args) 201 | { 202 | _logger.LogInformation($"TS Table Change Detected: {args.Message}"); 203 | } 204 | 205 | private void NetworkWorker(Uri sourceUri) 206 | { 207 | var multicastAddress = sourceUri.DnsSafeHost; 208 | var multicastPort = sourceUri.Port; 209 | var listenAdapter = sourceUri.UserInfo; 210 | 211 | var listenAddress = string.IsNullOrEmpty(listenAdapter) ? IPAddress.Any : IPAddress.Parse(listenAdapter); 212 | 213 | var localEp = new IPEndPoint(listenAddress, multicastPort); 214 | 215 | UdpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); 216 | UdpClient.Client.ReceiveBufferSize = 1500 * 3000; 217 | UdpClient.ExclusiveAddressUse = false; 218 | UdpClient.Client.Bind(localEp); 219 | //_analyser.NetworkMetric.UdpClient = UdpClient; 220 | 221 | var parsedMcastAddr = IPAddress.Parse(multicastAddress); 222 | UdpClient.JoinMulticastGroup(parsedMcastAddr, listenAddress); 223 | 224 | _logger.LogInformation($"Requesting Transport Stream on {sourceUri.Scheme}://{listenAddress}@{multicastAddress}:{multicastPort}"); 225 | 226 | IPEndPoint remoteEndPoint = null; 227 | while (_receiving && !_pendingExit && !_cancellationToken.IsCancellationRequested) 228 | { 229 | var data= UdpClient.Receive(ref remoteEndPoint); 230 | ProcessData(data, data.Length); 231 | } 232 | } 233 | 234 | 235 | private void VideoSrtWorker(Uri srtUri) 236 | { 237 | var srtAddress = srtUri.DnsSafeHost; 238 | 239 | if (Uri.CheckHostName(srtUri.DnsSafeHost) != UriHostNameType.IPv4) 240 | { 241 | srtAddress = Dns.GetHostEntry(srtUri.DnsSafeHost).AddressList.First().ToString(); 242 | } 243 | 244 | var inputVideoPacketsStarted = false; 245 | var endPoint = new IPEndPoint(IPAddress.Parse(srtAddress), srtUri.Port); 246 | var srtReceiver = _engine.CreateReceiver(endPoint, DefaultChunk); 247 | 248 | while (!_cancellationToken.IsCancellationRequested) 249 | { 250 | if (!inputVideoPacketsStarted) 251 | { 252 | _logger.LogInformation("Started receiving input video SRT packets..."); 253 | inputVideoPacketsStarted = true; 254 | } 255 | 256 | try 257 | { 258 | var chunk = srtReceiver.GetChunk(); 259 | if (chunk.DataLen == 0) continue; 260 | ProcessData(chunk.Data, chunk.DataLen); 261 | } 262 | catch (Exception ex) 263 | { 264 | _logger.LogError($@"Unhandled exception within video SRT receiver: {ex.Message}"); 265 | break; 266 | } 267 | } 268 | 269 | _logger.LogError("Closing video SRT Receiver"); 270 | } 271 | 272 | private void FileWorker(Stream stream) 273 | { 274 | var data = new byte[1316]; 275 | var readBytes = stream.Read(data, 0, 1316); 276 | 277 | while (readBytes > 0) 278 | { 279 | ProcessData(data, readBytes); 280 | readBytes = stream.Read(data, 0, 1316); 281 | } 282 | 283 | _pendingExit = true; 284 | Thread.Sleep(250); 285 | 286 | PrintConsoleFeedback(); 287 | Console.WriteLine("Completed reading of file - hit enter to exit!"); 288 | Console.ReadLine(); 289 | } 290 | 291 | private void ProcessData(byte[] data, int stat) 292 | { 293 | try 294 | { 295 | if (_warmedUp) 296 | { 297 | if (stat < data.Length) 298 | { 299 | var trimBuf = new byte[stat]; 300 | Buffer.BlockCopy(data, 0, trimBuf, 0, stat); 301 | _analyzer.RingBuffer.Add(trimBuf); 302 | } 303 | else 304 | { 305 | _analyzer.RingBuffer.Add(data); 306 | } 307 | 308 | var tsPackets = Factory.GetRentedTsPacketsFromData(data, out var tsPktCount, stat); 309 | if (tsPackets == null) 310 | { 311 | Debug.Assert(true); 312 | } 313 | 314 | if (tsPackets == null) return; 315 | 316 | Factory.ReturnTsPackets(tsPackets, tsPktCount); 317 | } 318 | else 319 | { 320 | if (DateTime.UtcNow.Subtract(StartTime) > new TimeSpan(0, 0, 0, 0, WarmUpTime)) 321 | _warmedUp = true; 322 | } 323 | } 324 | catch (Exception ex) 325 | { 326 | _logger.LogError($@"Unhandled exception decoding data from SRT payload: {ex.Message}"); 327 | } 328 | } 329 | #region ConsoleOutput 330 | 331 | private void PrintConsoleFeedback() 332 | { 333 | if (_appConfig.LiveConsole == false) return; 334 | 335 | var runningTime = DateTime.UtcNow.Subtract(StartTime); 336 | 337 | var networkMetric = _analyzer.NetworkMetric; 338 | var rtpMetric = _analyzer.RtpMetric; 339 | 340 | PrintToConsole("Network Details - {0}\t\tRunning: {1:hh\\:mm\\:ss}", _appConfig.SourceUrl, runningTime); 341 | 342 | PrintToConsole(LineBreak); 343 | 344 | if (_appConfig.SourceUrl.StartsWith("srt")) 345 | { 346 | PrintToConsole( 347 | "Total Packets Rcvd: {0} \t\t\t\t", 348 | networkMetric.TotalPackets, networkMetric.NetworkBufferUsage, networkMetric.PeriodMaxNetworkBufferUsage); 349 | } 350 | else 351 | { 352 | PrintToConsole( 353 | "Total Packets Rcvd: {0} \tBuffer Usage: {1:0.00}%/(Peak: {2:0.00}%)", 354 | networkMetric.TotalPackets, networkMetric.NetworkBufferUsage, networkMetric.PeriodMaxNetworkBufferUsage); 355 | } 356 | 357 | PrintToConsole( 358 | "Total Data (MB): {0}\t\tPackets per sec:{1}", Factory.TotalDataProcessed / 1048576, networkMetric.PacketsPerSecond); 359 | 360 | PrintToConsole("Period Max Packet Jitter (ms): {0:0.0}\tCorrupt TS Packets: {1}", 361 | networkMetric.PeriodLongestTimeBetweenPackets * 1000, Factory.TotalCorruptedTsPackets); 362 | 363 | PrintToConsole( 364 | "Bitrates (Mbps): {0:0.00}/{1:0.00}/{2:0.00}/{3:0.00} (Current/Avg/Peak/Low)", 365 | networkMetric.CurrentBitrate / 1048576.0, networkMetric.AverageBitrate / 1048576.0, 366 | networkMetric.HighestBitrate / 1048576.0, networkMetric.LowestBitrate / 1048576.0); 367 | 368 | if (_appConfig.SourceUrl.StartsWith("rtp://",StringComparison.InvariantCultureIgnoreCase)) 369 | { 370 | PrintClearLineToConsole(); 371 | PrintToConsole($"RTP Details - SSRC: {rtpMetric.Ssrc}"); 372 | PrintToConsole(LineBreak); 373 | PrintToConsole( 374 | "Seq Num: {0}\tTimestamp: {1}\tMin Lost Pkts: {2}", 375 | rtpMetric.LastSequenceNumber, rtpMetric.LastTimestamp, rtpMetric.EstimatedLostPackets); 376 | } 377 | 378 | var pidMetrics = _analyzer.PidMetrics; 379 | 380 | lock (pidMetrics) 381 | { 382 | var pcrPid = pidMetrics.FirstOrDefault(m => _analyzer.SelectedPcrPid > 0 && m.Pid == _analyzer.SelectedPcrPid); 383 | 384 | if (pcrPid != null) 385 | { 386 | var span = new TimeSpan((long)(_analyzer.LastPcr / 2.7)); 387 | var oPcrSpan = new TimeSpan((long)(_analyzer.LastOpcr / 2.7)); 388 | var largestDrift = pcrPid.PeriodLowestPcrDrift; 389 | if (pcrPid.PeriodLargestPcrDrift > largestDrift) largestDrift = pcrPid.PeriodLargestPcrDrift; 390 | PrintToConsole( 391 | $"PCR Value: {span:hh\\:mm\\:ss\\.fff}, OPCR Value: {oPcrSpan:hh\\:mm\\:ss\\.fff}, Period Drift (ms): {largestDrift:0.00}"); 392 | } 393 | 394 | //PrintToConsole($"RAW PCR / PTS: {_analyzer.LastPcr } / {_analyzer.LastVidPts * 8} / {_analyzer.LastSubPts * 8}"); 395 | PrintClearLineToConsole(); 396 | 397 | //PrintToConsole(pidMetrics.Count < 10 398 | // ? $"PID Details - Unique PIDs: {pidMetrics.Count}" 399 | // : $"PID Details - Unique PIDs: {pidMetrics.Count}, (10 shown by packet count)"); 400 | PrintToConsole(LineBreak); 401 | 402 | foreach (var pidMetric in pidMetrics.OrderByDescending(m => m.PacketCount).Take(10)) 403 | { 404 | PrintToConsole("TS PID: {0}\tPacket Count: {1} \t\tCC Error Count: {2}", pidMetric.Pid, 405 | pidMetric.PacketCount, pidMetric.CcErrorCount); 406 | } 407 | } 408 | 409 | var tsDecoder = _analyzer.TsDecoder; 410 | 411 | if (tsDecoder != null) 412 | { 413 | lock (tsDecoder) 414 | { 415 | var pmts = tsDecoder.ProgramMapTables.OrderBy(p => p.ProgramNumber).ToList(); 416 | 417 | PrintClearLineToConsole(); 418 | 419 | PrintToConsole(pmts.Count < 5 420 | ? $"Service Information - Service Count: {pmts.Count}" 421 | : $"Service Information - Service Count: {pmts.Count}, (5 shown)"); 422 | 423 | PrintToConsole(LineBreak); 424 | 425 | foreach (var pmtable in pmts.Take(5)) 426 | { 427 | var desc = tsDecoder.GetServiceDescriptorForProgramNumber(pmtable?.ProgramNumber); 428 | if (desc != null) 429 | { 430 | PrintToConsole( 431 | $"Service {pmtable?.ProgramNumber}: {desc.ServiceName.Value} ({desc.ServiceProviderName.Value}) - {desc.ServiceTypeDescription}" 432 | ); 433 | } 434 | } 435 | 436 | var pmt = tsDecoder.GetSelectedPmt();//_options.ProgramNumber); 437 | if (pmt != null) 438 | { 439 | _analyzer.SelectedPcrPid = pmt.PcrPid; 440 | } 441 | 442 | var serviceDesc = tsDecoder.GetServiceDescriptorForProgramNumber(pmt?.ProgramNumber); 443 | 444 | PrintClearLineToConsole(); 445 | 446 | PrintToConsole(serviceDesc != null 447 | ? $"Elements - Selected Program: {serviceDesc.ServiceName} (ID:{pmt?.ProgramNumber}) (first 5 shown)" 448 | : $"Elements - Selected Program Service ID {pmt?.ProgramNumber} (first 5 shown)"); 449 | PrintToConsole(LineBreak); 450 | 451 | if (pmt?.EsStreams != null) 452 | { 453 | foreach (var stream in pmt.EsStreams.Take(5)) 454 | { 455 | if (stream == null) continue; 456 | if (stream.StreamType != 6) 457 | { 458 | PrintToConsole( 459 | "PID: {0} ({1})", stream.ElementaryPid, 460 | DescriptorDictionaries.ShortElementaryStreamTypeDescriptions[ 461 | stream.StreamType]); 462 | } 463 | else 464 | { 465 | if (stream.Descriptors.OfType().Any()) 466 | { 467 | PrintToConsole("PID: {0} ({1})", stream.ElementaryPid, "AC-3 / Dolby Digital"); 468 | continue; 469 | } 470 | if (stream.Descriptors.OfType().Any()) 471 | { 472 | PrintToConsole("PID: {0} ({1})", stream.ElementaryPid, "EAC-3 / Dolby Digital Plus"); 473 | continue; 474 | } 475 | if (stream.Descriptors.OfType().Any()) 476 | { 477 | PrintToConsole("PID: {0} ({1})", stream.ElementaryPid, "DVB Subtitles"); 478 | continue; 479 | } 480 | if (stream.Descriptors.OfType().Any()) 481 | { 482 | PrintToConsole("PID: {0} ({1})", stream.ElementaryPid, "Teletext"); 483 | continue; 484 | } 485 | if (stream.Descriptors.OfType().Any()) 486 | { 487 | if (stream.Descriptors.OfType().First().Organization == "2LND") 488 | { 489 | PrintToConsole("PID: {0} ({1})", stream.ElementaryPid, "Cinegy DANIEL2"); 490 | continue; 491 | } 492 | } 493 | 494 | PrintToConsole( 495 | "PID: {0} ({1})", stream.ElementaryPid, 496 | DescriptorDictionaries.ShortElementaryStreamTypeDescriptions[ 497 | stream.StreamType]); 498 | 499 | } 500 | 501 | } 502 | } 503 | } 504 | } 505 | 506 | Console.CursorVisible = false; 507 | Console.SetCursorPosition(0, 0); 508 | 509 | foreach (var consoleLine in ConsoleLines) 510 | { 511 | ClearCurrentConsoleLine(); 512 | Console.WriteLine(consoleLine); 513 | } 514 | 515 | Console.CursorVisible = true; 516 | 517 | ConsoleLines.Clear(); 518 | 519 | } 520 | 521 | private static void PrintClearLineToConsole() 522 | { 523 | if (OperatingSystem.IsWindows()){ 524 | ConsoleLines.Add("\t"); //use a tab for a clear line, to ensure that an operation runs 525 | } 526 | } 527 | 528 | // TODO: LK to clean up 529 | private static void PrintToConsole(string message, params object[] arguments) 530 | { 531 | if (OperatingSystem.IsWindows()){ 532 | // if (_options.SuppressOutput) return; 533 | ConsoleLines.Add(string.Format(message, arguments)); 534 | } 535 | } 536 | 537 | // TODO: LK to clean up 538 | private static void ClearCurrentConsoleLine() 539 | { 540 | if (OperatingSystem.IsWindows()){ 541 | // Write space to end of line, and then CR with no LF 542 | Console.Write("\r".PadLeft(Console.WindowWidth - Console.CursorLeft - 1)); 543 | } 544 | } 545 | 546 | #endregion 547 | 548 | #region IDispose 549 | 550 | public void Dispose() 551 | { 552 | _pendingExit = true; 553 | _analyzer?.Dispose(); 554 | } 555 | 556 | #endregion 557 | } 558 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | win-x64;linux-x64 7 | enable 8 | tsanalyzer 9 | true 10 | disable 11 | True 12 | x64 13 | 4.0.0 14 | Lewis Kirkaldie 15 | Cinegy 16 | TSAnalyzer, in C# targetting NET 6.0 17 | Cinegy 2016-2023 18 | 19 | 20 | 21 | ..\_Output\TsAnalyzer 22 | 23 | 24 | 25 | ..\_ROutput\TsAnalyzer 26 | 27 | 28 | 29 | 30 | 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 | PreserveNewest 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 2 | WORKDIR /app 3 | 4 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 5 | WORKDIR /src 6 | COPY ["Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj", "Cinegy.TsAnalyzer/"] 7 | 8 | RUN dotnet restore "Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj" 9 | COPY . . 10 | WORKDIR "/src/" 11 | RUN dotnet build "Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj" -c Release -o /app/build 12 | 13 | FROM build AS publish 14 | RUN dotnet publish "Cinegy.TsAnalyzer/Cinegy.TsAnalyzer.csproj" -c Release --os linux -o /app/publish 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | 19 | COPY --from=publish /app/publish . 20 | ENTRYPOINT ["/app/tsanalyzer"] 21 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/Helpers/Product.cs: -------------------------------------------------------------------------------- 1 | /* Copyright 2022-2023 Cinegy GmbH. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | using System.Reflection; 17 | using static System.Char; 18 | 19 | namespace TsAnalyzer.Helpers; 20 | 21 | public static class Product 22 | { 23 | private static readonly Assembly Assembly; 24 | 25 | private static string _tracingName; 26 | 27 | #region Constructors 28 | 29 | static Product() 30 | { 31 | Assembly = Assembly.GetExecutingAssembly(); 32 | 33 | var appFile = Path.Combine(AppContext.BaseDirectory, "tsanalyzer.exe"); 34 | 35 | if (File.Exists(appFile)) 36 | { 37 | BuildTime = File.GetCreationTime(appFile); 38 | } 39 | else 40 | { 41 | var fileLocation = Assembly.Location; 42 | 43 | if (File.Exists(fileLocation)){ 44 | BuildTime = File.GetCreationTime(fileLocation); 45 | } 46 | } 47 | } 48 | 49 | #endregion 50 | 51 | #region Static members 52 | 53 | public static string Name => Assembly == null ? "Unknown Product Name" : Assembly.GetName().Name; 54 | 55 | public static string TracingName 56 | { 57 | get 58 | { 59 | return _tracingName ??= string.Concat(Name.Where(c => !IsWhiteSpace(c))).ToLowerInvariant(); 60 | } 61 | } 62 | 63 | public static string Version => Assembly == null ? "0.0" : Assembly.GetName().Version?.ToString(); 64 | 65 | public static DateTime BuildTime { get; } = DateTime.MinValue; 66 | 67 | #endregion 68 | } -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/Program.cs: -------------------------------------------------------------------------------- 1 | /* Copyright 2016-2023 Cinegy GmbH. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | using System.Diagnostics; 17 | using TsAnalyzer.SerializableModels.Settings; 18 | using Microsoft.Extensions.Configuration; 19 | using Microsoft.Extensions.DependencyInjection; 20 | using Microsoft.Extensions.Hosting; 21 | using Microsoft.Extensions.Logging; 22 | using NLog; 23 | using NLog.Config; 24 | using NLog.Extensions.Hosting; 25 | using NLog.Extensions.Logging; 26 | using NLog.LayoutRenderers.Wrappers; 27 | using NLog.Layouts; 28 | using NLog.Targets; 29 | using OpenTelemetry; 30 | using OpenTelemetry.Metrics; 31 | using OpenTelemetry.Resources; 32 | using TsAnalyzer.Helpers; 33 | using ILogger = NLog.ILogger; 34 | using LogLevel = NLog.LogLevel; 35 | using System.Diagnostics.CodeAnalysis; 36 | using OpenTelemetry.Logs; 37 | using System.Diagnostics.Metrics; 38 | using Cinegy.TsAnalyzer; 39 | 40 | namespace TsAnalyzer 41 | { 42 | internal class Program 43 | { 44 | #region Constants 45 | 46 | public const string EnvironmentVarPrefix = "CINEGYTSA"; 47 | public const string DirectoryAppName = "TSAnalyzer"; 48 | private static readonly string ProgramDataConfigFilePath; 49 | private static readonly string ProgramDataDirectory; 50 | private static readonly string BaseConfigFilePath; 51 | private static readonly string WorkingConfigFilePath; 52 | private static readonly string WorkingDirectory; 53 | 54 | #endregion 55 | 56 | #region Fields 57 | 58 | private static IConfigurationRoot _configRoot; 59 | private static IHost _host; 60 | private static List> _metricsTags = new(); 61 | private static readonly Meter MetricsMeter = new("Cinegy.TsAnalyzer"); 62 | private static readonly ObservableGauge ServiceUptimeGauge; 63 | private static readonly DateTime StartTime = DateTime.UtcNow; 64 | 65 | #endregion 66 | 67 | #region Constructors 68 | 69 | static Program() 70 | { 71 | WorkingDirectory = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName) ?? string.Empty; 72 | 73 | if (OperatingSystem.IsWindows()) 74 | { 75 | ProgramDataDirectory = Path.Combine( 76 | Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), 77 | $"Cinegy\\{DirectoryAppName}"); 78 | } 79 | else 80 | { 81 | ProgramDataDirectory = Path.Combine( 82 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 83 | $"Cinegy/{DirectoryAppName}"); 84 | } 85 | 86 | #if DEBUG 87 | const string configFilename = "appsettings.Development.json"; 88 | #else 89 | const string configFilename = "appsettings.json"; 90 | #endif 91 | 92 | BaseConfigFilePath = Path.Combine(AppContext.BaseDirectory, configFilename); 93 | WorkingConfigFilePath = Path.Combine(WorkingDirectory, configFilename); 94 | ProgramDataConfigFilePath = Path.Combine(ProgramDataDirectory, configFilename); 95 | 96 | ServiceUptimeGauge = MetricsMeter.CreateObservableGauge("tsAnalyzerUptime", () => new Measurement(DateTime.UtcNow.Subtract(StartTime).TotalSeconds, _metricsTags), "sec"); 97 | } 98 | 99 | #endregion 100 | 101 | #region Static members 102 | 103 | public static void Main(string[] args) 104 | { 105 | Console.CancelKeyPress += async delegate { 106 | await _host?.StopAsync(); 107 | await _host?.WaitForShutdownAsync(); 108 | }; 109 | 110 | Activity.DefaultIdFormat = ActivityIdFormat.W3C; 111 | var bufferedWarnings = PrepareConfigFile(); 112 | _configRoot = LoadConfiguration(ProgramDataConfigFilePath, args); 113 | var logger = InitializeLogger(); 114 | 115 | logger.Info("----------------------------------------"); 116 | logger.Info($"{Product.Name}: {Product.Version} (Built: {Product.BuildTime})"); 117 | logger.Info($"Executable directory: {WorkingDirectory}"); 118 | logger.Info($"Operating system: {Environment.OSVersion.Platform} ({Environment.OSVersion.VersionString})"); 119 | logger.Info($"Application data directory: {ProgramDataDirectory}"); 120 | 121 | _metricsTags.Add(new KeyValuePair("ProductVersion", Product.Version)); 122 | _metricsTags.Add(new KeyValuePair("OS", $"{Environment.OSVersion.Platform}({Environment.OSVersion.VersionString})")); 123 | 124 | // since the logger was not available during the initial config file prep, log anything that got queued for display 125 | foreach (var bufferedWarning in bufferedWarnings) 126 | { 127 | logger.Warn(bufferedWarning); 128 | } 129 | 130 | try 131 | { 132 | logger.Info($"Configuration running from {ProgramDataConfigFilePath}"); 133 | 134 | _host = CreateHostBuilder(args, logger).Build(); 135 | 136 | _host.Run(); 137 | } 138 | catch (Exception exception) 139 | { 140 | logger.Error(exception, "Stopped program because of exception"); 141 | throw; 142 | } 143 | finally 144 | { 145 | // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) 146 | LogManager.Shutdown(); 147 | } 148 | 149 | } 150 | 151 | [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Get()")] 152 | private static IHostBuilder CreateHostBuilder(string[] args, ILogger logger) 153 | { 154 | var config = _configRoot.Get(); 155 | 156 | _metricsTags.Add(new KeyValuePair("Ident", config.Ident)); 157 | 158 | if (!string.IsNullOrWhiteSpace(config.Label)) 159 | { 160 | _metricsTags.Add(new KeyValuePair("Label", config.Label)); 161 | } 162 | 163 | var hostnameVar = Environment.GetEnvironmentVariable($"{EnvironmentVarPrefix}_Hostname"); 164 | if (!string.IsNullOrWhiteSpace(hostnameVar)) 165 | { 166 | _metricsTags.Add(new KeyValuePair("Hostname", hostnameVar)); 167 | } 168 | 169 | var telemetryInstanceId = Guid.NewGuid(); 170 | 171 | return Host.CreateDefaultBuilder(args) 172 | .ConfigureAppConfiguration(configHost => { configHost.AddConfiguration(_configRoot); }) 173 | .ConfigureServices((hostContext, services) => 174 | { 175 | if (config.Metrics?.Enabled == true && !Sdk.SuppressInstrumentation) 176 | { 177 | logger.Log(LogLevel.Info,$"Metrics enabled - tagged with instance ID: {telemetryInstanceId}"); 178 | 179 | services.AddOpenTelemetry().WithMetrics(builder => 180 | { 181 | builder 182 | .AddMeter("Cinegy.TsAnalyzer") 183 | .AddMeter("Cinegy.TsDecoder") 184 | .AddMeter("Cinegy.TsAnalysis") 185 | .AddMeter($"Cinegy.TsAnalyzer.{nameof(AnalysisService)}") 186 | .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(Product.TracingName, config.Ident, Product.Version,false, telemetryInstanceId.ToString())); 187 | 188 | if (config.Metrics.ConsoleExporterEnabled) 189 | { 190 | logger.Log(LogLevel.Warn,"Console metrics enabled - only recommended during debugging metrics issues..."); 191 | builder.AddConsoleExporter(); 192 | } 193 | 194 | if (config.Metrics.OpenTelemetryExporterEnabled) 195 | { 196 | logger.Log(LogLevel.Info,$"OpenTelemetry metrics exporter enabled, using endpoint: {config.Metrics.OpenTelemetryEndpoint}"); 197 | builder.AddOtlpExporter((o, m) => { 198 | o.Endpoint = new Uri(config.Metrics.OpenTelemetryEndpoint); 199 | m.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = config.Metrics.OpenTelemetryPeriodicExportInterval; 200 | }); 201 | } 202 | }); 203 | } 204 | 205 | services.AddHostedService(); 206 | }) 207 | .ConfigureLogging(logging => 208 | { 209 | logging.ClearProviders(); 210 | logging.AddOpenTelemetry(builder => 211 | { 212 | builder.IncludeFormattedMessage = true; 213 | builder.IncludeScopes = true; 214 | builder.ParseStateValues = true; 215 | builder.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(Product.TracingName, config.Ident, Product.Version,false, telemetryInstanceId.ToString())); 216 | 217 | //if (config.Metrics?.ConsoleExporterEnabled == true) 218 | //{ 219 | // builder.AddConsoleExporter(); 220 | //} 221 | 222 | if (config.Metrics?.OpenTelemetryExporterEnabled == true) 223 | { 224 | builder.AddOtlpExporter(o => { 225 | o.Endpoint = new Uri(config.Metrics.OpenTelemetryEndpoint); 226 | }); 227 | } 228 | }); 229 | }) 230 | .UseNLog(); 231 | } 232 | 233 | private static List PrepareConfigFile() 234 | { 235 | var bufferedLogWarnMessages = new List(); 236 | if (ProgramDataConfigFilePath != null && File.Exists(ProgramDataConfigFilePath)) 237 | { 238 | if (File.Exists(WorkingConfigFilePath) && File.GetLastWriteTime(WorkingConfigFilePath) > File.GetLastWriteTime(ProgramDataConfigFilePath)) 239 | { 240 | if (File.Exists(ProgramDataConfigFilePath)) 241 | { 242 | bufferedLogWarnMessages.Add("There was a problem reading the settings file, resetting to defaults"); 243 | var programDataConfigFolder = Path.GetDirectoryName(ProgramDataConfigFilePath); 244 | if (ProgramDataConfigFilePath != null && Directory.Exists(programDataConfigFolder)) 245 | { 246 | var backupFileSettingsName = $"{Path.GetFileName(ProgramDataConfigFilePath)}-backup_{DateTime.UtcNow.ToFileTimeUtc()}"; 247 | bufferedLogWarnMessages.Add($"Problematic settings file has been copied to: {backupFileSettingsName}"); 248 | File.Move(ProgramDataConfigFilePath!, Path.Combine(programDataConfigFolder!, backupFileSettingsName)); 249 | } 250 | } 251 | 252 | bufferedLogWarnMessages.Add($"Performing import of newer settings from '{WorkingConfigFilePath}' file"); 253 | File.Copy(WorkingConfigFilePath, ProgramDataConfigFilePath); 254 | } 255 | } 256 | else 257 | { 258 | if (!Directory.Exists(ProgramDataDirectory)) 259 | Directory.CreateDirectory(ProgramDataDirectory); 260 | 261 | if (File.Exists(WorkingConfigFilePath)) 262 | { 263 | bufferedLogWarnMessages.Add($"Performing initial import of settings from '{WorkingConfigFilePath}' file to {ProgramDataConfigFilePath}"); 264 | File.Copy(WorkingConfigFilePath, ProgramDataConfigFilePath!); 265 | } 266 | else 267 | { 268 | bufferedLogWarnMessages.Add($"Performing import of default settings from '{BaseConfigFilePath}' file"); 269 | File.Copy(BaseConfigFilePath, ProgramDataConfigFilePath!); 270 | } 271 | } 272 | return bufferedLogWarnMessages; 273 | } 274 | 275 | private static ILogger InitializeLogger() 276 | { 277 | var config = _configRoot.Get(); 278 | 279 | var logger = LogManager.Setup() 280 | .LoadConfigurationFromSection(_configRoot) 281 | .GetCurrentClassLogger(); 282 | 283 | if (LogManager.Configuration != null) 284 | { 285 | return logger; 286 | 287 | } 288 | 289 | LogManager.Configuration = new LoggingConfiguration(); 290 | ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("pad", typeof(PaddingLayoutRendererWrapper)); 291 | 292 | var layout = new SimpleLayout 293 | { 294 | Text = "${longdate} ${pad:padding=-10:inner=(${level:upperCase=true})} " + 295 | "${pad:padding=-20:fixedLength=true:inner=${logger:shortName=true}} " + 296 | "${message} ${exception:format=tostring}" 297 | }; 298 | 299 | if (config.LiveConsole) 300 | { 301 | Console.WriteLine("LiveConsole mode is enabled, so normal logging is disabled - disable LiveConsole option for troubleshooting!"); 302 | } 303 | else 304 | { 305 | var consoleTarget = new ColoredConsoleTarget 306 | { 307 | UseDefaultRowHighlightingRules = true, 308 | DetectConsoleAvailable = true, 309 | Layout = layout 310 | }; 311 | 312 | LogManager.Configuration.AddRule(LogLevel.Info, LogLevel.Fatal, consoleTarget, 313 | "Microsoft.Hosting.Lifetime"); 314 | LogManager.Configuration.AddRule(LogLevel.Trace, LogLevel.Info, new NullTarget(), "Microsoft.*", true); 315 | LogManager.Configuration.AddRule(LogLevel.Info, LogLevel.Fatal, consoleTarget); 316 | } 317 | 318 | LogManager.ReconfigExistingLoggers(); 319 | return LogManager.GetCurrentClassLogger(); 320 | } 321 | 322 | private static IConfigurationRoot LoadConfiguration(string filepath, string[] args) 323 | { 324 | var configBuilder = new ConfigurationBuilder(); 325 | var config = configBuilder.AddJsonFile(filepath, false) 326 | .AddCommandLine(args) 327 | .AddEnvironmentVariables($"{EnvironmentVarPrefix}_") 328 | .Build(); 329 | 330 | return config; 331 | } 332 | 333 | #endregion 334 | 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\Publish 10 | FileSystem 11 | <_TargetId>Folder 12 | net6.0 13 | win-x64 14 | false 15 | false 16 | 17 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "TsAnalyzer": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--sourceurl srt://srt.cinegy.com:9000 --metrics:OpenTelemetryPeriodicExportInterval=1000 --metrics:opentelemetryexporterenabled=true --metrics:opentelemetryendpoint=http://otel-local.cinegy.com --liveconsole=true" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/SerializableModels/Settings/AppConfig.cs: -------------------------------------------------------------------------------- 1 | /* Copyright 2022-2023 Cinegy GmbH. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | using System.Runtime.InteropServices; 17 | 18 | namespace TsAnalyzer.SerializableModels.Settings 19 | { 20 | public class AppConfig 21 | { 22 | private bool _liveConsole = true; 23 | 24 | public string Ident { get; set; } = "Analyzer1"; 25 | 26 | public string Label { get; set; } 27 | 28 | public string SourceUrl { get; set; } 29 | 30 | public bool LiveConsole 31 | { 32 | get => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _liveConsole; 33 | set => _liveConsole = value; 34 | } 35 | 36 | public MetricsSetting Metrics { get; set; } = new(); 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/SerializableModels/Settings/MetricsSetting.cs: -------------------------------------------------------------------------------- 1 | /* Copyright 2022-2023 Cinegy GmbH. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | namespace TsAnalyzer.SerializableModels.Settings 17 | { 18 | public class MetricsSetting 19 | { 20 | public const string SectionName = "Metrics"; 21 | 22 | public bool Enabled { get; set; } = true; 23 | 24 | public bool ConsoleExporterEnabled { get; set; } 25 | 26 | public bool OpenTelemetryExporterEnabled { get; set; } 27 | 28 | public string OpenTelemetryEndpoint { get; set; } = "http://localhost:4317"; 29 | 30 | public int OpenTelemetryPeriodicExportInterval { get; set; } = 10000; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /Cinegy.TsAnalyzer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cinegy TS Analyzer Tool 2 | 3 | Use this tool to view inbound network, RTP, SRT and plain UDP TS packet details. 4 | 5 | Linux builds, for Intel/AMD 64-bit are now created by the AppVeyor build but the general expectation for use on Linux would be to be running inside a Docker Container! 6 | 7 | ## How easy is it? 8 | 9 | Well, we've added everything you need into a single EXE again, which holds the Net Core 3.1 runtime - so if you have the basics installed on a machine, you should be pretty much good to go. We gave it all a nice Apache license, so you can tinker and throw the tool wherever you need to on the planet. 10 | 11 | Just run the EXE from inside a command-prompt, and you will be offered a basic interactive mode to get cracking checking out your stream. 12 | 13 | From v1.3, you can check out a .TS file and scan through it - just drag / drop the .TS file onto the EXE! 14 | 15 | Starting with V4, there is a Dockerfile, so you can build your own container and use it like so: 16 | 17 | ` 18 | docker build . -t mylocaltsanalyzer:latest -f .\Cinegy.TsAnalyzer\Dockerfile 19 | ` 20 | 21 | You can then run it with something like this: 22 | 23 | ` 24 | docker run --rm -it docker.io/library/tsanalyzer --sourceurl=srt://srt.cinegy.com:6000 25 | ` 26 | 27 | ## Command line arguments: 28 | 29 | With the migration to containers, the magic command-line arguments auto-documentation got broken - but we cleaned up the logic to encapsulate a more 'URL' style model instead. You can set up an appsettings.json file, set environment VARS, or inject command-line arguments like so: 30 | 31 | Argument: 32 | ``` 33 | --sourceurl=srt://srt.cinegy.com:9000 34 | ``` 35 | 36 | ENV VAR: 37 | ``` 38 | CINEGYTSA_SourceURL=srt://srt.cinegy.com:9000 39 | ``` 40 | 41 | appsettings.json: 42 | ``` 43 | { 44 | "sourceUrl": "srt://srt.cinegy.com:9000" 45 | } 46 | ``` 47 | 48 | Here is the list of settable parameters (in command-line-args style): 49 | 50 | ``` 51 | //Core settings 52 | --ident="Analyzer1" //value tagged to core metric, which can be used in statistics aggregation and identification 53 | --label="Cinegy Test Stream" //value tagged to core metric, which can be used in statistics aggregation and identification 54 | --sourceUrl="srt://srt.cinegy.com:9000" //URL format of source - supports srt, rtp and udp schemes with optional source-specific formatting 55 | --liveConsole=true //note - this is only supported on Windows, since more aggressive Console.Write operations fail on Linux 56 | 57 | //Metrics-related settings 58 | --metrics:enabled=false //default is true 59 | --metrics:consoleExporterEnabled=true //default is false - used to enable console output of OTEL metrics 60 | --metrics:openTelemetryExporterEnabled=true //default is false - used to enable exporting of OTEL metrics to endpoint 61 | --metrics:openTelemetryEndpoint="http://localhost:4317" //default is that localhost value - used to specify OTEL collection endpoint URL 62 | --metrics:openTelemetryPeriodicExportInterval=1000 //in milliseconds, default is 10 seconds - used to control frequency of data samples pushed via OTEL 63 | 64 | ``` 65 | 66 | To read more about how these settings can work, and how to transform console arguments to JSON or ENV vars, see the MS documentation here: 67 | 68 | https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration 69 | 70 | 71 | So - what does this look like when you point it at a complex live stream? Here is a shot from a UK DVB-T2 stream: 72 | 73 | ``` 74 | Network Details - srt://srt.cinegy.com:9000 Running: 00:00:01 75 | --------------------------------------------------------------------- 76 | Total Packets Rcvd: 694 77 | Total Data (MB): 0 Packets per sec:636 78 | Period Max Packet Jitter (ms): 0.0 Corrupt TS Packets: 0 79 | Bitrates (Mbps): 6.39/6.47/6.39/6.39 (Current/Avg/Peak/Low) 80 | PCR Value: 21:49:38.425, OPCR Value: 00:00:00.000, Period Drift (ms): 0.00 81 | 82 | --------------------------------------------------------------------- 83 | TS PID: 4095 Packet Count: 4443 CC Error Count: 0 84 | TS PID: 8191 Packet Count: 265 CC Error Count: 0 85 | TS PID: 4097 Packet Count: 98 CC Error Count: 0 86 | TS PID: 4096 Packet Count: 32 CC Error Count: 0 87 | TS PID: 0 Packet Count: 10 CC Error Count: 0 88 | TS PID: 256 Packet Count: 10 CC Error Count: 0 89 | 90 | Service Information - Service Count: 1 91 | --------------------------------------------------------------------- 92 | 93 | Elements - Selected Program Service ID 1 (first 5 shown) 94 | --------------------------------------------------------------------- 95 | PID: 4095 (H.264 video) 96 | PID: 4097 (MPEG-1 audio) 97 | ``` 98 | 99 | Just to make your life easier, we auto-build this using AppVeyor - here is how we are doing right now: 100 | 101 | [![Build status](https://ci.appveyor.com/api/projects/status/08dqscip26lr0g1o/branch/master?svg=true)](https://ci.appveyor.com/project/cinegy/tsanalyser/branch/master) 102 | 103 | You can check out the latest compiled binary from the master or pre-master code here: 104 | 105 | [AppVeyor TSAnalyzer Project Builder](https://ci.appveyor.com/project/cinegy/tsanalyser/build/artifacts) 106 | 107 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 4.0.{build} 2 | image: Visual Studio 2022 3 | configuration: Release 4 | platform: x64 5 | dotnet_csproj: 6 | patch: true 7 | file: '**\*.csproj' 8 | version: '{version}' 9 | version_prefix: '{version}' 10 | package_version: '{version}' 11 | assembly_version: '{version}' 12 | file_version: '{version}' 13 | informational_version: '{version}' 14 | before_build: 15 | - pwsh: nuget restore Cinegy.TsAnalyzer.sln 16 | build: 17 | project: Cinegy.TsAnalyzer\Cinegy.TsAnalyzer.csproj 18 | verbosity: minimal 19 | after_build: 20 | - pwsh: >- 21 | dotnet publish Cinegy.TsAnalyzer\Cinegy.TsAnalyzer.csproj -c Release -r win-x64 22 | 23 | 7z a TSAnalyzer-Win-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip $Env:APPVEYOR_BUILD_FOLDER\Cinegy.TsAnalyzer\bin\Release\net6.0\win-x64\publish\tsanalyzer.exe 24 | 25 | appveyor PushArtifact TSAnalyzer-Win-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip 26 | 27 | dotnet publish Cinegy.TsAnalyzer\Cinegy.TsAnalyzer.csproj -c Release -r linux-x64 28 | 29 | 7z a TSAnalyzer-Linux-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip $Env:APPVEYOR_BUILD_FOLDER\Cinegy.TsAnalyzer\bin\Release\net6.0\linux-x64\publish\tsanalyzer 30 | 31 | appveyor PushArtifact TSAnalyzer-Linux-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip 32 | --------------------------------------------------------------------------------