├── .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 | [](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
--------------------------------------------------------------------------------