< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.TunerHosts.HdHomerun.HdHomerunManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
Line coverage
83%
Covered lines: 54
Uncovered lines: 11
Coverable lines: 65
Total lines: 351
Line coverage: 83%
Branch coverage
83%
Covered branches: 20
Total branches: 24
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%210%
Dispose()0%620%
StopStreaming(...)0%620%
WriteGetMessage(...)100%11100%
WriteSetMessage(...)100%22100%
WriteNullTerminatedString(...)100%11100%
WriteHeaderAndPayload(...)100%11100%
FinishPacket(...)100%11100%
VerifyReturnValueOfGetSet(...)100%22100%
TryGetReturnValueOfGetSet(...)100%1616100%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Buffers;
 7using System.Buffers.Binary;
 8using System.Globalization;
 9using System.Net;
 10using System.Net.Sockets;
 11using System.Text;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using MediaBrowser.Common;
 15using MediaBrowser.Controller.LiveTv;
 16
 17namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
 18{
 19    public sealed class HdHomerunManager : IDisposable
 20    {
 21        public const int HdHomeRunPort = 65001;
 22
 23        // Message constants
 24        private const byte GetSetName = 3;
 25        private const byte GetSetValue = 4;
 26        private const byte GetSetLockkey = 21;
 27        private const ushort GetSetRequest = 4;
 28        private const ushort GetSetReply = 5;
 29
 30        private uint? _lockkey = null;
 031        private int _activeTuner = -1;
 32        private IPEndPoint _remoteEndPoint;
 33
 34        private TcpClient _tcpClient;
 35
 36        public void Dispose()
 37        {
 038            using (var socket = _tcpClient)
 39            {
 040                if (socket is not null)
 41                {
 042                    _tcpClient = null;
 43
 044                    StopStreaming(socket).GetAwaiter().GetResult();
 45                }
 046            }
 047        }
 48
 49        public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToke
 50        {
 51            using var client = new TcpClient();
 52            await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
 53
 54            using var stream = client.GetStream();
 55            return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
 56        }
 57
 58        private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancel
 59        {
 60            byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
 61            try
 62            {
 63                var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
 64                await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
 65
 66                int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 67
 68                return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none");
 69            }
 70            finally
 71            {
 72                ArrayPool<byte>.Shared.Return(buffer);
 73            }
 74        }
 75
 76        public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands
 77        {
 78            _remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort);
 79
 80            _tcpClient = new TcpClient();
 81            await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
 82
 83            if (!_lockkey.HasValue)
 84            {
 85                _lockkey = (uint)Random.Shared.Next();
 86            }
 87
 88            var lockKeyValue = _lockkey.Value;
 89            var stream = _tcpClient.GetStream();
 90
 91            byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
 92            try
 93            {
 94                for (int i = 0; i < numTuners; ++i)
 95                {
 96                    if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false))
 97                    {
 98                        continue;
 99                    }
 100
 101                    _activeTuner = i;
 102                    var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
 103                    var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
 104                    await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
 105                    int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 106
 107                    // parse response to make sure it worked
 108                    if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
 109                    {
 110                        continue;
 111                    }
 112
 113                    foreach (var command in commands.GetCommands())
 114                    {
 115                        var channelMsgLen = WriteSetMessage(buffer, i, command.CommandName, command.CommandValue, lockKe
 116                        await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(fal
 117                        receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 118
 119                        // parse response to make sure it worked
 120                        if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
 121                        {
 122                            await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
 123                        }
 124                    }
 125
 126                    var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort);
 127                    var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
 128
 129                    await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
 130                    receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 131
 132                    // parse response to make sure it worked
 133                    if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
 134                    {
 135                        await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
 136                        continue;
 137                    }
 138
 139                    return;
 140                }
 141            }
 142            finally
 143            {
 144                ArrayPool<byte>.Shared.Return(buffer);
 145            }
 146
 147            _activeTuner = -1;
 148            throw new LiveTvConflictException("No tuners available");
 149        }
 150
 151        public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken)
 152        {
 153            if (!_lockkey.HasValue)
 154            {
 155                return;
 156            }
 157
 158            using var tcpClient = new TcpClient();
 159            await tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
 160
 161            using var stream = tcpClient.GetStream();
 162            var commandList = commands.GetCommands();
 163            byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
 164            try
 165            {
 166                foreach (var command in commandList)
 167                {
 168                    var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.CommandName, command.CommandValue,
 169                    await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
 170                    int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 171
 172                    // parse response to make sure it worked
 173                    if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
 174                    {
 175                        return;
 176                    }
 177                }
 178            }
 179            finally
 180            {
 181                ArrayPool<byte>.Shared.Return(buffer);
 182            }
 183        }
 184
 185        public Task StopStreaming(TcpClient client)
 186        {
 0187            var lockKey = _lockkey;
 188
 0189            if (!lockKey.HasValue)
 190            {
 0191                return Task.CompletedTask;
 192            }
 193
 0194            return ReleaseLockkey(client, lockKey.Value);
 195        }
 196
 197        private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue)
 198        {
 199            var stream = client.GetStream();
 200
 201            var buffer = ArrayPool<byte>.Shared.Rent(8192);
 202            try
 203            {
 204                var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
 205                await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
 206
 207                await stream.ReadAsync(buffer).ConfigureAwait(false);
 208                var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
 209                _lockkey = null;
 210                await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
 211                await stream.ReadAsync(buffer).ConfigureAwait(false);
 212            }
 213            finally
 214            {
 215                ArrayPool<byte>.Shared.Return(buffer);
 216            }
 217        }
 218
 219        internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
 220        {
 1221            var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
 1222            int offset = WriteHeaderAndPayload(buffer, byteName);
 1223            return FinishPacket(buffer, offset);
 224        }
 225
 226        internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
 227        {
 2228            var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
 2229            int offset = WriteHeaderAndPayload(buffer, byteName);
 230
 2231            buffer[offset++] = GetSetValue;
 2232            offset += WriteNullTerminatedString(buffer.Slice(offset), value);
 233
 2234            if (lockkey.HasValue)
 235            {
 1236                buffer[offset++] = GetSetLockkey;
 1237                buffer[offset++] = 4;
 1238                BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
 1239                offset += 4;
 240            }
 241
 2242            return FinishPacket(buffer, offset);
 243        }
 244
 245        internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
 246        {
 7247            int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
 248
 249            // TODO: variable length: this can be 2 bytes if len > 127
 250            // Write length in front of value
 7251            buffer[0] = Convert.ToByte(len);
 252
 253            // null-terminate
 7254            buffer[len++] = 0;
 255
 7256            return len;
 257        }
 258
 259        private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
 260        {
 261            // Packet type
 3262            BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
 263
 264            // We write the payload length at the end
 3265            int offset = 4;
 266
 267            // Tag
 3268            buffer[offset++] = GetSetName;
 269
 270            // Payload length + data
 3271            int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
 3272            offset += strLen;
 273
 3274            return offset;
 275        }
 276
 277        private static int FinishPacket(Span<byte> buffer, int offset)
 278        {
 279            // Payload length
 3280            BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
 281
 282            // calculate crc and insert at the end of the message
 3283            var crc = Crc32.Compute(buffer.Slice(0, offset));
 3284            BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
 285
 3286            return offset + 4;
 287        }
 288
 289        internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected)
 290        {
 3291            return TryGetReturnValueOfGetSet(buffer, out var value)
 3292                && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
 293        }
 294
 295        internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value)
 296        {
 13297            value = ReadOnlySpan<byte>.Empty;
 298
 13299            if (buffer.Length < 8)
 300            {
 1301                return false;
 302            }
 303
 12304            uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
 12305            if (crc != Crc32.Compute(buffer[..^4]))
 306            {
 2307                return false;
 308            }
 309
 10310            if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
 311            {
 1312                return false;
 313            }
 314
 9315            var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
 9316            if (buffer.Length != 2 + 2 + 4 + msgLength)
 317            {
 2318                return false;
 319            }
 320
 7321            var offset = 4;
 7322            if (buffer[offset++] != GetSetName)
 323            {
 1324                return false;
 325            }
 326
 6327            var nameLength = buffer[offset++];
 6328            if (buffer.Length < 4 + 1 + offset + nameLength)
 329            {
 1330                return false;
 331            }
 332
 5333            offset += nameLength;
 334
 5335            if (buffer[offset++] != GetSetValue)
 336            {
 1337                return false;
 338            }
 339
 4340            var valueLength = buffer[offset++];
 4341            if (buffer.Length < 4 + offset + valueLength)
 342            {
 1343                return false;
 344            }
 345
 346            // remove null terminator
 3347            value = buffer.Slice(offset, valueLength - 1);
 3348            return true;
 349        }
 350    }
 351}