jellyfin/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

356 lines
12 KiB
C#
Raw Normal View History

#nullable disable
#pragma warning disable CS1591
using System;
using System.Buffers;
using System.Buffers.Binary;
2019-08-16 19:18:37 +00:00
using System.Globalization;
using System.Net;
using System.Net.Sockets;
2017-03-02 20:50:09 +00:00
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
2017-03-02 20:50:09 +00:00
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
2020-07-24 14:37:54 +00:00
public sealed class HdHomerunManager : IDisposable
2017-03-02 20:50:09 +00:00
{
public const int HdHomeRunPort = 65001;
2019-08-16 19:18:37 +00:00
2017-03-02 20:50:09 +00:00
// Message constants
private const byte GetSetName = 3;
private const byte GetSetValue = 4;
private const byte GetSetLockkey = 21;
private const ushort GetSetRequest = 4;
private const ushort GetSetReply = 5;
2017-03-02 20:50:09 +00:00
private uint? _lockkey = null;
2018-09-12 17:26:21 +00:00
private int _activeTuner = -1;
private IPEndPoint _remoteEndPoint;
2017-03-02 20:50:09 +00:00
private TcpClient _tcpClient;
2017-10-23 19:14:11 +00:00
2017-03-02 20:50:09 +00:00
public void Dispose()
{
using (var socket = _tcpClient)
2018-09-12 17:26:21 +00:00
{
if (socket != null)
{
_tcpClient = null;
2017-03-02 20:50:09 +00:00
2019-07-07 14:39:35 +00:00
StopStreaming(socket).GetAwaiter().GetResult();
2018-09-12 17:26:21 +00:00
}
}
2020-07-24 14:37:54 +00:00
GC.SuppressFinalize(this);
2017-03-02 20:50:09 +00:00
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
2017-03-02 20:59:33 +00:00
{
2020-09-19 22:22:48 +00:00
using var client = new TcpClient();
client.Connect(remoteIp, HdHomeRunPort);
using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
2017-03-02 20:59:33 +00:00
}
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
2017-03-02 20:50:09 +00:00
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
2017-05-26 06:48:54 +00:00
return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
2017-03-02 20:50:09 +00:00
}
public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
2017-03-02 20:50:09 +00:00
{
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
2017-03-02 20:50:09 +00:00
2020-09-19 22:22:48 +00:00
_tcpClient = new TcpClient();
_tcpClient.Connect(_remoteEndPoint);
2017-03-02 21:01:42 +00:00
2018-09-12 17:26:21 +00:00
if (!_lockkey.HasValue)
{
var rand = new Random();
_lockkey = (uint)rand.Next();
}
2017-03-02 21:01:42 +00:00
2018-09-12 17:26:21 +00:00
var lockKeyValue = _lockkey.Value;
var stream = _tcpClient.GetStream();
2017-03-02 21:01:42 +00:00
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
2018-09-12 17:26:21 +00:00
{
for (int i = 0; i < numTuners; ++i)
2018-09-12 17:26:21 +00:00
{
if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false))
{
continue;
}
_activeTuner = i;
2020-07-24 14:37:54 +00:00
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
2019-08-16 19:18:37 +00:00
2017-03-02 21:01:42 +00:00
// parse response to make sure it worked
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
2017-03-02 21:01:42 +00:00
{
continue;
}
2020-07-24 14:37:54 +00:00
foreach (var command in commands.GetCommands())
{
var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
2019-08-16 19:18:37 +00:00
// parse response to make sure it worked
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
}
}
2018-09-12 17:26:21 +00:00
2020-07-24 14:37:54 +00:00
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
2018-09-12 17:26:21 +00:00
await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
2019-08-16 19:18:37 +00:00
// parse response to make sure it worked
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
}
2018-09-12 17:26:21 +00:00
return;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
2017-03-02 20:50:09 +00:00
}
2018-09-12 17:26:21 +00:00
_activeTuner = -1;
throw new LiveTvConflictException();
2017-03-02 20:50:09 +00:00
}
2017-03-07 18:27:56 +00:00
public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken)
{
if (!_lockkey.HasValue)
{
2017-03-07 18:27:56 +00:00
return;
}
2017-03-07 18:27:56 +00:00
2020-09-19 22:22:48 +00:00
using var tcpClient = new TcpClient();
tcpClient.Connect(_remoteEndPoint);
using var stream = tcpClient.GetStream();
var commandList = commands.GetCommands();
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
2017-03-07 18:27:56 +00:00
{
2020-09-19 22:22:48 +00:00
foreach (var command in commandList)
2017-03-07 18:27:56 +00:00
{
var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
2019-08-16 19:18:37 +00:00
2020-09-19 22:22:48 +00:00
// parse response to make sure it worked
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
2020-09-19 22:22:48 +00:00
{
return;
2017-03-07 18:27:56 +00:00
}
}
2020-09-19 22:22:48 +00:00
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
2017-03-07 18:27:56 +00:00
}
}
public Task StopStreaming(TcpClient client)
2017-03-02 20:50:09 +00:00
{
2017-10-23 19:14:11 +00:00
var lockKey = _lockkey;
if (!lockKey.HasValue)
{
2018-09-12 17:26:21 +00:00
return Task.CompletedTask;
}
2017-03-02 20:50:09 +00:00
return ReleaseLockkey(client, lockKey.Value);
2017-03-02 20:50:09 +00:00
}
private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue)
2017-03-02 20:50:09 +00:00
{
var stream = client.GetStream();
2018-09-12 17:26:21 +00:00
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
await stream.ReadAsync(buffer).ConfigureAwait(false);
var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
_lockkey = null;
await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
await stream.ReadAsync(buffer).ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
2017-03-02 20:50:09 +00:00
}
internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
2017-03-02 20:50:09 +00:00
{
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
2021-02-12 17:35:54 +00:00
int offset = WriteHeaderAndPayload(buffer, byteName);
return FinishPacket(buffer, offset);
2017-03-02 20:50:09 +00:00
}
internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
2017-03-02 20:50:09 +00:00
{
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
2021-02-12 17:35:54 +00:00
int offset = WriteHeaderAndPayload(buffer, byteName);
2017-03-02 20:50:09 +00:00
buffer[offset++] = GetSetValue;
offset += WriteNullTerminatedString(buffer.Slice(offset), value);
2017-03-02 20:50:09 +00:00
if (lockkey.HasValue)
{
buffer[offset++] = GetSetLockkey;
buffer[offset++] = 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
2017-03-02 20:50:09 +00:00
offset += 4;
}
2021-02-12 17:35:54 +00:00
return FinishPacket(buffer, offset);
2017-03-02 20:50:09 +00:00
}
2021-02-12 17:35:54 +00:00
internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
2017-03-02 20:50:09 +00:00
{
2021-02-12 17:35:54 +00:00
int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
2017-03-02 20:50:09 +00:00
2021-02-12 17:35:54 +00:00
// TODO: variable length: this can be 2 bytes if len > 127
// Write length in front of value
buffer[0] = Convert.ToByte(len);
2017-03-02 20:50:09 +00:00
// null-terminate
buffer[len++] = 0;
return len;
}
2017-03-02 20:50:09 +00:00
2021-02-12 17:35:54 +00:00
private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
{
2021-02-12 17:35:54 +00:00
// Packet type
BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
2017-03-02 20:50:09 +00:00
2021-02-12 17:35:54 +00:00
// We write the payload length at the end
int offset = 4;
// Tag
buffer[offset++] = GetSetName;
2021-02-12 17:35:54 +00:00
// Payload length + data
int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
offset += strLen;
2017-03-02 20:50:09 +00:00
return offset;
}
2021-02-12 17:35:54 +00:00
private static int FinishPacket(Span<byte> buffer, int offset)
{
// Payload length
BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
// calculate crc and insert at the end of the message
var crc = Crc32.Compute(buffer.Slice(0, offset));
BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
return offset + 4;
}
internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected)
2017-03-02 20:50:09 +00:00
{
return TryGetReturnValueOfGetSet(buffer, out var value)
&& string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
}
2017-03-02 20:50:09 +00:00
internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value)
{
value = ReadOnlySpan<byte>.Empty;
if (buffer.Length < 8)
2019-07-07 14:39:35 +00:00
{
2017-03-02 20:50:09 +00:00
return false;
2019-07-07 14:39:35 +00:00
}
2017-03-02 20:50:09 +00:00
uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
if (crc != Crc32.Compute(buffer[..^4]))
2019-07-07 14:39:35 +00:00
{
return false;
2019-07-07 14:39:35 +00:00
}
2017-03-02 20:50:09 +00:00
if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
2019-07-07 14:39:35 +00:00
{
2017-03-02 20:50:09 +00:00
return false;
2019-07-07 14:39:35 +00:00
}
2017-03-02 20:50:09 +00:00
var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
if (buffer.Length != 2 + 2 + 4 + msgLength)
2019-07-07 14:39:35 +00:00
{
return false;
2019-07-07 14:39:35 +00:00
}
2017-03-02 20:50:09 +00:00
var offset = 4;
if (buffer[offset++] != GetSetName)
2019-07-07 14:39:35 +00:00
{
2017-03-02 20:50:09 +00:00
return false;
2019-07-07 14:39:35 +00:00
}
2017-03-02 20:50:09 +00:00
var nameLength = buffer[offset++];
if (buffer.Length < 4 + 1 + offset + nameLength)
{
return false;
}
2017-03-02 20:50:09 +00:00
offset += nameLength;
if (buffer[offset++] != GetSetValue)
{
return false;
}
2017-03-02 20:50:09 +00:00
var valueLength = buffer[offset++];
if (buffer.Length < 4 + offset + valueLength)
{
return false;
}
2017-03-02 20:50:09 +00:00
// remove null terminator
value = buffer.Slice(offset, valueLength - 1);
2017-03-02 20:50:09 +00:00
return true;
}
}
}