diff options
| author | Luke <luke.pulverenti@gmail.com> | 2017-03-03 00:53:47 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2017-03-03 00:53:47 -0500 |
| commit | 9f8cbc668db3885f2a30ebf5ca62d2e1de3af98f (patch) | |
| tree | 4a9f055e10fa90384d74a635ca535e3de328f9bd /Emby.Server.Implementations | |
| parent | 8e1c53aaf482ec89df00066ca827239e5dde3346 (diff) | |
| parent | 7cbc76af27637fca10bca21d0b343f96b1a02b6a (diff) | |
Merge pull request #2504 from MediaBrowser/dev
Dev
Diffstat (limited to 'Emby.Server.Implementations')
| -rw-r--r-- | Emby.Server.Implementations/Emby.Server.Implementations.csproj | 4 | ||||
| -rw-r--r-- | Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 302 | ||||
| -rw-r--r-- | Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs (renamed from Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs) | 4 | ||||
| -rw-r--r-- | Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs | 406 | ||||
| -rw-r--r-- | Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 294 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Udp/UdpServer.cs | 4 |
6 files changed, 893 insertions, 121 deletions
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 50c01110f..c704d0e4e 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -169,9 +169,11 @@ <Compile Include="LiveTv\RecordingImageProvider.cs" /> <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" /> + <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunManager.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" /> - <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunLiveStream.cs" /> + <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHttpStream.cs" /> + <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunUdpStream.cs" /> <Compile Include="LiveTv\TunerHosts\M3uParser.cs" /> <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" /> <Compile Include="LiveTv\TunerHosts\MulticastStream.cs" /> diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 2e70e1ac1..c07b6be82 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -28,13 +28,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; private readonly IServerApplicationHost _appHost; + private readonly ISocketFactory _socketFactory; + private readonly INetworkManager _networkManager; - public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost) + public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager) : base(config, logger, jsonSerializer, mediaEncoder) { _httpClient = httpClient; _fileSystem = fileSystem; _appHost = appHost; + _socketFactory = socketFactory; + _networkManager = networkManager; } public string Name @@ -84,11 +88,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } + private class HdHomerunChannelInfo : ChannelInfo + { + public bool IsLegacyTuner { get; set; } + public string Url { get; set; } + } + protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) { var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false); - return lineup.Select(i => new ChannelInfo + return lineup.Select(i => new HdHomerunChannelInfo { Name = i.GuideName, Number = i.GuideNumber, @@ -98,20 +108,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IsHD = i.HD == 1, AudioCodec = i.AudioCodec, VideoCodec = i.VideoCodec, - ChannelType = ChannelType.TV + ChannelType = ChannelType.TV, + IsLegacyTuner = (i.URL ?? string.Empty).StartsWith("hdhomerun", StringComparison.OrdinalIgnoreCase), + Url = i.URL - }).ToList(); + }).Cast<ChannelInfo>().ToList(); } private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); - private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken) + private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) { lock (_modelCache) { DiscoverResponse response; if (_modelCache.TryGetValue(info.Url, out response)) { - return response.ModelNumber; + return response; } } @@ -135,67 +147,59 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _modelCache[info.Id] = response; } - return response.ModelNumber; + return response; } } catch (HttpException ex) { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) + if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) { var defaultValue = "HDHR"; + var response = new DiscoverResponse + { + ModelNumber = defaultValue + }; // HDHR4 doesn't have this api lock (_modelCache) { - _modelCache[info.Id] = new DiscoverResponse - { - ModelNumber = defaultValue - }; + _modelCache[info.Id] = response; } - return defaultValue; + return response; } throw; } } - public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) + private async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) { - var model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false); + var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var stream = await _httpClient.Get(new HttpRequestOptions() - { - Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)), - CancellationToken = cancellationToken, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds), - BufferContent = false + var tuners = new List<LiveTvTunerInfo>(); + + var uri = new Uri(GetApiUrl(info, false)); - }).ConfigureAwait(false)) + using (var manager = new HdHomerunManager(_socketFactory)) { - var tuners = new List<LiveTvTunerInfo>(); - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) + // Legacy HdHomeruns are IPv4 only + var ipInfo = new IpAddressInfo(uri.Host, IpAddressFamily.InterNetwork); + + for (int i = 0; i < model.TunerCount; ++i) { - while (!sr.EndOfStream) + var name = String.Format("Tuner {0}", i + 1); + var currentChannel = "none"; /// @todo Get current channel and map back to Station Id + var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); + LiveTvTunerStatus status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; + tuners.Add(new LiveTvTunerInfo { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) - { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model) ? Name : model, - ProgramName = currentChannel, - Status = status - }); - } - } + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); } - return tuners; } + return tuners; } public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -244,34 +248,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return uri.AbsoluteUri.TrimEnd('/'); } - private static string StripXML(string source) - { - char[] buffer = new char[source.Length]; - int bufferIndex = 0; - bool inside = false; - - for (int i = 0; i < source.Length; i++) - { - char let = source[i]; - if (let == '<') - { - inside = true; - continue; - } - if (let == '>') - { - inside = false; - continue; - } - if (!inside) - { - buffer[bufferIndex] = let; - bufferIndex++; - } - } - return new string(buffer, 0, bufferIndex); - } - private class Channels { public string GuideNumber { get; set; } @@ -284,13 +260,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public int HD { get; set; } } - private async Task<MediaSourceInfo> GetMediaSource(TunerHostInfo info, string channelId, string profile) + private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile) { int? width = null; int? height = null; bool isInterlaced = true; string videoCodec = null; - string audioCodec = "ac3"; + string audioCodec = null; int? videoBitrate = null; int? audioBitrate = null; @@ -344,21 +320,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun videoBitrate = 1000000; } - var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); - var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase)); - if (channel != null) + if (channelInfo != null) { if (string.IsNullOrWhiteSpace(videoCodec)) { - videoCodec = channel.VideoCodec; + videoCodec = channelInfo.VideoCodec; } - audioCodec = channel.AudioCodec; + audioCodec = channelInfo.AudioCodec; if (!videoBitrate.HasValue) { - videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000; + videoBitrate = (channelInfo.IsHD ?? true) ? 15000000 : 2000000; } - audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000; + audioBitrate = (channelInfo.IsHD ?? true) ? 448000 : 192000; } // normalize @@ -443,6 +417,82 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return channelId.Split('_')[1]; } + private MediaSourceInfo GetLegacyMediaSource(TunerHostInfo info, string channelId, ChannelInfo channel) + { + int? width = null; + int? height = null; + bool isInterlaced = true; + string videoCodec = null; + string audioCodec = null; + + int? videoBitrate = null; + int? audioBitrate = null; + + if (channel != null) + { + if (string.IsNullOrWhiteSpace(videoCodec)) + { + videoCodec = channel.VideoCodec; + } + audioCodec = channel.AudioCodec; + } + + // normalize + if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase)) + { + videoCodec = "mpeg2video"; + } + + string nal = null; + + var url = info.Url; + var id = channelId; + id += "_" + url.GetMD5().ToString("N"); + + var mediaSource = new MediaSourceInfo + { + Path = url, + Protocol = MediaProtocol.Udp, + MediaStreams = new List<MediaStream> + { + new MediaStream + { + Type = MediaStreamType.Video, + // Set the index to -1 because we don't know the exact index of the video stream within the container + Index = -1, + IsInterlaced = isInterlaced, + Codec = videoCodec, + Width = width, + Height = height, + BitRate = videoBitrate, + NalLengthSize = nal + + }, + new MediaStream + { + Type = MediaStreamType.Audio, + // Set the index to -1 because we don't know the exact index of the audio stream within the container + Index = -1, + Codec = audioCodec, + BitRate = audioBitrate + } + }, + RequiresOpening = true, + RequiresClosing = true, + BufferMs = 0, + Container = "ts", + Id = id, + SupportsDirectPlay = false, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsInfiniteStream = true + }; + + mediaSource.InferTotalBitrate(); + + return mediaSource; + } + protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken) { var list = new List<MediaSourceInfo>(); @@ -453,35 +503,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - try - { - var model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false); - model = model ?? string.Empty; + var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); + var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); + + var hdHomerunChannelInfo = channelInfo as HdHomerunChannelInfo; + + var isLegacyTuner = hdHomerunChannelInfo != null && hdHomerunChannelInfo.IsLegacyTuner; - if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) + if (isLegacyTuner) + { + list.Add(GetLegacyMediaSource(info, hdhrId, channelInfo)); + } + else + { + try { - list.Add(await GetMediaSource(info, hdhrId, "native").ConfigureAwait(false)); + var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var model = modelInfo == null ? string.Empty : (modelInfo.ModelNumber ?? string.Empty); - if (info.AllowHWTranscoding) + if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) { - list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false)); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + + if (info.AllowHWTranscoding) + { + list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false)); - list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false)); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); + } } } - } - catch - { + catch + { - } + } - if (list.Count == 0) - { - list.Add(await GetMediaSource(info, hdhrId, "native").ConfigureAwait(false)); + if (list.Count == 0) + { + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + } } return list; @@ -509,11 +573,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false); + var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false); + var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); + + var hdhomerunChannel = channelInfo as HdHomerunChannelInfo; - var liveStream = new HdHomerunLiveStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); - liveStream.EnableStreamSharing = true; - return liveStream; + if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) + { + var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var mediaSource = GetLegacyMediaSource(info, hdhrId, channelInfo); + + var liveStream = new HdHomerunUdpStream(mediaSource, streamId, hdhomerunChannel.Url, modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return liveStream; + } + else + { + var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile); + + var liveStream = new HdHomerunHttpStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + return liveStream; + } } public async Task Validate(TunerHostInfo info) @@ -531,18 +610,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { // Test it by pulling down the lineup - using (var stream = await _httpClient.Get(new HttpRequestOptions - { - Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), - CancellationToken = CancellationToken.None, - BufferContent = false - - }).ConfigureAwait(false)) - { - var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); - - info.DeviceId = response.DeviceID; - } + var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false); + info.DeviceId = modelInfo.DeviceID; } catch (HttpException ex) { @@ -573,6 +642,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public string DeviceAuth { get; set; } public string BaseURL { get; set; } public string LineupURL { get; set; } + public int TunerCount { get; set; } } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs index 625e4457d..2798805fa 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.MediaInfo; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { - public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider + public class HdHomerunHttpStream : LiveStream, IDirectStreamProvider { private readonly ILogger _logger; private readonly IHttpClient _httpClient; @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>(); private readonly MulticastStream _multicastStream; - public HdHomerunLiveStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) + public HdHomerunHttpStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) : base(mediaSource) { _fileSystem = fileSystem; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs new file mode 100644 index 000000000..a6e9491a4 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; + +namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +{ + public class HdHomerunManager : IDisposable + { + public static int HdHomeRunPort = 65001; + // Message constants + private static byte GetSetName = 3; + private static byte GetSetValue = 4; + private static byte GetSetLockkey = 21; + private static ushort GetSetRequest = 4; + private static ushort GetSetReply = 5; + + private uint? _lockkey = null; + private int _activeTuner = 0; + private readonly ISocketFactory _socketFactory; + private IpAddressInfo _remoteIp; + + public HdHomerunManager(ISocketFactory socketFactory) + { + _socketFactory = socketFactory; + } + + public void Dispose() + { + var task = StopStreaming(); + + Task.WaitAll(task); + } + + public async Task<bool> CheckTunerAvailability(IpAddressInfo remoteIp, int tuner, CancellationToken cancellationToken) + { + using (var socket = _socketFactory.CreateTcpSocket(remoteIp, HdHomeRunPort)) + { + return await CheckTunerAvailability(socket, remoteIp, tuner, cancellationToken).ConfigureAwait(false); + } + } + + private async Task<bool> CheckTunerAvailability(ISocket socket, IpAddressInfo remoteIp, int tuner, CancellationToken cancellationToken) + { + var ipEndPoint = new IpEndPointInfo(remoteIp, HdHomeRunPort); + + var lockkeyMsg = CreateGetMessage(tuner, "lockkey"); + await socket.SendAsync(lockkeyMsg, lockkeyMsg.Length, ipEndPoint, cancellationToken); + var response = await socket.ReceiveAsync(cancellationToken).ConfigureAwait(false); + string returnVal; + ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal); + + return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); + } + + public async Task StartStreaming(IpAddressInfo remoteIp, IpAddressInfo localIp, int localPort, string url, int numTuners, CancellationToken cancellationToken) + { + _remoteIp = remoteIp; + // parse url for channel and program + string frequency, program; + if (!ParseUrl(url, out frequency, out program)) + { + return; + } + + using (var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort)) + { + if (!_lockkey.HasValue) + { + var rand = new Random(); + _lockkey = (uint)rand.Next(); + } + + var ipEndPoint = new IpEndPointInfo(_remoteIp, HdHomeRunPort); + + for (int i = 0; i < numTuners; ++i) + { + if (!await CheckTunerAvailability(tcpClient, _remoteIp, i, cancellationToken).ConfigureAwait(false)) + continue; + + _activeTuner = i; + var lockKeyString = String.Format("{0:d}", _lockkey.Value); + var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); + await tcpClient.SendAsync(lockkeyMsg, lockkeyMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + var response = await tcpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + string returnVal; + // parse response to make sure it worked + if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + continue; + + var channelMsg = CreateSetMessage(i, "channel", frequency, _lockkey.Value); + await tcpClient.SendAsync(channelMsg, channelMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + await tcpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + { + await ReleaseLockkey(tcpClient).ConfigureAwait(false); + continue; + } + + if (program != String.Empty) + { + var programMsg = CreateSetMessage(i, "program", program, _lockkey.Value); + await tcpClient.SendAsync(programMsg, programMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + await tcpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + { + await ReleaseLockkey(tcpClient).ConfigureAwait(false); + continue; + } + } + + var targetValue = String.Format("rtp://{0}:{1}", localIp, localPort); + var targetMsg = CreateSetMessage(i, "target", targetValue, _lockkey.Value); + + await tcpClient.SendAsync(targetMsg, targetMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); + response = await tcpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + { + await ReleaseLockkey(tcpClient).ConfigureAwait(false); + continue; + } + + break; + } + } + } + + public async Task StopStreaming() + { + if (!_lockkey.HasValue) + return; + + using (var socket = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort)) + { + await ReleaseLockkey(socket).ConfigureAwait(false); + } + } + + private async Task ReleaseLockkey(ISocket tcpClient) + { + var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", _lockkey); + await tcpClient.SendAsync(releaseTarget, releaseTarget.Length, new IpEndPointInfo(_remoteIp, HdHomeRunPort), CancellationToken.None).ConfigureAwait(false); + await tcpClient.ReceiveAsync(CancellationToken.None).ConfigureAwait(false); + var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", _lockkey); + _lockkey = null; + await tcpClient.SendAsync(releaseKeyMsg, releaseKeyMsg.Length, new IpEndPointInfo(_remoteIp, HdHomeRunPort), CancellationToken.None).ConfigureAwait(false); + await tcpClient.ReceiveAsync(CancellationToken.None).ConfigureAwait(false); + } + + private static bool ParseUrl(string url, out string frequency, out string program) + { + frequency = String.Empty; + program = String.Empty; + var regExp = new Regex(@"\/ch(\d+)-?(\d*)"); + var match = regExp.Match(url); + if (match.Success) + { + frequency = match.Groups[1].Value; + program = match.Groups[2].Value; + return true; + } + + return false; + } + + private static byte[] CreateGetMessage(int tuner, string name) + { + var byteName = Encoding.UTF8.GetBytes(String.Format("/tuner{0}/{1}\0", tuner, name)); + int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length + + var message = new byte[messageLength]; + + int offset = InsertHeaderAndName(byteName, messageLength, message); + + bool flipEndian = BitConverter.IsLittleEndian; + + // calculate crc and insert at the end of the message + var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); + if (flipEndian) + Array.Reverse(crcBytes); + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); + + return message; + } + + private static byte[] CreateSetMessage(int tuner, String name, String value, uint? lockkey) + { + var byteName = Encoding.UTF8.GetBytes(String.Format("/tuner{0}/{1}\0", tuner, name)); + var byteValue = Encoding.UTF8.GetBytes(String.Format("{0}\0", value)); + + int messageLength = byteName.Length + byteValue.Length + 12; + if (lockkey.HasValue) + messageLength += 6; + + var message = new byte[messageLength]; + + int offset = InsertHeaderAndName(byteName, messageLength, message); + + bool flipEndian = BitConverter.IsLittleEndian; + + message[offset] = GetSetValue; + offset++; + message[offset] = Convert.ToByte(byteValue.Length); + offset++; + Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length); + offset += byteValue.Length; + if (lockkey.HasValue) + { + message[offset] = GetSetLockkey; + offset++; + message[offset] = (byte)4; + offset++; + var lockKeyBytes = BitConverter.GetBytes(lockkey.Value); + if (flipEndian) + Array.Reverse(lockKeyBytes); + Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4); + offset += 4; + } + + // calculate crc and insert at the end of the message + var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); + if (flipEndian) + Array.Reverse(crcBytes); + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); + + return message; + } + + private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message) + { + // check to see if we need to flip endiannes + bool flipEndian = BitConverter.IsLittleEndian; + int offset = 0; + + // create header bytes + var getSetBytes = BitConverter.GetBytes(GetSetRequest); + var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc + + if (flipEndian) + { + Array.Reverse(getSetBytes); + Array.Reverse(msgLenBytes); + } + + // insert header bytes into message + Buffer.BlockCopy(getSetBytes, 0, message, offset, 2); + offset += 2; + Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2); + offset += 2; + + // insert tag name and length + message[offset] = GetSetName; + offset++; + message[offset] = Convert.ToByte(byteName.Length); + offset++; + + // insert name string + Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length); + offset += byteName.Length; + + 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; + + var nameTag = buf[offset]; + offset++; + + var nameLength = buf[offset]; + offset++; + + // skip the name field to get to value for return + offset += nameLength; + + var valueTag = buf[offset]; + offset++; + + var valueLength = buf[offset]; + offset++; + + returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator + return true; + } + + private class HdHomerunCrc + { + private static UInt32[] crc_table = { + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, + 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, + 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, + 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, + 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, + 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, + 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, + 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, + 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, + 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, + 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, + 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, + 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, + 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, + 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, + 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, + 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, + 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, + 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d }; + + public static UInt32 GetCrc32(byte[] bytes, int numBytes) + { + var hash = 0xffffffff; + for (var i = 0; i < numBytes; i++) + hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff]; + + var tmp = ~hash & 0xffffffff; + var b0 = tmp & 0xff; + var b1 = (tmp >> 8) & 0xff; + var b2 = (tmp >> 16) & 0xff; + var b3 = (tmp >> 24) & 0xff; + hash = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; + return hash; + } + } + } +} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs new file mode 100644 index 000000000..95ceb0660 --- /dev/null +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.Net; + +namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +{ + public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider + { + private readonly ILogger _logger; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerApplicationPaths _appPaths; + private readonly IServerApplicationHost _appHost; + private readonly ISocketFactory _socketFactory; + + private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource(); + private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>(); + private readonly MulticastStream _multicastStream; + private readonly string _channelUrl; + private readonly int _numTuners; + private readonly INetworkManager _networkManager; + + public HdHomerunUdpStream(MediaSourceInfo mediaSource, string originalStreamId, string channelUrl, int numTuners, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager) + : base(mediaSource) + { + _fileSystem = fileSystem; + _httpClient = httpClient; + _logger = logger; + _appPaths = appPaths; + _appHost = appHost; + _socketFactory = socketFactory; + _networkManager = networkManager; + OriginalStreamId = originalStreamId; + _multicastStream = new MulticastStream(_logger); + _channelUrl = channelUrl; + _numTuners = numTuners; + } + + protected override async Task OpenInternal(CancellationToken openCancellationToken) + { + _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); + + var mediaSource = OriginalMediaSource; + + var uri = new Uri(mediaSource.Path); + var localPort = _networkManager.GetRandomUnusedUdpPort(); + + _logger.Info("Opening HDHR UDP Live stream from {0}", uri.Host); + + var taskCompletionSource = new TaskCompletionSource<bool>(); + + StartStreaming(uri.Host, localPort, taskCompletionSource, _liveStreamCancellationTokenSource.Token); + + //OpenedMediaSource.Protocol = MediaProtocol.File; + //OpenedMediaSource.Path = tempFile; + //OpenedMediaSource.ReadAtNativeFramerate = true; + + OpenedMediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; + OpenedMediaSource.Protocol = MediaProtocol.Http; + OpenedMediaSource.SupportsDirectPlay = false; + OpenedMediaSource.SupportsDirectStream = true; + OpenedMediaSource.SupportsTranscoding = true; + + await taskCompletionSource.Task.ConfigureAwait(false); + + //await Task.Delay(5000).ConfigureAwait(false); + } + + public override Task Close() + { + _logger.Info("Closing HDHR UDP live stream"); + _liveStreamCancellationTokenSource.Cancel(); + + return _liveStreamTaskCompletionSource.Task; + } + + private async Task StartStreaming(string remoteIp, int localPort, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + { + await Task.Run(async () => + { + var isFirstAttempt = true; + using (var udpClient = _socketFactory.CreateUdpSocket(localPort)) + { + using (var hdHomerunManager = new HdHomerunManager(_socketFactory)) + { + var remoteAddress = new IpAddressInfo(remoteIp, IpAddressFamily.InterNetwork); + IpAddressInfo localAddress = null; + using (var tcpSocket = _socketFactory.CreateSocket(IpAddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp, false)) + { + try + { + tcpSocket.Connect(new IpEndPointInfo(remoteAddress, HdHomerunManager.HdHomeRunPort)); + localAddress = tcpSocket.LocalEndPoint.IpAddress; + tcpSocket.Close(); + } + catch (Exception) + { + _logger.Error("Unable to determine local ip address for Legacy HDHomerun stream."); + return; + } + } + + while (!cancellationToken.IsCancellationRequested) + { + try + { + // send url to start streaming + await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelUrl, _numTuners, cancellationToken).ConfigureAwait(false); + + var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + _logger.Info("Opened HDHR UDP stream from {0}", _channelUrl); + + if (!cancellationToken.IsCancellationRequested) + { + Action onStarted = null; + if (isFirstAttempt) + { + onStarted = () => openTaskCompletionSource.TrySetResult(true); + } + + var stream = new UdpClientStream(udpClient); + await _multicastStream.CopyUntilCancelled(stream, onStarted, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (isFirstAttempt) + { + _logger.ErrorException("Error opening live stream:", ex); + openTaskCompletionSource.TrySetException(ex); + break; + } + + _logger.ErrorException("Error copying live stream, will reopen", ex); + } + + isFirstAttempt = false; + } + + await hdHomerunManager.StopStreaming().ConfigureAwait(false); + udpClient.Dispose(); + _liveStreamTaskCompletionSource.TrySetResult(true); + } + } + + }).ConfigureAwait(false); + } + + public Task CopyToAsync(Stream stream, CancellationToken cancellationToken) + { + return _multicastStream.CopyToAsync(stream); + } + } + + // This handles the ReadAsync function only of a Stream object + // This is used to wrap a UDP socket into a stream for MulticastStream which only uses ReadAsync + public class UdpClientStream : Stream + { + private static int RtpHeaderBytes = 12; + private static int PacketSize = 1316; + private readonly ISocket _udpClient; + bool disposed; + + public UdpClientStream(ISocket udpClient) : base() + { + _udpClient = udpClient; + } + + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + + if (offset + count < 0) + throw new ArgumentOutOfRangeException("offset + count must not be negative", "offset+count"); + + if (offset + count > buffer.Length) + throw new ArgumentException("offset + count must not be greater than the length of buffer", "offset+count"); + + if (disposed) + throw new ObjectDisposedException(typeof(UdpClientStream).ToString()); + + // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize) + // The RTP header will be stripped so see how many reads we need to make to fill the buffer. + int numReads = count / PacketSize; + int totalBytesRead = 0; + + for (int i = 0; i < numReads; ++i) + { + var data = await _udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + + var bytesRead = data.ReceivedBytes - RtpHeaderBytes; + + // remove rtp header + Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, buffer, offset, bytesRead); + offset += bytesRead; + totalBytesRead += bytesRead; + } + return totalBytesRead; + } + + protected override void Dispose(bool disposing) + { + disposed = true; + } + + public override bool CanRead + { + get + { + throw new NotImplementedException(); + } + } + + public override bool CanSeek + { + get + { + throw new NotImplementedException(); + } + } + + public override bool CanWrite + { + get + { + throw new NotImplementedException(); + } + } + + public override long Length + { + get + { + throw new NotImplementedException(); + } + } + + public override long Position + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } +} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 691ff0699..bb303d8fa 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Udp /// <summary> /// The _udp client /// </summary> - private IUdpSocket _udpClient; + private ISocket _udpClient; private readonly ISocketFactory _socketFactory; /// <summary> @@ -148,7 +148,7 @@ namespace Emby.Server.Implementations.Udp { try { - var result = await _udpClient.ReceiveAsync().ConfigureAwait(false); + var result = await _udpClient.ReceiveAsync(CancellationToken.None).ConfigureAwait(false); OnMessageReceived(result); } |
