< 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
42%
Covered lines: 57
Uncovered lines: 77
Coverable lines: 134
Total lines: 210
Line coverage: 42.5%
Branch coverage
54%
Covered branches: 24
Total branches: 44
Branch coverage: 54.5%
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%210140%
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
 039            && request.MediaSourceId is not null
 040            && TryExtractKeyframes(request.MediaSourceId.Value, request.FilePath, out var keyframeData))
 041        {
 042            segments = ComputeSegments(keyframeData, request.DesiredSegmentLengthMs);
 043        }
 44        else
 045        {
 046            segments = ComputeEqualLengthSegments(request.DesiredSegmentLengthMs, request.TotalRuntimeTicks);
 047        }
 48
 049        var segmentExtension = EncodingHelper.GetSegmentFileExtension(request.SegmentContainer);
 50
 51        // http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
 052        var isHlsInFmp4 = string.Equals(segmentExtension, ".mp4", StringComparison.OrdinalIgnoreCase);
 053        var hlsVersion = isHlsInFmp4 ? "7" : "3";
 54
 055        var builder = new StringBuilder(128);
 56
 057        builder.AppendLine("#EXTM3U")
 058            .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
 059            .Append("#EXT-X-VERSION:")
 060            .Append(hlsVersion)
 061            .AppendLine()
 062            .Append("#EXT-X-TARGETDURATION:")
 063            .Append(Math.Ceiling(segments.Count > 0 ? segments.Max() : request.DesiredSegmentLengthMs))
 064            .AppendLine()
 065            .AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
 66
 067        var index = 0;
 68
 069        if (isHlsInFmp4)
 070        {
 71            // Init file that only includes fMP4 headers
 072            builder.Append("#EXT-X-MAP:URI=\"")
 073                .Append(request.EndpointPrefix)
 074                .Append("-1")
 075                .Append(segmentExtension)
 076                .Append(request.QueryString)
 077                .Append("&runtimeTicks=0")
 078                .Append("&actualSegmentLengthTicks=0")
 079                .Append('"')
 080                .AppendLine();
 081        }
 82
 083        long currentRuntimeInSeconds = 0;
 084        foreach (var length in segments)
 085        {
 86            // Manually convert to ticks to avoid precision loss when converting double
 087            var lengthTicks = Convert.ToInt64(length * TimeSpan.TicksPerSecond);
 088            builder.Append("#EXTINF:")
 089                .Append(length.ToString("0.000000", CultureInfo.InvariantCulture))
 090                .AppendLine(", nodesc")
 091                .Append(request.EndpointPrefix)
 092                .Append(index++)
 093                .Append(segmentExtension)
 094                .Append(request.QueryString)
 095                .Append("&runtimeTicks=")
 096                .Append(currentRuntimeInSeconds)
 097                .Append("&actualSegmentLengthTicks=")
 098                .Append(lengthTicks)
 099                .AppendLine();
 100
 0101            currentRuntimeInSeconds += lengthTicks;
 0102        }
 103
 0104        builder.AppendLine("#EXT-X-ENDLIST");
 105
 0106        return builder.ToString();
 0107    }
 108
 109    private bool TryExtractKeyframes(Guid itemId, string filePath, [NotNullWhen(true)] out KeyframeData? keyframeData)
 0110    {
 0111        keyframeData = null;
 0112        if (!IsExtractionAllowedForFile(filePath, _serverConfigurationManager.GetEncodingOptions().AllowOnDemandMetadata
 0113        {
 0114            return false;
 115        }
 116
 0117        var len = _extractors.Length;
 0118        for (var i = 0; i < len; i++)
 0119        {
 0120            var extractor = _extractors[i];
 0121            if (!extractor.TryExtractKeyframes(itemId, filePath, out var result))
 0122            {
 0123                continue;
 124            }
 125
 0126            keyframeData = result;
 0127            return true;
 128        }
 129
 0130        return false;
 0131    }
 132
 133    internal static bool IsExtractionAllowedForFile(ReadOnlySpan<char> filePath, IReadOnlyList<string> allowedExtensions
 5134    {
 5135        var extension = Path.GetExtension(filePath);
 5136        if (extension.IsEmpty)
 1137        {
 1138            return false;
 139        }
 140
 141        // Remove the leading dot
 4142        var extensionWithoutDot = extension[1..];
 20143        for (var i = 0; i < allowedExtensions.Count; i++)
 8144        {
 8145            var allowedExtension = allowedExtensions[i].AsSpan().TrimStart('.');
 8146            if (extensionWithoutDot.Equals(allowedExtension, StringComparison.OrdinalIgnoreCase))
 2147            {
 2148                return true;
 149            }
 6150        }
 151
 2152        return false;
 5153    }
 154
 155    internal static IReadOnlyList<double> ComputeSegments(KeyframeData keyframeData, int desiredSegmentLengthMs)
 5156    {
 5157        if (keyframeData.KeyframeTicks.Count > 0 && keyframeData.TotalDuration < keyframeData.KeyframeTicks[^1])
 1158        {
 1159            throw new ArgumentException("Invalid duration in keyframe data", nameof(keyframeData));
 160        }
 161
 4162        long lastKeyframe = 0;
 4163        var result = new List<double>();
 164        // Scale the segment length to ticks to match the keyframes
 4165        var desiredSegmentLengthTicks = TimeSpan.FromMilliseconds(desiredSegmentLengthMs).Ticks;
 4166        var desiredCutTime = desiredSegmentLengthTicks;
 30167        for (var j = 0; j < keyframeData.KeyframeTicks.Count; j++)
 11168        {
 11169            var keyframe = keyframeData.KeyframeTicks[j];
 11170            if (keyframe >= desiredCutTime)
 5171            {
 5172                var currentSegmentLength = keyframe - lastKeyframe;
 5173                result.Add(TimeSpan.FromTicks(currentSegmentLength).TotalSeconds);
 5174                lastKeyframe = keyframe;
 5175                desiredCutTime += desiredSegmentLengthTicks;
 5176            }
 11177        }
 178
 4179        result.Add(TimeSpan.FromTicks(keyframeData.TotalDuration - lastKeyframe).TotalSeconds);
 4180        return result;
 4181    }
 182
 183    internal static double[] ComputeEqualLengthSegments(int desiredSegmentLengthMs, long totalRuntimeTicks)
 7184    {
 7185        if (desiredSegmentLengthMs == 0 || totalRuntimeTicks == 0)
 2186        {
 2187            throw new InvalidOperationException($"Invalid segment length ({desiredSegmentLengthMs}) or runtime ticks ({t
 188        }
 189
 5190        var desiredSegmentLength = TimeSpan.FromMilliseconds(desiredSegmentLengthMs);
 191
 5192        var segmentLengthTicks = desiredSegmentLength.Ticks;
 5193        var wholeSegments = totalRuntimeTicks / segmentLengthTicks;
 5194        var remainingTicks = totalRuntimeTicks % segmentLengthTicks;
 195
 5196        var segmentsLen = wholeSegments + (remainingTicks == 0 ? 0 : 1);
 5197        var segments = new double[segmentsLen];
 38198        for (int i = 0; i < wholeSegments; i++)
 14199        {
 14200            segments[i] = desiredSegmentLength.TotalSeconds;
 14201        }
 202
 5203        if (remainingTicks != 0)
 4204        {
 4205            segments[^1] = TimeSpan.FromTicks(remainingTicks).TotalSeconds;
 4206        }
 207
 5208        return segments;
 5209    }
 210}