You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

409 lines
14 KiB

#pragma warning disable CS1591
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public interface IHdHomerunChannelCommands
{
IEnumerable<(string, string)> GetCommands();
}
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string _channel;
private string _program;
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success)
{
_channel = match.Groups[1].Value;
_program = match.Groups[2].Value;
}
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
yield return ("channel", _channel);
}
if (!string.IsNullOrEmpty(_program))
{
yield return ("program", _program);
}
}
}
public class HdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string _channel;
private string _profile;
public HdHomerunChannelCommands(string channel, string profile)
{
_channel = channel;
_profile = profile;
}
public IEnumerable<(string, string)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
{
if (!string.IsNullOrEmpty(_profile)
&& !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase))
{
yield return ("vchannel", $"{_channel} transcode={_profile}");
}
else
{
yield return ("vchannel", _channel);
}
}
}
}
public sealed class HdHomerunManager : IDisposable
{
public const int HdHomeRunPort = 65001;
// 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;
private uint? _lockkey = null;
private int _activeTuner = -1;
private IPEndPoint _remoteEndPoint;
private TcpClient _tcpClient;
public void Dispose()
{
using (var socket = _tcpClient)
{
if (socket != null)
{
_tcpClient = null;
StopStreaming(socket).GetAwaiter().GetResult();
}
}
GC.SuppressFinalize(this);
}
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
{
using var client = new TcpClient();
client.Connect(remoteIp, HdHomeRunPort);
using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
}
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
{
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);
ParseReturnMessage(buffer, receivedBytes, out string returnVal);
return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
{
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
_tcpClient = new TcpClient();
_tcpClient.Connect(_remoteEndPoint);
if (!_lockkey.HasValue)
{
var rand = new Random();
_lockkey = (uint)rand.Next();
}
var lockKeyValue = _lockkey.Value;
var stream = _tcpClient.GetStream();
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
for (int i = 0; i < numTuners; ++i)
{
if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false))
{
continue;
}
_activeTuner = i;
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);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
continue;
}
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);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
}
}
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
}
return;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
_activeTuner = -1;
throw new LiveTvConflictException();
}
public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken)
{
if (!_lockkey.HasValue)
{
return;
}
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
{
foreach (var command in commandList)
{
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);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
{
return;
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public Task StopStreaming(TcpClient client)
{
var lockKey = _lockkey;
if (!lockKey.HasValue)
{
return Task.CompletedTask;
}
return ReleaseLockkey(client, lockKey.Value);
}
private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue)
{
var stream = client.GetStream();
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);
}
}
internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
{
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
int offset = WriteHeaderAndName(buffer, byteName);
// 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;
}
private static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
{
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
int offset = WriteHeaderAndName(buffer, byteName);
buffer[offset++] = GetSetValue;
offset += WriteNullTerminatedString(buffer.Slice(offset), value);
if (lockkey.HasValue)
{
buffer[offset++] = GetSetLockkey;
buffer[offset++] = 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
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 int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> value)
{
int len = Encoding.UTF8.GetBytes(value, buffer.Slice(1)) + 1;
// Write length in front of value
buffer[0] = Convert.ToByte(len);
// null-terminate
buffer[len++] = 0;
return len;
}
private static int WriteHeaderAndName(Span<byte> buffer, ReadOnlySpan<char> payload)
{
// insert header bytes into message
BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
int offset = 2;
// Subtract 4 bytes for header and 4 bytes for crc
BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(offset), (ushort)(payload.Length + 2));
// insert tag name and length
buffer[offset++] = GetSetName;
offset += WriteNullTerminatedString(buffer.Slice(offset), payload);
return offset;
}
private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
{
returnVal = string.Empty;
if (numBytes < 4)
{
return false;
}
var flipEndian = BitConverter.IsLittleEndian;
int offset = 0;
byte[] msgTypeBytes = new byte[2];
Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
if (flipEndian)
{
Array.Reverse(msgTypeBytes);
}
var msgType = BitConverter.ToUInt16(msgTypeBytes, 0);
offset += 2;
if (msgType != GetSetReply)
{
return false;
}
byte[] msgLengthBytes = new byte[2];
Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length);
if (flipEndian)
{
Array.Reverse(msgLengthBytes);
}
var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0);
offset += 2;
if (numBytes < msgLength + 8)
{
return false;
}
offset++; // Name Tag
var nameLength = buf[offset++];
// skip the name field to get to value for return
offset += nameLength;
offset++; // Value Tag
var valueLength = buf[offset++];
returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
return true;
}
}
}