├── .gitignore ├── Cinegy.TsMuxer.sln ├── Cinegy.TsMuxer ├── Cinegy.TsMuxer.csproj ├── Logging │ └── LogRecord.cs ├── Options.cs ├── Program.cs ├── Properties │ └── launchSettings.json └── SubPidMuxer.cs ├── LICENSE ├── README.md └── appveyor.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /Cinegy.TsDecoder/bin 2 | /Cinegy.TsDecoder/obj 3 | /.vs 4 | /packages 5 | /Cinegy.TsDecoder/*.user 6 | /Cinegy.TsMuxer/bin 7 | /Cinegy.TsMuxer/obj 8 | /Cinegy.TsMuxer/*.user 9 | -------------------------------------------------------------------------------- /Cinegy.TsMuxer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32526.322 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cinegy.TsMuxer", "Cinegy.TsMuxer\Cinegy.TsMuxer.csproj", "{CB4F2F24-68A3-4799-9234-FAC6A8327FF4}" 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 | {CB4F2F24-68A3-4799-9234-FAC6A8327FF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CB4F2F24-68A3-4799-9234-FAC6A8327FF4}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CB4F2F24-68A3-4799-9234-FAC6A8327FF4}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CB4F2F24-68A3-4799-9234-FAC6A8327FF4}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {513754B3-C7DA-4CC3-9B0D-D4B0DF363E39} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Cinegy.TsMuxer/Cinegy.TsMuxer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | 2.0.1 7 | Lewis Kirkaldie 8 | Cinegy GmbH 9 | Utility for re-muxing two UDP streams into a new single stream 10 | Cinegy GmbH 11 | Transport Stream MPEGTS Muxer Multiplexer 12 | https://github.com/Cinegy/TsMuxer 13 | Migrated to Net 6 14 | 15 | tsmuxer 16 | win-x64;linux-x64 17 | https://github.com/Cinegy/TsMuxer 18 | 19 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Cinegy.TsMuxer/Logging/LogRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.Serialization; 4 | 5 | namespace Cinegy.TsMuxer.Logging 6 | { 7 | [DataContract] 8 | internal class LogRecord 9 | { 10 | [DataMember] 11 | public string EventTime => DateTime.UtcNow.ToString("o"); 12 | 13 | [DataMember] 14 | public string EventMessage { get; set; } 15 | 16 | [DataMember] 17 | public string EventCategory { get; set; } 18 | 19 | [DataMember] 20 | public string ProductName => "TSMuxer"; 21 | 22 | [DataMember] 23 | public string ProductVersion 24 | => FileVersionInfo.GetVersionInfo(AppContext.BaseDirectory).ProductVersion; 25 | 26 | [DataMember] 27 | public string EventKey { get; set; } 28 | 29 | [DataMember] 30 | public string EventTags { get; set; } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Cinegy.TsMuxer/Options.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | 3 | namespace Cinegy.TsMuxer 4 | { 5 | internal class Options 6 | { 7 | [Option("silent", Required = false, 8 | HelpText = "Silence console output")] 9 | public bool SuppressOutput { get; set; } 10 | 11 | } 12 | 13 | // Define a class to receive parsed values 14 | [Verb("mux", HelpText = "Mux from the network.")] 15 | internal class StreamOptions : Options 16 | { 17 | [Option('a', "multicastadapter", Required = false, 18 | HelpText = "IP address of the adapter to listen for multicast data (if not set, tries first binding adapter).")] 19 | public string MulticastAdapterAddress { get; set; } 20 | 21 | [Option('m', "mainmulticastaddress", Required = true, 22 | HelpText = "Primary multicast address to subscribe this instance to.")] 23 | public string MainMulticastAddress { get; set; } 24 | 25 | [Option('p', "mainmulticastport", Required = true, 26 | HelpText = "Primary multicast port number to subscribe this instance to.")] 27 | public int MainMulticastPort { get; set; } 28 | 29 | [Option('n', "submulticastaddress", Required = true, 30 | HelpText = "Primary multicast address to subscribe this instance to.")] 31 | public string SubMulticastAddress { get; set; } 32 | 33 | [Option('q', "submulticastport", Required = true, 34 | HelpText = "Primary multicast port number to subscribe this instance to.")] 35 | public int SubMulticastPort { get; set; } 36 | 37 | [Option('o', "outputmulticastaddress", Required = true, 38 | HelpText = "Output multicast address to send results to.")] 39 | public string OutputMulticastAddress { get; set; } 40 | 41 | [Option('r', "outputmulticastport", Required = true, 42 | HelpText = "Output multicast port number to send results to.")] 43 | public int OutputMulticastPort { get; set; } 44 | 45 | [Option('t', "outputmulticastttl", Required = false, Default = 1, 46 | HelpText = "Output multicast time-to-live (router hops)")] 47 | public int OutputMulticastTtl { get; set; } 48 | 49 | [Option('h', "nortpheaders", Required = false, Default = false, 50 | HelpText = "Optional instruction to skip the expected 12 byte RTP headers (meaning plain MPEGTS inside UDP is expected")] 51 | public bool NoRtpHeaders { get; set; } 52 | 53 | [Option('s', "subpids", Required = true, 54 | HelpText = "Comma separated list of sub stream PIDs to map into master")] 55 | public string SubPids { get; set; } 56 | 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Cinegy.TsMuxer/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | /* Copyright 2019-2022 Cinegy GmbH 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | using System; 18 | using System.Collections.Generic; 19 | using System.IO; 20 | using System.Net; 21 | using System.Net.Sockets; 22 | using System.Threading; 23 | using CommandLine; 24 | using static System.String; 25 | using System.Runtime; 26 | 27 | namespace Cinegy.TsMuxer 28 | { 29 | /// 30 | /// This tool was created to allow testing of subtitle PIDs being muxed into a Cinegy TS output 31 | /// 32 | /// Don't forget this EXE will need inbound firewall traffic allowed inbound - since multicast appears as inbound traffic... 33 | /// 34 | /// Originally created by Lewis, so direct complaints his way. 35 | /// 36 | public class Program 37 | { 38 | 39 | private enum ExitCodes 40 | { 41 | SubPidError = 102, 42 | UnknownError = 2000 43 | } 44 | 45 | private static UdpClient _outputUdpClient; 46 | 47 | private static bool _mainPacketsStarted; 48 | private static bool _subPacketsStarted; 49 | private static bool _pendingExit; 50 | private static bool _suppressOutput; 51 | 52 | private static StreamOptions _options; 53 | private static readonly object ConsoleOutputLock = new object(); 54 | 55 | private static SubPidMuxer _muxer; 56 | 57 | private static int Main(string[] args) 58 | { 59 | try 60 | { 61 | var result = Parser.Default.ParseArguments(args); 62 | 63 | return result.MapResult( 64 | Run, 65 | _ => CheckArgumentErrors()); 66 | } 67 | catch (Exception ex) 68 | { 69 | Environment.ExitCode = (int)ExitCodes.UnknownError; 70 | PrintToConsole("Unknown error: " + ex.Message); 71 | throw; 72 | } 73 | } 74 | 75 | private static int CheckArgumentErrors() 76 | { 77 | //will print using library the appropriate help - now pause the console for the viewer 78 | Console.WriteLine("Hit enter to quit"); 79 | Console.ReadLine(); 80 | return -1; 81 | } 82 | 83 | ~Program() 84 | { 85 | Console.CursorVisible = true; 86 | } 87 | 88 | private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) 89 | { 90 | Console.CursorVisible = true; 91 | if (_pendingExit) return; //already trying to exit - allow normal behaviour on subsequent presses 92 | _pendingExit = true; 93 | e.Cancel = true; 94 | } 95 | 96 | private static int Run(StreamOptions options) 97 | { 98 | Console.Clear(); 99 | Console.CancelKeyPress += Console_CancelKeyPress; 100 | 101 | Console.WriteLine( 102 | // ReSharper disable once AssignNullToNotNullAttribute 103 | $"Cinegy TS Muxing tool (Built: {File.GetCreationTime(AppContext.BaseDirectory)})\n"); 104 | 105 | _options = options; 106 | 107 | GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency; 108 | 109 | var subPids = new List(); 110 | 111 | foreach(var pid in _options.SubPids.Split(',')) { if (int.TryParse(pid, out var intPid)) subPids.Add(intPid); } 112 | 113 | if (subPids.Count < 1) 114 | { 115 | Console.WriteLine("Provided sub PIDs argument did not contain one or more comma separated numbers - please check format"); 116 | return (int)ExitCodes.SubPidError; 117 | } 118 | 119 | _suppressOutput = _options.SuppressOutput; //only suppresses extra logging to screen, not dynamic output 120 | 121 | _muxer = new SubPidMuxer(subPids) {PrintErrorsToConsole = !_suppressOutput}; 122 | 123 | _muxer.PacketReady += _muxer_PacketReady; 124 | 125 | Console.WriteLine($"Outputting multicast data to {_options.OutputMulticastAddress}:{_options.OutputMulticastPort} via adapter {_options.MulticastAdapterAddress}"); 126 | _outputUdpClient = PrepareOutputClient(_options.OutputMulticastAddress,_options.OutputMulticastPort,_options.MulticastAdapterAddress); 127 | Console.WriteLine($"Listening for Primary Transport Stream on rtp://@{ _options.MainMulticastAddress}:{ _options.MainMulticastPort}"); 128 | StartListeningToPrimaryStream(); 129 | Console.WriteLine($"Listening for Sub Transport Stream on rtp://@{_options.SubMulticastAddress}:{_options.SubMulticastPort}"); 130 | StartListeningToSubStream(); 131 | 132 | Console.CursorVisible = false; 133 | 134 | Thread.Sleep(40); 135 | while (!_pendingExit) 136 | { 137 | lock (ConsoleOutputLock) 138 | { 139 | Console.SetCursorPosition(0, 8); 140 | Console.WriteLine($"Primary Stream Buffer fullness: {_muxer.PrimaryBufferFullness} \b \t\t\t"); 141 | Console.WriteLine($"Sub Stream Buffer fullness: {_muxer.SecondaryBufferFullness} \b \t\t\t"); 142 | Console.WriteLine($"Sub Stream PID queue depth: {_muxer.SecondaryPidBufferFullness} \b \t\t"); 143 | } 144 | 145 | Thread.Sleep(40); 146 | } 147 | 148 | Console.CursorVisible = true; 149 | 150 | return 0; 151 | 152 | } 153 | 154 | private static void _muxer_PacketReady(object sender, SubPidMuxer.PacketReadyEventArgs e) 155 | { 156 | _outputUdpClient.Send(e.UdpPacketData, e.UdpPacketData.Length); 157 | } 158 | 159 | private static void StartListeningToPrimaryStream() 160 | { 161 | var listenAddress = IsNullOrEmpty(_options.MulticastAdapterAddress) ? IPAddress.Any : IPAddress.Parse(_options.MulticastAdapterAddress); 162 | 163 | var localEp = new IPEndPoint(listenAddress, _options.MainMulticastPort); 164 | 165 | var udpClient = SetupInputUdpClient(localEp, _options.MainMulticastAddress, listenAddress); 166 | 167 | var ts = new ThreadStart(delegate 168 | { 169 | PrimaryReceivingNetworkWorkerThread(udpClient, localEp); 170 | }); 171 | 172 | var receiverThread = new Thread(ts) { Priority = ThreadPriority.Highest }; 173 | 174 | receiverThread.Start(); 175 | 176 | } 177 | 178 | private static void StartListeningToSubStream() 179 | { 180 | var listenAddress = IsNullOrEmpty(_options.MulticastAdapterAddress) ? IPAddress.Any : IPAddress.Parse(_options.MulticastAdapterAddress); 181 | 182 | var localEp = new IPEndPoint(listenAddress, _options.SubMulticastPort); 183 | 184 | var udpClient = SetupInputUdpClient(localEp, _options.SubMulticastAddress, listenAddress); 185 | 186 | var ts = new ThreadStart(delegate 187 | { 188 | SubReceivingNetworkWorkerThread(udpClient, localEp); 189 | }); 190 | 191 | var receiverThread = new Thread(ts) { Priority = ThreadPriority.Highest }; 192 | 193 | receiverThread.Start(); 194 | 195 | } 196 | 197 | private static UdpClient SetupInputUdpClient(EndPoint localEndpoint, string multicastAddress, IPAddress multicastAdapter) 198 | { 199 | var udpClient = new UdpClient { ExclusiveAddressUse = false }; 200 | 201 | udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); 202 | udpClient.Client.ReceiveBufferSize = 1500 * 3000; 203 | udpClient.ExclusiveAddressUse = false; 204 | udpClient.Client.Bind(localEndpoint); 205 | 206 | var parsedMcastAddr = IPAddress.Parse(multicastAddress); 207 | udpClient.JoinMulticastGroup(parsedMcastAddr, multicastAdapter); 208 | 209 | return udpClient; 210 | } 211 | 212 | private static UdpClient PrepareOutputClient(string multicastAddress, int multicastPort, string outputAdapter) 213 | { 214 | var outputIp = outputAdapter != null ? IPAddress.Parse(outputAdapter) : IPAddress.Any; 215 | 216 | var outputUdpClient = new UdpClient { ExclusiveAddressUse = false }; 217 | var localEp = new IPEndPoint(outputIp, multicastPort); 218 | 219 | outputUdpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); 220 | outputUdpClient.Ttl = (short)_options.OutputMulticastTtl; 221 | outputUdpClient.ExclusiveAddressUse = false; 222 | outputUdpClient.Client.Bind(localEp); 223 | 224 | var parsedMcastAddr = IPAddress.Parse(multicastAddress); 225 | outputUdpClient.Connect(parsedMcastAddr, multicastPort); 226 | 227 | return outputUdpClient; 228 | } 229 | 230 | private static void PrimaryReceivingNetworkWorkerThread(UdpClient client, IPEndPoint localEp) 231 | { 232 | while (!_pendingExit) 233 | { 234 | var data = client.Receive(ref localEp); 235 | 236 | if (!_mainPacketsStarted) 237 | { 238 | PrintToConsole("Started receiving primary multicast packets..."); 239 | _mainPacketsStarted = true; 240 | } 241 | try 242 | { 243 | _muxer.AddToPrimaryBuffer(ref data); 244 | } 245 | catch (Exception ex) 246 | { 247 | PrintToConsole($@"Unhandled exception within network receiver: {ex.Message}"); 248 | return; 249 | } 250 | } 251 | } 252 | 253 | private static void SubReceivingNetworkWorkerThread(UdpClient client, IPEndPoint localEp) 254 | { 255 | while (!_pendingExit) 256 | { 257 | var data = client.Receive(ref localEp); 258 | 259 | if (!_subPacketsStarted) 260 | { 261 | PrintToConsole("Started receiving sub multicast packets..."); 262 | _subPacketsStarted = true; 263 | } 264 | 265 | try 266 | { 267 | _muxer.AddToSecondaryBuffer(ref data); 268 | } 269 | catch (Exception ex) 270 | { 271 | PrintToConsole($@"Unhandled exception within network receiver: {ex.Message}"); 272 | return; 273 | } 274 | } 275 | } 276 | 277 | private static void PrintToConsole(string message) 278 | { 279 | if (_suppressOutput) 280 | return; 281 | 282 | var currentLine = Console.CursorTop; 283 | Console.SetCursorPosition(0, 13); 284 | Console.WriteLine($" \b \b \b{message} \b \b \b \b"); 285 | Console.SetCursorPosition(0, currentLine); 286 | 287 | } 288 | 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /Cinegy.TsMuxer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Cinegy.TsMuxer": { 4 | "commandName": "Project", 5 | "commandLineArgs": "-m 239.186.32.42 -p 1234 -n 239.186.31.42 -q 1234 -o 239.186.33.42 -r 1234 -a 10.186.3.42 -s 1006" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Cinegy.TsMuxer/SubPidMuxer.cs: -------------------------------------------------------------------------------- 1 | using Cinegy.TsDecoder.Buffers; 2 | using Cinegy.TsDecoder.Tables; 3 | using Cinegy.TsDecoder.TransportStream; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Threading; 8 | 9 | namespace Cinegy.TsMuxer 10 | { 11 | internal class SubPidMuxer : IDisposable 12 | { 13 | 14 | private bool _pendingExit; 15 | 16 | private readonly RingBuffer _ringBuffer = new RingBuffer(1000); 17 | private readonly RingBuffer _subRingBuffer = new RingBuffer(1000); 18 | private readonly RingBuffer _subPidBuffer = new RingBuffer(1000, TsPacketSize); 19 | private readonly List _subPids; 20 | private ProgramMapTable _subStreamSourcePmt; 21 | private ProgramMapTable _mainStreamTargetPmt; 22 | private ulong _referencePcr; 23 | private ulong _referenceTime; 24 | private ulong _lastPcr; 25 | 26 | private readonly TsDecoder.TransportStream.TsDecoder _subStreamDecoder = new TsDecoder.TransportStream.TsDecoder(); 27 | private readonly TsDecoder.TransportStream.TsDecoder _mainStreamDecoder = new TsDecoder.TransportStream.TsDecoder(); 28 | private static readonly TsPacketFactory Factory = new TsPacketFactory(); 29 | 30 | // ReSharper disable once InconsistentNaming 31 | private const uint CRC32_POLYNOMIAL = ((0x02608EDB << 1) | 1); 32 | private const int TsPacketSize = 188; 33 | private const short SyncByte = 0x47; 34 | 35 | public bool PrintErrorsToConsole { get; set; } 36 | public int PrimaryBufferFullness => _ringBuffer.BufferFullness; 37 | public int SecondaryBufferFullness => _subRingBuffer.BufferFullness; 38 | 39 | public SubPidMuxer(List subPids) 40 | { 41 | _subPids = subPids; 42 | 43 | var queueThread = new Thread(ProcessQueueWorkerThread) {Priority = ThreadPriority.AboveNormal}; 44 | 45 | queueThread.Start(); 46 | 47 | var subQueueThread = new Thread(ProcessSubQueueWorkerThread) {Priority = ThreadPriority.AboveNormal}; 48 | 49 | subQueueThread.Start(); 50 | } 51 | 52 | public int SecondaryPidBufferFullness 53 | { 54 | get 55 | { 56 | lock (_subPidBuffer) 57 | { 58 | return _subPidBuffer.BufferFullness; 59 | } 60 | } 61 | } 62 | 63 | public void AddToPrimaryBuffer(ref byte[] data) 64 | { 65 | CheckPcr(data); 66 | 67 | if (_lastPcr > 0) 68 | { 69 | //add to buffer once we have a PCR, and set timestamp to the earliest playback time 70 | var pcrDelta = _lastPcr - _referencePcr; 71 | 72 | //TODO: Hardcoded to 200ms buffer time currently 73 | var broadcastTime = _referenceTime + (pcrDelta / 2.7) + ((TimeSpan.TicksPerSecond / 1000) * 20); 74 | 75 | _ringBuffer.Add(ref data, (ulong) broadcastTime); 76 | 77 | } 78 | } 79 | 80 | public void AddToSecondaryBuffer(ref byte[] data) 81 | { 82 | _subRingBuffer.Add(ref data); 83 | } 84 | 85 | private void ProcessQueueWorkerThread() 86 | { 87 | var dataBuffer = new byte[12 + (188 * 7)]; 88 | 89 | while (_pendingExit != true) 90 | { 91 | try 92 | { 93 | if (_ringBuffer.BufferFullness < 10) 94 | { 95 | Thread.Sleep(1); 96 | continue; 97 | } 98 | 99 | lock (_ringBuffer) 100 | { 101 | var capacity = _ringBuffer.Remove(ref dataBuffer, out var dataSize, out _); 102 | 103 | if (capacity > 0) 104 | { 105 | dataBuffer = new byte[capacity]; 106 | continue; 107 | } 108 | 109 | if (dataBuffer == null) continue; 110 | 111 | var packets = Factory.GetTsPacketsFromData(dataBuffer, dataSize); 112 | 113 | //use decoder to register default program (muxing always happens on default program) 114 | if (_mainStreamDecoder.GetSelectedPmt() == null) 115 | { 116 | _mainStreamDecoder.AddPackets(packets); 117 | } 118 | else 119 | { 120 | if (_mainStreamTargetPmt == null && _subStreamSourcePmt != null) 121 | { 122 | _mainStreamTargetPmt = _mainStreamDecoder.GetSelectedPmt(); 123 | 124 | var pmtSpaceNeeded = 0; 125 | foreach (var esInfo in _subStreamSourcePmt.EsStreams) 126 | { 127 | if (_subPids.Contains(esInfo.ElementaryPid)) 128 | { 129 | pmtSpaceNeeded += esInfo.SourceData.Length; 130 | } 131 | } 132 | 133 | if ((_mainStreamTargetPmt.SectionLength + pmtSpaceNeeded) > (TsPacketSize - 12)) 134 | { 135 | throw new InvalidDataException( 136 | "Cannot add to PMT - no room (packet spanned PMT not supported)"); 137 | } 138 | } 139 | } 140 | 141 | //check for any PMT packets, and adjust them to reflect the new muxed reality... 142 | foreach (var packet in packets) 143 | { 144 | if (_mainStreamTargetPmt != null && packet.Pid == _mainStreamTargetPmt.Pid) 145 | { 146 | //this is the PMT for the target program on the target stream - patch in the substream PID entries 147 | foreach (var esInfo in _subStreamSourcePmt.EsStreams) 148 | { 149 | if (_subPids.Contains(esInfo.ElementaryPid)) 150 | { 151 | //locate current SectionLength bytes in databuffer 152 | var pos = packet.SourceBufferIndex + 153 | 4; //advance to start of PMT data structure (past TS header) 154 | var pointerField = dataBuffer[pos]; 155 | pos += pointerField; //advance by pointer field 156 | var sectionLength = 157 | (short) (((dataBuffer[pos + 2] & 0x3) << 8) + 158 | dataBuffer[pos + 3]); //get current length 159 | 160 | //increase length value by esinfo length 161 | var extendedSectionLength = 162 | (short) (sectionLength + (short) esInfo.SourceData.Length); 163 | 164 | //set back new length into databuffer 165 | var bytes = BitConverter.GetBytes(extendedSectionLength); 166 | dataBuffer[pos + 2] = 167 | (byte) ((dataBuffer[pos + 2] & 0xFC) + (byte) (bytes[1] & 0x3)); 168 | dataBuffer[pos + 3] = bytes[0]; 169 | 170 | //copy esinfo source data to end of program block in pmt 171 | Buffer.BlockCopy(esInfo.SourceData, 0, dataBuffer, 172 | packet.SourceBufferIndex + 4 + pointerField + sectionLength, 173 | esInfo.SourceData.Length); 174 | 175 | //correct CRC after each extension 176 | var crcBytes = BitConverter.GetBytes(GenerateCrc(ref dataBuffer, pos + 1, 177 | extendedSectionLength - 1)); 178 | dataBuffer[packet.SourceBufferIndex + 4 + pointerField + extendedSectionLength] 179 | = crcBytes[3]; 180 | dataBuffer[ 181 | packet.SourceBufferIndex + 4 + pointerField + extendedSectionLength + 182 | 1] = 183 | crcBytes[2]; 184 | dataBuffer[ 185 | packet.SourceBufferIndex + 4 + pointerField + extendedSectionLength + 186 | 2] = 187 | crcBytes[1]; 188 | dataBuffer[ 189 | packet.SourceBufferIndex + 4 + pointerField + extendedSectionLength + 190 | 3] = 191 | crcBytes[0]; 192 | } 193 | } 194 | } 195 | } 196 | 197 | //insert any queued filtered sub PID packets 198 | if (_subPidBuffer.BufferFullness > 0) 199 | { 200 | foreach (var packet in packets) 201 | { 202 | if (packet.Pid == (short) PidType.NullPid) 203 | { 204 | //candidate for wiping with any data queued up for muxing in 205 | byte[] subPidPacketBuffer = new byte[TsPacketSize]; 206 | 207 | //see if there is any data waiting to get switched into the mux... 208 | lock (_subPidBuffer) 209 | { 210 | if (_subPidBuffer.BufferFullness < 1) 211 | break; //double check here because prior check was not thread safe 212 | var subPidPacketDataReturned = _subPidBuffer.Remove(ref subPidPacketBuffer, 213 | out _, out _); 214 | if (subPidPacketDataReturned != 0 && subPidPacketDataReturned != TsPacketSize) 215 | { 216 | PrintToConsole("Sub PID data seems to not be size of TS packet!"); 217 | return; 218 | } 219 | } 220 | 221 | if (packet.SourceBufferIndex % 188 != 0) 222 | { 223 | PrintToConsole("Misaligned packet"); 224 | return; 225 | } 226 | 227 | Buffer.BlockCopy(subPidPacketBuffer, 0, dataBuffer, packet.SourceBufferIndex, 228 | TsPacketSize); 229 | } 230 | } 231 | } 232 | var packetReadyEventArgs = new PacketReadyEventArgs(); 233 | packetReadyEventArgs.UdpPacketData = new byte[dataSize]; 234 | Buffer.BlockCopy(dataBuffer,0,packetReadyEventArgs.UdpPacketData,0,dataSize); 235 | OnPacketReady(packetReadyEventArgs); 236 | } 237 | } 238 | catch (Exception ex) 239 | { 240 | PrintToConsole($@"Unhandled exception within network receiver: {ex.Message}"); 241 | } 242 | } 243 | 244 | //Logger.Log(new TelemetryLogEventInfo { Level = LogLevel.Info, Message = "Stopping analysis thread due to exit request." }); 245 | } 246 | 247 | private void ProcessSubQueueWorkerThread() 248 | { 249 | var dataBuffer = new byte[12 + (188 * 7)]; 250 | 251 | while (_pendingExit != true) 252 | { 253 | try 254 | { 255 | if (_subRingBuffer.BufferFullness < 1) 256 | { 257 | Thread.Sleep(1); 258 | continue; 259 | } 260 | 261 | lock (_subRingBuffer) 262 | { 263 | if (_subRingBuffer.BufferFullness < 1) 264 | continue; 265 | 266 | int dataSize; 267 | 268 | var capacity = _subRingBuffer.Remove(ref dataBuffer, out dataSize, out _); 269 | 270 | if (capacity > 0) 271 | { 272 | dataBuffer = new byte[capacity]; 273 | continue; 274 | } 275 | 276 | if (dataBuffer == null) continue; 277 | 278 | //check to see if there are any specific TS packets by PIDs we want to select 279 | 280 | var packets = Factory.GetTsPacketsFromData(dataBuffer, dataSize, true, true); 281 | 282 | if (packets == null) return; 283 | 284 | foreach (var packet in packets) 285 | { 286 | if (_subStreamDecoder.GetSelectedPmt() == null) 287 | { 288 | _subStreamDecoder.AddPackets(packets); 289 | } 290 | else 291 | { 292 | if (_subStreamSourcePmt == null) 293 | { 294 | _subStreamSourcePmt = _subStreamDecoder.GetSelectedPmt(); 295 | } 296 | } 297 | 298 | if (_subPids.Contains(packet.Pid)) 299 | { 300 | //this pid is selected for mapping across... add to PID buffer to merge replacing NULL pid 301 | var buffer = new byte[packet.SourceData.Length]; 302 | Buffer.BlockCopy(packet.SourceData, 0, buffer, 0, packet.SourceData.Length); 303 | _subPidBuffer.Add(ref buffer); 304 | } 305 | } 306 | 307 | //lock (_outputUdpClient) 308 | //{ 309 | // _outputUdpClient.Send(dataBuffer, dataSize); 310 | //} 311 | } 312 | } 313 | catch (Exception ex) 314 | { 315 | PrintToConsole($@"Unhandled exception within network receiver: {ex.Message}"); 316 | } 317 | } 318 | 319 | //Logger.Log(new TelemetryLogEventInfo { Level = LogLevel.Info, Message = "Stopping analysis thread due to exit request." }); 320 | } 321 | 322 | 323 | 324 | protected virtual void OnPacketReady(PacketReadyEventArgs e) 325 | { 326 | var handler = PacketReady; 327 | handler?.Invoke(this, e); 328 | } 329 | 330 | public event EventHandler PacketReady; 331 | 332 | public class PacketReadyEventArgs : EventArgs 333 | { 334 | public byte[] UdpPacketData { get; set; } 335 | } 336 | 337 | private void CheckPcr(byte[] dataBuffer) 338 | { 339 | var tsPackets = Factory.GetTsPacketsFromData(dataBuffer); 340 | 341 | if (tsPackets == null) 342 | { 343 | //Logger.Log(new TelemetryLogEventInfo 344 | //{ 345 | // Level = LogLevel.Info, 346 | // Key = "NullPackets", 347 | // Message = "Packet recieved with no detected TS packets" 348 | //}); 349 | return; 350 | } 351 | 352 | foreach (var tsPacket in tsPackets) 353 | { 354 | if (!tsPacket.AdaptationFieldExists) continue; 355 | if (!tsPacket.AdaptationField.PcrFlag) continue; 356 | if (tsPacket.AdaptationField.FieldSize < 1) continue; 357 | 358 | if (tsPacket.AdaptationField.DiscontinuityIndicator) 359 | { 360 | PrintToConsole("Adaptation field discont indicator"); 361 | continue; 362 | } 363 | 364 | if (_lastPcr == 0) 365 | { 366 | _referencePcr = tsPacket.AdaptationField.Pcr; 367 | _referenceTime = (ulong) (DateTime.UtcNow.Ticks); 368 | } 369 | 370 | _lastPcr = tsPacket.AdaptationField.Pcr; 371 | } 372 | } 373 | 374 | public static int FindSync(IList tsData, int offset) 375 | { 376 | if (tsData == null) throw new ArgumentNullException(nameof(tsData)); 377 | 378 | //not big enough to be any kind of single TS packet 379 | if (tsData.Count < 188) 380 | { 381 | return -1; 382 | } 383 | 384 | for (var i = offset; i < tsData.Count; i++) 385 | { 386 | //check to see if we found a sync byte 387 | if (tsData[i] != SyncByte) continue; 388 | if (i + 1 * TsPacketSize < tsData.Count && tsData[i + 1 * TsPacketSize] != SyncByte) continue; 389 | if (i + 2 * TsPacketSize < tsData.Count && tsData[i + 2 * TsPacketSize] != SyncByte) continue; 390 | if (i + 3 * TsPacketSize < tsData.Count && tsData[i + 3 * TsPacketSize] != SyncByte) continue; 391 | if (i + 4 * TsPacketSize < tsData.Count && tsData[i + 4 * TsPacketSize] != SyncByte) continue; 392 | // seems to be ok 393 | return i; 394 | } 395 | 396 | return -1; 397 | } 398 | 399 | private static uint GenerateCrc(ref byte[] dataBuffer, int position, int length) 400 | { 401 | var endPos = position + length; 402 | uint crc = uint.MaxValue; 403 | 404 | for (int i = position; i < endPos; i++) 405 | { 406 | for (int masking = 0x80; masking != 0; masking >>= 1) 407 | { 408 | uint carry = crc & 0x80000000; 409 | crc <<= 1; 410 | if (!(carry == 0) ^ !((dataBuffer[i] & masking) == 0)) 411 | crc ^= CRC32_POLYNOMIAL; 412 | } 413 | } 414 | 415 | return crc; 416 | } 417 | 418 | private void PrintToConsole(string message) 419 | { 420 | if (PrintErrorsToConsole) 421 | { 422 | var currentLine = Console.CursorTop; 423 | Console.SetCursorPosition(0, 13); 424 | Console.WriteLine($" \b \b \b{message} \b \b \b \b"); 425 | Console.SetCursorPosition(0, currentLine); 426 | } 427 | } 428 | 429 | public void Dispose() 430 | { 431 | _pendingExit = true; 432 | } 433 | } 434 | } 435 | 436 | -------------------------------------------------------------------------------- /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.TsMuxer 2 | 3 | ## What is it? 4 | 5 | TSMuxer is a simple tool, designed to help make testing easier for people with two streams they need to merge - for example, a main playout stream and a smaller subtitling stream. 6 | 7 | It's not 'production ready' - it's really designed as a stand-in for a full quality muxer that would be used at a broadcast site for light duties in a lab or during demos or training. 8 | 9 | However, it works - although at the moment it is young and has not spent long being tuned or having any features added to allow fine-grained controls. 10 | 11 | ## How easy is it? 12 | 13 | Well, we've added everything you need into a single teeny-tiny EXE again, which just depends on .NET 4.5. And then we gave it all a nice Apache license, so you can tinker and throw the tool wherever you need to on the planet. 14 | 15 | Just run the EXE from inside a command-prompt, and the application will just run - although you might want to provide it with some arguments too. 16 | 17 | ## Command line arguments: 18 | 19 | Run with a --help argument, and you will get interactive help information like this: 20 | 21 | ``` 22 | C:\Program Files\Cinegy>Cinegy.TsMuxer.exe 23 | Cinegy 0.0.0.1 24 | Copyright cCinegy GmbH 2017 25 | 26 | ERROR(S): 27 | Required option 'm, mainmulticastaddress' is missing. 28 | Required option 'p, mainmulticastport' is missing. 29 | Required option 'n, submulticastaddress' is missing. 30 | Required option 'q, submulticastport' is missing. 31 | Required option 'o, outputmulticastaddress' is missing. 32 | Required option 'r, outputmulticastport' is missing. 33 | Required option 's, subpids' is missing. 34 | 35 | -a, --multicastadapter IP address of the adapter to listen for multicast data (if not set, tries first 36 | binding adapter). 37 | 38 | -m, --mainmulticastaddress Required. Primary multicast address to subscribe this instance to. 39 | 40 | -p, --mainmulticastport Required. Primary multicast port number to subscribe this instance to. 41 | 42 | -n, --submulticastaddress Required. Primary multicast address to subscribe this instance to. 43 | 44 | -q, --submulticastport Required. Primary multicast port number to subscribe this instance to. 45 | 46 | -o, --outputmulticastaddress Required. Output multicast address to send results to. 47 | 48 | -r, --outputmulticastport Required. Output multicast port number to send results to. 49 | 50 | -h, --nortpheaders (Default: false) Optional instruction to skip the expected 12 byte RTP headers 51 | (meaning plain MPEGTS inside UDP is expected) 52 | 53 | -s, --subpids Required. Comma separated list of sub stream PIDs to map into master 54 | 55 | -l, --logfile Optional file to record events to. 56 | 57 | -d, --descriptortags (Default: ) Comma separated tag values added to all log entries for instance and 58 | machine identification 59 | 60 | -t, --telemetry Enable integrated telemetry 61 | 62 | --help Display this help screen. 63 | 64 | --version Display version information. 65 | 66 | Hit enter to quit 67 | ``` 68 | 69 | Just to make your life easier, we auto-build this using AppVeyor - here is how we are doing right now: 70 | 71 | [![Build status](https://ci.appveyor.com/api/projects/status/njky44r567b8x634?svg=true)](https://ci.appveyor.com/project/cinegy/tsmuxer) 72 | 73 | You can check out the latest compiled binary from the master or pre-master code here: 74 | 75 | [Download TSMuxer Binary Artifacts](https://ci.appveyor.com/project/cinegy/tsmuxer/build/artifacts) 76 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: Visual Studio 2022 3 | configuration: Release 4 | platform: Any CPU 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 16 | build: 17 | project: Cinegy.TsMuxer.sln 18 | verbosity: minimal 19 | after_build: 20 | - pwsh: >- 21 | dotnet publish -c Release -r win-x64 22 | 23 | 7z a Cinegy.TsMuxer-Win-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip $Env:APPVEYOR_BUILD_FOLDER\Cinegy.TsMuxer\bin\Release\netcoreapp3.1\win-x64\publish\tsmuxer.exe 24 | 25 | appveyor PushArtifact Cinegy.TsMuxer-Win-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip 26 | 27 | dotnet publish -c Release -r linux-x64 28 | 29 | 7z a Cinegy.TsMuxer-Linux-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip $Env:APPVEYOR_BUILD_FOLDER\Cinegy.TsMuxer\bin\Release\netcoreapp3.1\linux-x64\publish\tsmuxer 30 | 31 | appveyor PushArtifact Cinegy.TsMuxer-Linux-x64-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip 32 | 33 | 7z a Cinegy.TsMuxer-PDB-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip $Env:APPVEYOR_BUILD_FOLDER\Cinegy.TsMuxer\bin 34 | 35 | appveyor PushArtifact Cinegy.TsMuxer-PDB-$Env:APPVEYOR_REPO_BRANCH-$Env:APPVEYOR_BUILD_VERSION.zip --------------------------------------------------------------------------------