< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.TunerHosts.M3UTunerHost
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
Line coverage
13%
Covered lines: 9
Uncovered lines: 57
Coverable lines: 66
Total lines: 218
Line coverage: 13.6%
Branch coverage
0%
Covered branches: 0
Total branches: 8
Branch coverage: 0%
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
.cctor()100%210%
.ctor(...)100%11100%
get_Type()100%11100%
get_Name()100%210%
GetFullChannelIdPrefix(...)100%210%
GetChannelStreamMediaSources(...)100%210%
CreateMediaSourceInfo(...)0%7280%
DiscoverDevices(...)100%11100%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Net;
 11using System.Net.Http;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Common.Extensions;
 16using MediaBrowser.Common.Net;
 17using MediaBrowser.Controller;
 18using MediaBrowser.Controller.Configuration;
 19using MediaBrowser.Controller.Library;
 20using MediaBrowser.Controller.LiveTv;
 21using MediaBrowser.Model.Dto;
 22using MediaBrowser.Model.Entities;
 23using MediaBrowser.Model.IO;
 24using MediaBrowser.Model.LiveTv;
 25using MediaBrowser.Model.MediaInfo;
 26using Microsoft.Extensions.Logging;
 27using Microsoft.Net.Http.Headers;
 28
 29namespace Jellyfin.LiveTv.TunerHosts
 30{
 31    public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
 32    {
 033        private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"];
 034        private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"];
 35
 36        private readonly IHttpClientFactory _httpClientFactory;
 37        private readonly IServerApplicationHost _appHost;
 38        private readonly INetworkManager _networkManager;
 39        private readonly IMediaSourceManager _mediaSourceManager;
 40        private readonly IStreamHelper _streamHelper;
 41
 42        public M3UTunerHost(
 43            IServerConfigurationManager config,
 44            IMediaSourceManager mediaSourceManager,
 45            ILogger<M3UTunerHost> logger,
 46            IFileSystem fileSystem,
 47            IHttpClientFactory httpClientFactory,
 48            IServerApplicationHost appHost,
 49            INetworkManager networkManager,
 50            IStreamHelper streamHelper)
 2251            : base(config, logger, fileSystem)
 52        {
 2253            _httpClientFactory = httpClientFactory;
 2254            _appHost = appHost;
 2255            _networkManager = networkManager;
 2256            _mediaSourceManager = mediaSourceManager;
 2257            _streamHelper = streamHelper;
 2258        }
 59
 460        public override string Type => "m3u";
 61
 062        public virtual string Name => "M3U Tuner";
 63
 64        private string GetFullChannelIdPrefix(TunerHostInfo info)
 65        {
 066            return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 67        }
 68
 69        protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken canc
 70        {
 71            var channelIdPrefix = GetFullChannelIdPrefix(tuner);
 72
 73            return await new M3uParser(Logger, _httpClientFactory)
 74                .Parse(tuner, channelIdPrefix, cancellationToken)
 75                .ConfigureAwait(false);
 76        }
 77
 78        protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string
 79        {
 80            var tunerCount = tunerHost.TunerCount;
 81
 82            if (tunerCount > 0)
 83            {
 84                var tunerHostId = tunerHost.Id;
 85                var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparis
 86
 87                if (liveStreams.Count() >= tunerCount)
 88                {
 89                    throw new LiveTvConflictException("M3U simultaneous stream limit has been reached.");
 90                }
 91            }
 92
 93            var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false
 94
 95            var mediaSource = sources[0];
 96
 97            if (tunerHost.AllowStreamSharing && mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLoopi
 98            {
 99                var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
 100
 101                if (string.IsNullOrEmpty(extension))
 102                {
 103                    try
 104                    {
 105                        using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
 106                        using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
 107                            .SendAsync(message, cancellationToken)
 108                            .ConfigureAwait(false);
 109
 110                        if (response.IsSuccessStatusCode)
 111                        {
 112                            if (_mimeTypesCanShareHttpStream.Contains(response.Content.Headers.ContentType?.MediaType, S
 113                            {
 114                                return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFac
 115                            }
 116                        }
 117                    }
 118                    catch (Exception)
 119                    {
 120                        Logger.LogWarning("HEAD request to check MIME type failed, shared stream disabled");
 121                    }
 122                }
 123                else if (_extensionsCanShareHttpStream.Contains(extension, StringComparison.OrdinalIgnoreCase))
 124                {
 125                    return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger
 126                }
 127            }
 128
 129            return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper);
 130        }
 131
 132        public async Task Validate(TunerHostInfo info)
 133        {
 134            using (await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).Confi
 135            {
 136            }
 137        }
 138
 139        protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo cha
 140        {
 0141            return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) });
 142        }
 143
 144        protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel)
 145        {
 0146            var path = channel.Path;
 147
 0148            var supportsDirectPlay = !info.EnableStreamLooping && info.TunerCount == 0;
 0149            var supportsDirectStream = !info.EnableStreamLooping;
 150
 0151            var protocol = _mediaSourceManager.GetPathProtocol(path);
 152
 0153            var isRemote = true;
 0154            if (Uri.TryCreate(path, UriKind.Absolute, out var uri))
 155            {
 0156                isRemote = !_networkManager.IsInLocalNetwork(uri.Host);
 157            }
 158
 0159            var httpHeaders = new Dictionary<string, string>();
 160
 0161            if (protocol == MediaProtocol.Http)
 162            {
 163                // Use user-defined user-agent. If there isn't one, make it look like a browser.
 0164                httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ?
 0165                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 S
 0166                    info.UserAgent;
 167            }
 168
 0169            var mediaSource = new MediaSourceInfo
 0170            {
 0171                Path = path,
 0172                Protocol = protocol,
 0173                MediaStreams = new MediaStream[]
 0174                {
 0175                    new MediaStream
 0176                    {
 0177                        Type = MediaStreamType.Video,
 0178                        // Set the index to -1 because we don't know the exact index of the video stream within the cont
 0179                        Index = -1,
 0180                        IsInterlaced = true
 0181                    },
 0182                    new MediaStream
 0183                    {
 0184                        Type = MediaStreamType.Audio,
 0185                        // Set the index to -1 because we don't know the exact index of the audio stream within the cont
 0186                        Index = -1
 0187                    }
 0188                },
 0189                RequiresOpening = true,
 0190                RequiresClosing = true,
 0191                RequiresLooping = info.EnableStreamLooping,
 0192
 0193                ReadAtNativeFramerate = false,
 0194
 0195                Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 0196                IsInfiniteStream = true,
 0197                IsRemote = isRemote,
 0198
 0199                IgnoreDts = info.IgnoreDts,
 0200                SupportsDirectPlay = supportsDirectPlay,
 0201                SupportsDirectStream = supportsDirectStream,
 0202
 0203                RequiredHttpHeaders = httpHeaders,
 0204                UseMostCompatibleTranscodingProfile = !info.AllowFmp4TranscodingContainer,
 0205                FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate
 0206            };
 207
 0208            mediaSource.InferTotalBitrate();
 209
 0210            return mediaSource;
 211        }
 212
 213        public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
 214        {
 1215            return Task.FromResult(new List<TunerHostInfo>());
 216        }
 217    }
 218}