< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.TunerHosts.M3UTunerHost
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs
Line coverage
68%
Covered lines: 73
Uncovered lines: 33
Coverable lines: 106
Total lines: 242
Line coverage: 68.8%
Branch coverage
21%
Covered branches: 7
Total branches: 32
Branch coverage: 21.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 12% (12/100) Branch coverage: 0% (0/28) Total lines: 21810/29/2026 - 12:17:11 AM Line coverage: 68.8% (73/106) Branch coverage: 21.8% (7/32) Total lines: 242 5/16/2026 - 12:15:55 AM Line coverage: 12% (12/100) Branch coverage: 0% (0/28) Total lines: 21810/29/2026 - 12:17:11 AM Line coverage: 68.8% (73/106) Branch coverage: 21.8% (7/32) Total lines: 242

Coverage delta

Coverage delta 57 -57

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
get_Type()100%11100%
get_Name()100%210%
GetFullChannelIdPrefix(...)100%210%
GetChannelsInternal()100%210%
GetChannelStream()0%420200%
Validate()100%11100%
GetChannelStreamMediaSources(...)100%11100%
CreateMediaSourceInfo(...)60%1010100%
IsManifest(...)50%22100%
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    {
 133        private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"];
 134        private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"];
 135        private static readonly string[] _manifestExtensions = [".m3u8", ".m3u", ".mpd"];
 36
 37        private readonly IHttpClientFactory _httpClientFactory;
 38        private readonly IServerApplicationHost _appHost;
 39        private readonly INetworkManager _networkManager;
 40        private readonly IMediaSourceManager _mediaSourceManager;
 41        private readonly IStreamHelper _streamHelper;
 42
 43        public M3UTunerHost(
 44            IServerConfigurationManager config,
 45            IMediaSourceManager mediaSourceManager,
 46            ILogger<M3UTunerHost> logger,
 47            IFileSystem fileSystem,
 48            IHttpClientFactory httpClientFactory,
 49            IServerApplicationHost appHost,
 50            INetworkManager networkManager,
 51            IStreamHelper streamHelper)
 3252            : base(config, logger, fileSystem)
 53        {
 3254            _httpClientFactory = httpClientFactory;
 3255            _appHost = appHost;
 3256            _networkManager = networkManager;
 3257            _mediaSourceManager = mediaSourceManager;
 3258            _streamHelper = streamHelper;
 3259        }
 60
 461        public override string Type => "m3u";
 62
 063        public virtual string Name => "M3U Tuner";
 64
 65        private string GetFullChannelIdPrefix(TunerHostInfo info)
 66        {
 067            return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 68        }
 69
 70        protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken canc
 71        {
 072            var channelIdPrefix = GetFullChannelIdPrefix(tuner);
 73
 074            return await new M3uParser(Logger, _httpClientFactory)
 075                .Parse(tuner, channelIdPrefix, cancellationToken)
 076                .ConfigureAwait(false);
 077        }
 78
 79        protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string
 80        {
 081            var tunerCount = tunerHost.TunerCount;
 82
 083            if (tunerCount > 0)
 84            {
 085                var tunerHostId = tunerHost.Id;
 086                var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparis
 87
 088                if (liveStreams.Count() >= tunerCount)
 89                {
 090                    throw new LiveTvConflictException("M3U simultaneous stream limit has been reached.");
 91                }
 92            }
 93
 094            var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false
 95
 096            var mediaSource = sources[0];
 97
 098            if (tunerHost.AllowStreamSharing && mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLoopi
 99            {
 0100                var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
 101
 0102                if (string.IsNullOrEmpty(extension))
 103                {
 104                    try
 105                    {
 0106                        using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
 0107                        using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
 0108                            .SendAsync(message, cancellationToken)
 0109                            .ConfigureAwait(false);
 110
 0111                        if (response.IsSuccessStatusCode)
 112                        {
 0113                            if (_mimeTypesCanShareHttpStream.Contains(response.Content.Headers.ContentType?.MediaType, S
 114                            {
 0115                                return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFac
 116                            }
 117                        }
 0118                    }
 0119                    catch (Exception)
 120                    {
 0121                        Logger.LogWarning("HEAD request to check MIME type failed, shared stream disabled");
 0122                    }
 123                }
 0124                else if (_extensionsCanShareHttpStream.Contains(extension, StringComparison.OrdinalIgnoreCase))
 125                {
 0126                    return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger
 127                }
 128            }
 129
 0130            return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper);
 0131        }
 132
 133        public async Task Validate(TunerHostInfo info)
 134        {
 2135            using (await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).Confi
 136            {
 1137            }
 1138        }
 139
 140        protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo cha
 141        {
 5142            return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) });
 143        }
 144
 145        protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel)
 146        {
 5147            var path = channel.Path;
 148
 5149            var supportsDirectPlay = !info.EnableStreamLooping && info.TunerCount == 0;
 5150            var supportsDirectStream = !info.EnableStreamLooping;
 151
 5152            var protocol = _mediaSourceManager.GetPathProtocol(path);
 153
 5154            var isRemote = true;
 5155            Uri.TryCreate(path, UriKind.Absolute, out var uri);
 5156            if (uri is not null)
 157            {
 5158                isRemote = !_networkManager.IsInLocalNetwork(uri.Host);
 159            }
 160
 161            // A manifest is not a byte stream. Serving one directly hands the client a playlist whose
 162            // variant and segment URIs are relative to the origin, and those do not resolve against the
 163            // Jellyfin url the client fetched it from. Remux or transcode these instead.
 5164            if (IsManifest(path, uri))
 165            {
 3166                supportsDirectPlay = false;
 167            }
 168
 5169            var httpHeaders = new Dictionary<string, string>();
 170
 5171            if (protocol == MediaProtocol.Http)
 172            {
 173                // Use user-defined user-agent. If there isn't one, make it look like a browser.
 5174                httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ?
 5175                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 S
 5176                    info.UserAgent;
 177            }
 178
 5179            var mediaSource = new MediaSourceInfo
 5180            {
 5181                Path = path,
 5182                Protocol = protocol,
 5183                MediaStreams = new MediaStream[]
 5184                {
 5185                    new MediaStream
 5186                    {
 5187                        Type = MediaStreamType.Video,
 5188                        // Set the index to -1 because we don't know the exact index of the video stream within the cont
 5189                        Index = -1,
 5190                        IsInterlaced = true
 5191                    },
 5192                    new MediaStream
 5193                    {
 5194                        Type = MediaStreamType.Audio,
 5195                        // Set the index to -1 because we don't know the exact index of the audio stream within the cont
 5196                        Index = -1
 5197                    }
 5198                },
 5199                RequiresOpening = true,
 5200                RequiresClosing = true,
 5201                RequiresLooping = info.EnableStreamLooping,
 5202
 5203                ReadAtNativeFramerate = info.ReadAtNativeFramerate,
 5204
 5205                Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture),
 5206                IsInfiniteStream = true,
 5207                IsRemote = isRemote,
 5208
 5209                IgnoreDts = info.IgnoreDts,
 5210                SupportsDirectPlay = supportsDirectPlay,
 5211                SupportsDirectStream = supportsDirectStream,
 5212
 5213                RequiredHttpHeaders = httpHeaders,
 5214                UseMostCompatibleTranscodingProfile = !info.AllowFmp4TranscodingContainer,
 5215                FallbackMaxStreamingBitrate = info.FallbackMaxStreamingBitrate
 5216            };
 217
 5218            mediaSource.InferTotalBitrate();
 219
 5220            return mediaSource;
 221        }
 222
 223        /// <summary>
 224        /// Determines whether a channel path points at an HLS or DASH manifest rather than at a byte stream.
 225        /// </summary>
 226        /// <param name="path">The channel path.</param>
 227        /// <param name="uri">The channel path parsed as an absolute uri, or <c>null</c> if it is not one.</param>
 228        /// <returns><c>true</c> if the path names a streaming manifest.</returns>
 229        private static bool IsManifest(string path, Uri uri)
 230        {
 231            // Use the uri path when there is one so that a query string does not hide the extension.
 5232            var extension = Path.GetExtension(uri is null ? path : uri.AbsolutePath);
 233
 5234            return _manifestExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
 235        }
 236
 237        public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
 238        {
 1239            return Task.FromResult(new List<TunerHostInfo>());
 240        }
 241    }
 242}