< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.TunerHosts.HdHomerun.HdHomerunHost
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
Line coverage
12%
Covered lines: 16
Uncovered lines: 114
Coverable lines: 130
Total lines: 557
Line coverage: 12.3%
Branch coverage
11%
Covered branches: 4
Total branches: 34
Branch coverage: 11.7%
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%11100%
get_Name()100%210%
get_Type()100%11100%
get_ChannelIdPrefix()100%210%
GetChannelId(...)100%210%
GetApiUrl(...)100%44100%
GetHdHrIdFromChannelId(...)100%210%
GetMediaSource(...)0%930300%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.Linq;
 9using System.Net;
 10using System.Net.Http;
 11using System.Net.Http.Json;
 12using System.Text.Json;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using Jellyfin.Extensions;
 16using Jellyfin.Extensions.Json;
 17using Jellyfin.Extensions.Json.Converters;
 18using MediaBrowser.Common.Extensions;
 19using MediaBrowser.Common.Net;
 20using MediaBrowser.Controller;
 21using MediaBrowser.Controller.Configuration;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.LiveTv;
 24using MediaBrowser.Model.Dto;
 25using MediaBrowser.Model.Entities;
 26using MediaBrowser.Model.IO;
 27using MediaBrowser.Model.LiveTv;
 28using MediaBrowser.Model.MediaInfo;
 29using MediaBrowser.Model.Net;
 30using Microsoft.Extensions.Logging;
 31
 32namespace Jellyfin.LiveTv.TunerHosts.HdHomerun
 33{
 34    public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
 35    {
 36        private readonly IHttpClientFactory _httpClientFactory;
 37        private readonly IServerApplicationHost _appHost;
 38        private readonly ISocketFactory _socketFactory;
 39        private readonly IStreamHelper _streamHelper;
 40
 41        private readonly JsonSerializerOptions _jsonOptions;
 42
 2943        private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
 44
 45        public HdHomerunHost(
 46            IServerConfigurationManager config,
 47            ILogger<HdHomerunHost> logger,
 48            IFileSystem fileSystem,
 49            IHttpClientFactory httpClientFactory,
 50            IServerApplicationHost appHost,
 51            ISocketFactory socketFactory,
 52            IStreamHelper streamHelper)
 2953            : base(config, logger, fileSystem)
 54        {
 2955            _httpClientFactory = httpClientFactory;
 2956            _appHost = appHost;
 2957            _socketFactory = socketFactory;
 2958            _streamHelper = streamHelper;
 59
 2960            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
 2961            _jsonOptions.Converters.Add(new JsonBoolNumberConverter());
 2962        }
 63
 064        public string Name => "HD Homerun";
 65
 666        public override string Type => "hdhomerun";
 67
 068        protected override string ChannelIdPrefix => "hdhr_";
 69
 70        private string GetChannelId(Channels i)
 071            => ChannelIdPrefix + i.GuideNumber;
 72
 73        internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
 74        {
 75            var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 76
 77            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? 
 78            var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken
 79            if (info.ImportFavoritesOnly)
 80            {
 81                lineup = lineup.Where(i => i.Favorite);
 82            }
 83
 84            return lineup.Where(i => !i.DRM).ToList();
 85        }
 86
 87        protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken canc
 88        {
 89            var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false);
 90
 91            return lineup.Select(i => new HdHomerunChannelInfo
 92            {
 93                Name = i.GuideName,
 94                Number = i.GuideNumber,
 95                Id = GetChannelId(i),
 96                IsFavorite = i.Favorite,
 97                TunerHostId = tuner.Id,
 98                IsHD = i.HD,
 99                AudioCodec = i.AudioCodec,
 100                VideoCodec = i.VideoCodec,
 101                ChannelType = ChannelType.TV,
 102                IsLegacyTuner = (i.URL ?? string.Empty).StartsWith("hdhomerun", StringComparison.OrdinalIgnoreCase),
 103                Path = i.URL
 104            }).Cast<ChannelInfo>().ToList();
 105        }
 106
 107        internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToke
 108        {
 109            var cacheKey = info.Id;
 110
 111            lock (_modelCache)
 112            {
 113                if (!string.IsNullOrEmpty(cacheKey))
 114                {
 115                    if (_modelCache.TryGetValue(cacheKey, out DiscoverResponse response))
 116                    {
 117                        return response;
 118                    }
 119                }
 120            }
 121
 122            try
 123            {
 124                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
 125                    .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellation
 126                    .ConfigureAwait(false);
 127                response.EnsureSuccessStatusCode();
 128                var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancella
 129
 130                if (!string.IsNullOrEmpty(cacheKey))
 131                {
 132                    lock (_modelCache)
 133                    {
 134                        _modelCache[cacheKey] = discoverResponse;
 135                    }
 136                }
 137
 138                return discoverResponse;
 139            }
 140            catch (HttpRequestException ex)
 141            {
 142                if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
 143                {
 144                    const string DefaultValue = "HDHR";
 145                    var discoverResponse = new DiscoverResponse
 146                    {
 147                        ModelNumber = DefaultValue
 148                    };
 149                    if (!string.IsNullOrEmpty(cacheKey))
 150                    {
 151                        // HDHR4 doesn't have this api
 152                        lock (_modelCache)
 153                        {
 154                            _modelCache[cacheKey] = discoverResponse;
 155                        }
 156                    }
 157
 158                    return discoverResponse;
 159                }
 160
 161                throw;
 162            }
 163        }
 164
 165        private static string GetApiUrl(TunerHostInfo info)
 166        {
 7167            var url = info.Url;
 168
 7169            if (string.IsNullOrWhiteSpace(url))
 170            {
 1171                throw new ArgumentException("Invalid tuner info");
 172            }
 173
 6174            if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
 175            {
 6176                url = "http://" + url;
 177            }
 178
 6179            return new Uri(url).AbsoluteUri.TrimEnd('/');
 180        }
 181
 182        private static string GetHdHrIdFromChannelId(string channelId)
 183        {
 0184            return channelId.Split('_')[1];
 185        }
 186
 187        private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string pro
 188        {
 0189            int? width = null;
 0190            int? height = null;
 0191            bool isInterlaced = true;
 0192            string videoCodec = null;
 193
 0194            int? videoBitrate = null;
 195
 0196            var isHd = channelInfo.IsHD ?? true;
 197
 0198            if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase))
 199            {
 0200                width = 1280;
 0201                height = 720;
 0202                isInterlaced = false;
 0203                videoCodec = "h264";
 0204                videoBitrate = 2000000;
 205            }
 0206            else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase))
 207            {
 0208                width = 1920;
 0209                height = 1080;
 0210                isInterlaced = false;
 0211                videoCodec = "h264";
 0212                videoBitrate = 15000000;
 213            }
 0214            else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase))
 215            {
 0216                width = 1280;
 0217                height = 720;
 0218                isInterlaced = false;
 0219                videoCodec = "h264";
 0220                videoBitrate = 8000000;
 221            }
 0222            else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase))
 223            {
 0224                width = 960;
 0225                height = 540;
 0226                isInterlaced = false;
 0227                videoCodec = "h264";
 0228                videoBitrate = 2500000;
 229            }
 0230            else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase))
 231            {
 0232                width = 848;
 0233                height = 480;
 0234                isInterlaced = false;
 0235                videoCodec = "h264";
 0236                videoBitrate = 2000000;
 237            }
 0238            else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase))
 239            {
 0240                width = 640;
 0241                height = 360;
 0242                isInterlaced = false;
 0243                videoCodec = "h264";
 0244                videoBitrate = 1500000;
 245            }
 0246            else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase))
 247            {
 0248                width = 432;
 0249                height = 240;
 0250                isInterlaced = false;
 0251                videoCodec = "h264";
 0252                videoBitrate = 1000000;
 253            }
 254            else
 255            {
 256                // This is for android tv's 1200 condition. Remove once not needed anymore so that we can avoid possible
 0257                if (isHd)
 258                {
 0259                    width = 1920;
 0260                    height = 1080;
 261                }
 262            }
 263
 0264            if (string.IsNullOrWhiteSpace(videoCodec))
 265            {
 0266                videoCodec = channelInfo.VideoCodec;
 267            }
 268
 0269            string audioCodec = channelInfo.AudioCodec;
 270
 0271            videoBitrate ??= isHd ? 15000000 : 2000000;
 272
 0273            int? audioBitrate = isHd ? 448000 : 192000;
 274
 275            // normalize
 0276            if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
 277            {
 0278                videoCodec = "mpeg2video";
 279            }
 280
 0281            string nal = null;
 0282            if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
 283            {
 0284                nal = "0";
 285            }
 286
 0287            var url = GetApiUrl(info);
 288
 0289            var id = profile;
 0290            if (string.IsNullOrWhiteSpace(id))
 291            {
 0292                id = "native";
 293            }
 294
 0295            id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N"
 296
 0297            var mediaSource = new MediaSourceInfo
 0298            {
 0299                Path = url,
 0300                Protocol = MediaProtocol.Udp,
 0301                MediaStreams = new MediaStream[]
 0302                {
 0303                    new MediaStream
 0304                    {
 0305                        Type = MediaStreamType.Video,
 0306                        // Set the index to -1 because we don't know the exact index of the video stream within the cont
 0307                        Index = -1,
 0308                        IsInterlaced = isInterlaced,
 0309                        Codec = videoCodec,
 0310                        Width = width,
 0311                        Height = height,
 0312                        BitRate = videoBitrate,
 0313                        NalLengthSize = nal
 0314                    },
 0315                    new MediaStream
 0316                    {
 0317                        Type = MediaStreamType.Audio,
 0318                        // Set the index to -1 because we don't know the exact index of the audio stream within the cont
 0319                        Index = -1,
 0320                        Codec = audioCodec,
 0321                        BitRate = audioBitrate
 0322                    }
 0323                },
 0324                RequiresOpening = true,
 0325                RequiresClosing = true,
 0326                BufferMs = 0,
 0327                Container = "ts",
 0328                Id = id,
 0329                SupportsDirectPlay = false,
 0330                SupportsDirectStream = true,
 0331                SupportsTranscoding = true,
 0332                IsInfiniteStream = true,
 0333                IgnoreDts = true,
 0334                UseMostCompatibleTranscodingProfile = true, // All HDHR tuners require this
 0335                FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate,
 0336                // IgnoreIndex = true,
 0337                // ReadAtNativeFramerate = true
 0338            };
 339
 0340            mediaSource.InferTotalBitrate();
 341
 0342            return mediaSource;
 343        }
 344
 345        protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelIn
 346        {
 347            var list = new List<MediaSourceInfo>();
 348
 349            var channelId = channel.Id;
 350            var hdhrId = GetHdHrIdFromChannelId(channelId);
 351
 352            if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner)
 353            {
 354                list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
 355            }
 356            else
 357            {
 358                var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false);
 359
 360                if (modelInfo is not null && modelInfo.SupportsTranscoding)
 361                {
 362                    if (tuner.AllowHWTranscoding)
 363                    {
 364                        list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy"));
 365
 366                        list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540"));
 367                        list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480"));
 368                        list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360"));
 369                        list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240"));
 370                        list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile"));
 371                    }
 372
 373                    list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
 374                }
 375
 376                if (list.Count == 0)
 377                {
 378                    list.Add(GetMediaSource(tuner, hdhrId, channel, "native"));
 379                }
 380            }
 381
 382            return list;
 383        }
 384
 385        protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string
 386        {
 387            var tunerCount = tunerHost.TunerCount;
 388
 389            if (tunerCount > 0)
 390            {
 391                var tunerHostId = tunerHost.Id;
 392                var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparis
 393
 394                if (liveStreams.Count() >= tunerCount)
 395                {
 396                    throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached.");
 397                }
 398            }
 399
 400            var profile = streamId.AsSpan().LeftPart('_').ToString();
 401
 402            Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId
 403
 404            var hdhrId = GetHdHrIdFromChannelId(channel.Id);
 405
 406            var hdhomerunChannel = channel as HdHomerunChannelInfo;
 407
 408            var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false);
 409
 410            if (!modelInfo.SupportsTranscoding)
 411            {
 412                profile = "native";
 413            }
 414
 415            var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile);
 416
 417            if (hdhomerunChannel is not null && hdhomerunChannel.IsLegacyTuner)
 418            {
 419                return new HdHomerunUdpStream(
 420                    mediaSource,
 421                    tunerHost,
 422                    streamId,
 423                    new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path),
 424                    modelInfo.TunerCount,
 425                    FileSystem,
 426                    Logger,
 427                    Config,
 428                    _appHost,
 429                    _streamHelper);
 430            }
 431
 432            mediaSource.Protocol = MediaProtocol.Http;
 433
 434            var httpUrl = channel.Path;
 435
 436            // If raw was used, the tuner doesn't support params
 437            if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreC
 438            {
 439                httpUrl += "?transcode=" + profile;
 440            }
 441
 442            mediaSource.Path = httpUrl;
 443
 444            return new SharedHttpStream(
 445                mediaSource,
 446                tunerHost,
 447                streamId,
 448                FileSystem,
 449                _httpClientFactory,
 450                Logger,
 451                Config,
 452                _appHost,
 453                _streamHelper);
 454        }
 455
 456        public async Task Validate(TunerHostInfo info)
 457        {
 458            lock (_modelCache)
 459            {
 460                _modelCache.Clear();
 461            }
 462
 463            try
 464            {
 465                // Test it by pulling down the lineup
 466                var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false);
 467                info.DeviceId = modelInfo.DeviceID;
 468            }
 469            catch (HttpRequestException ex)
 470            {
 471                if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
 472                {
 473                    // HDHR4 doesn't have this api
 474                    return;
 475                }
 476
 477                throw;
 478            }
 479        }
 480
 481        public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationTo
 482        {
 483            lock (_modelCache)
 484            {
 485                _modelCache.Clear();
 486            }
 487
 488            using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs);
 489            using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationT
 490            cancellationToken = linkedCancellationTokenSource.Token;
 491            var list = new List<TunerHostInfo>();
 492
 493            // Create udp broadcast discovery message
 494            byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 };
 495            using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0))
 496            {
 497                // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
 498                try
 499                {
 500                    await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken
 501                    var receiveBuffer = new byte[8192];
 502
 503                    while (!cancellationToken.IsCancellationRequested)
 504                    {
 505                        var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.A
 506                        var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString();
 507
 508                        // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd 
 509                        if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3)
 510                        {
 511                            var deviceAddress = "http://" + deviceIP;
 512
 513                            var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false)
 514
 515                            if (info is not null)
 516                            {
 517                                list.Add(info);
 518                            }
 519                        }
 520                    }
 521                }
 522                catch (OperationCanceledException)
 523                {
 524                }
 525                catch (Exception ex)
 526                {
 527                    // Socket timeout indicates all messages have been received.
 528                    Logger.LogError(ex, "Error while sending discovery message");
 529                }
 530            }
 531
 532            return list;
 533        }
 534
 535        internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
 536        {
 537            var hostInfo = new TunerHostInfo
 538            {
 539                Type = Type,
 540                Url = url
 541            };
 542
 543            var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
 544
 545            hostInfo.DeviceId = modelInfo.DeviceID;
 546            hostInfo.FriendlyName = modelInfo.FriendlyName;
 547            hostInfo.TunerCount = modelInfo.TunerCount;
 548
 549            return hostInfo;
 550        }
 551
 552        private class HdHomerunChannelInfo : ChannelInfo
 553        {
 554            public bool IsLegacyTuner { get; set; }
 555        }
 556    }
 557}