< Summary - Jellyfin

Information
Class: Jellyfin.MediaEncoding.Hls.Playlist.DynamicHlsPlaylistGenerator
Assembly: Jellyfin.MediaEncoding.Hls
File(s): /srv/git/jellyfin/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs
Line coverage
43%
Covered lines: 57
Uncovered lines: 75
Coverable lines: 132
Total lines: 208
Line coverage: 43.1%
Branch coverage
57%
Covered branches: 24
Total branches: 42
Branch coverage: 57.1%
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%210%
CreateMainPlaylist(...)0%156120%
TryExtractKeyframes(...)0%4260%
IsExtractionAllowedForFile(...)100%66100%
ComputeSegments(...)100%88100%
ComputeEqualLengthSegments(...)100%1010100%

File(s)

/srv/git/jellyfin/src/Jellyfin.MediaEncoding.Hls/Playlist/DynamicHlsPlaylistGenerator.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics.CodeAnalysis;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Text;
 8using Jellyfin.MediaEncoding.Hls.Extractors;
 9using Jellyfin.MediaEncoding.Keyframes;
 10using MediaBrowser.Common.Configuration;
 11using MediaBrowser.Controller.Configuration;
 12using MediaBrowser.Controller.MediaEncoding;
 13
 14namespace Jellyfin.MediaEncoding.Hls.Playlist;
 15
 16/// <inheritdoc />
 17public class DynamicHlsPlaylistGenerator : IDynamicHlsPlaylistGenerator
 18{
 19    private readonly IServerConfigurationManager _serverConfigurationManager;
 20    private readonly IKeyframeExtractor[] _extractors;
 21
 22    /// <summary>
 23    /// Initializes a new instance of the <see cref="DynamicHlsPlaylistGenerator"/> class.
 24    /// </summary>
 25    /// <param name="serverConfigurationManager">An instance of the see <see cref="IServerConfigurationManager"/> interf
 26    /// <param name="extractors">An instance of <see cref="IEnumerable{IKeyframeExtractor}"/>.</param>
 27    public DynamicHlsPlaylistGenerator(IServerConfigurationManager serverConfigurationManager, IEnumerable<IKeyframeExtr
 028    {
 029        _serverConfigurationManager = serverConfigurationManager;
 030        _extractors = extractors.Where(e => e.IsMetadataBased).ToArray();
 031    }
 32
 33    /// <inheritdoc />
 34    public string CreateMainPlaylist(CreateMainPlaylistRequest request)
 035    {
 36        IReadOnlyList<double> segments;
 37        // For video transcodes it is sufficient with equal length segments as ffmpeg will create new keyframes
 038        if (request.IsRemuxingVideo && TryExtractKeyframes(request.FilePath, out var keyframeData))
 039        {
 040            segments = ComputeSegments(keyframeData, request.DesiredSegmentLengthMs);
 041        }
 42        else
 043        {
 044            segments = ComputeEqualLengthSegments(request.DesiredSegmentLengthMs, request.TotalRuntimeTicks);
 045        }
 46
 047        var segmentExtension = EncodingHelper.GetSegmentFileExtension(request.SegmentContainer);
 48
 49        // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
 050        var isHlsInFmp4 = string.Equals(segmentExtension, ".mp4", StringComparison.OrdinalIgnoreCase);
 051        var hlsVersion = isHlsInFmp4 ? "7" : "3";
 52
 053        var builder = new StringBuilder(128);
 54
 055        builder.AppendLine("#EXTM3U")
 056            .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
 057            .Append("#EXT-X-VERSION:")
 058            .Append(hlsVersion)
 059            .AppendLine()
 060            .Append("#EXT-X-TARGETDURATION:")
 061            .Append(Math.Ceiling(segments.Count > 0 ? segments.Max() : request.DesiredSegmentLengthMs))
 062            .AppendLine()
 063            .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
 64
 065        var index = 0;
 66
 067        if (isHlsInFmp4)
 068        {
 69            // Init file that only includes fMP4 headers
 070            builder.Append("#EXT-X-MAP:URI=\"")
 071                .Append(request.EndpointPrefix)
 072                .Append("-1")
 073                .Append(segmentExtension)
 074                .Append(request.QueryString)
 075                .Append("&runtimeTicks=0")
 076                .Append("&actualSegmentLengthTicks=0")
 077                .Append('"')
 078                .AppendLine();
 079        }
 80
 081        long currentRuntimeInSeconds = 0;
 082        foreach (var length in segments)
 083        {
 84            // Manually convert to ticks to avoid precision loss when converting double
 085            var lengthTicks = Convert.ToInt64(length * TimeSpan.TicksPerSecond);
 086            builder.Append("#EXTINF:")
 087                .Append(length.ToString("0.000000", CultureInfo.InvariantCulture))
 088                .AppendLine(", nodesc")
 089                .Append(request.EndpointPrefix)
 090                .Append(index++)
 091                .Append(segmentExtension)
 092                .Append(request.QueryString)
 093                .Append("&runtimeTicks=")
 094                .Append(currentRuntimeInSeconds)
 095                .Append("&actualSegmentLengthTicks=")
 096                .Append(lengthTicks)
 097                .AppendLine();
 98
 099            currentRuntimeInSeconds += lengthTicks;
 0100        }
 101
 0102        builder.AppendLine("#EXT-X-ENDLIST");
 103
 0104        return builder.ToString();
 0105    }
 106
 107    private bool TryExtractKeyframes(string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
 0108    {
 0109        keyframeData = null;
 0110        if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowOnDemandMetadata
 0111        {
 0112            return false;
 113        }
 114
 0115        var len = _extractors.Length;
 0116        for (var i = 0; i < len; i++)
 0117        {
 0118            var extractor = _extractors[i];
 0119            if (!extractor.TryExtractKeyframes(filePath, out var result))
 0120            {
 0121                continue;
 122            }
 123
 0124            keyframeData = result;
 0125            return true;
 126        }
 127
 0128        return false;
 0129    }
 130
 131    internal static bool IsExtractionAllowedForFile(ReadOnlySpan<char> filePath, IReadOnlyList<string> allowedExtensions
 5132    {
 5133        var extension = Path.GetExtension(filePath);
 5134        if (extension.IsEmpty)
 1135        {
 1136            return false;
 137        }
 138
 139        // Remove the leading dot
 4140        var extensionWithoutDot = extension[1..];
 20141        for (var i = 0; i < allowedExtensions.Count; i++)
 8142        {
 8143            var allowedExtension = allowedExtensions[i].AsSpan().TrimStart('.');
 8144            if (extensionWithoutDot.Equals(allowedExtension, StringComparison.OrdinalIgnoreCase))
 2145            {
 2146                return true;
 147            }
 6148        }
 149
 2150        return false;
 5151    }
 152
 153    internal static IReadOnlyList<double> ComputeSegments(KeyframeData keyframeData, int desiredSegmentLengthMs)
 5154    {
 5155        if (keyframeData.KeyframeTicks.Count > 0 && keyframeData.TotalDuration < keyframeData.KeyframeTicks[^1])
 1156        {
 1157            throw new ArgumentException("Invalid duration in keyframe data", nameof(keyframeData));
 158        }
 159
 4160        long lastKeyframe = 0;
 4161        var result = new List<double>();
 162        // Scale the segment length to ticks to match the keyframes
 4163        var desiredSegmentLengthTicks = TimeSpan.FromMilliseconds(desiredSegmentLengthMs).Ticks;
 4164        var desiredCutTime = desiredSegmentLengthTicks;
 30165        for (var j = 0; j < keyframeData.KeyframeTicks.Count; j++)
 11166        {
 11167            var keyframe = keyframeData.KeyframeTicks[j];
 11168            if (keyframe >= desiredCutTime)
 5169            {
 5170                var currentSegmentLength = keyframe - lastKeyframe;
 5171                result.Add(TimeSpan.FromTicks(currentSegmentLength).TotalSeconds);
 5172                lastKeyframe = keyframe;
 5173                desiredCutTime += desiredSegmentLengthTicks;
 5174            }
 11175        }
 176
 4177        result.Add(TimeSpan.FromTicks(keyframeData.TotalDuration - lastKeyframe).TotalSeconds);
 4178        return result;
 4179    }
 180
 181    internal static double[] ComputeEqualLengthSegments(int desiredSegmentLengthMs, long totalRuntimeTicks)
 7182    {
 7183        if (desiredSegmentLengthMs == 0 || totalRuntimeTicks == 0)
 2184        {
 2185            throw new InvalidOperationException($"Invalid segment length ({desiredSegmentLengthMs}) or runtime ticks ({t
 186        }
 187
 5188        var desiredSegmentLength = TimeSpan.FromMilliseconds(desiredSegmentLengthMs);
 189
 5190        var segmentLengthTicks = desiredSegmentLength.Ticks;
 5191        var wholeSegments = totalRuntimeTicks / segmentLengthTicks;
 5192        var remainingTicks = totalRuntimeTicks % segmentLengthTicks;
 193
 5194        var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
 5195        var segments = new double[segmentsLen];
 38196        for (int i = 0; i < wholeSegments; i++)
 14197        {
 14198            segments[i] = desiredSegmentLength.TotalSeconds;
 14199        }
 200
 5201        if (remainingTicks != 0)
 4202        {
 4203            segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
 4204        }
 205
 5206        return segments;
 5207    }
 208}