< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.UniversalAudioController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/UniversalAudioController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 186
Coverable lines: 186
Total lines: 359
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 68
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 0% (0/78) Branch coverage: 0% (0/18) Total lines: 3584/19/2026 - 12:14:27 AM Line coverage: 0% (0/186) Branch coverage: 0% (0/68) Total lines: 3584/27/2026 - 12:15:04 AM Line coverage: 0% (0/186) Branch coverage: 0% (0/68) Total lines: 359 1/23/2026 - 12:11:06 AM Line coverage: 0% (0/78) Branch coverage: 0% (0/18) Total lines: 3584/19/2026 - 12:14:27 AM Line coverage: 0% (0/186) Branch coverage: 0% (0/68) Total lines: 3584/27/2026 - 12:15:04 AM Line coverage: 0% (0/186) Branch coverage: 0% (0/68) Total lines: 359

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GetUniversalAudioStream()0%2550500%
GetDeviceProfile(...)0%342180%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/UniversalAudioController.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.ComponentModel.DataAnnotations;
 4using System.Globalization;
 5using System.Linq;
 6using System.Threading.Tasks;
 7using Jellyfin.Api.Attributes;
 8using Jellyfin.Api.Helpers;
 9using Jellyfin.Api.ModelBinders;
 10using Jellyfin.Api.Models.StreamingDtos;
 11using Jellyfin.Data.Enums;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Common.Extensions;
 14using MediaBrowser.Controller.Entities;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.MediaEncoding;
 17using MediaBrowser.Controller.Streaming;
 18using MediaBrowser.Model.Dlna;
 19using MediaBrowser.Model.MediaInfo;
 20using MediaBrowser.Model.Session;
 21using Microsoft.AspNetCore.Authorization;
 22using Microsoft.AspNetCore.Http;
 23using Microsoft.AspNetCore.Mvc;
 24using Microsoft.Extensions.Logging;
 25
 26namespace Jellyfin.Api.Controllers;
 27
 28/// <summary>
 29/// The universal audio controller.
 30/// </summary>
 31[Route("")]
 32[Tags("Audio")]
 33public class UniversalAudioController : BaseJellyfinApiController
 34{
 35    private readonly ILibraryManager _libraryManager;
 36    private readonly ILogger<UniversalAudioController> _logger;
 37    private readonly MediaInfoHelper _mediaInfoHelper;
 38    private readonly AudioHelper _audioHelper;
 39    private readonly DynamicHlsHelper _dynamicHlsHelper;
 40    private readonly IUserManager _userManager;
 41
 42    /// <summary>
 43    /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
 44    /// </summary>
 45    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 46    /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
 47    /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
 48    /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
 49    /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
 50    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 051    public UniversalAudioController(
 052        ILibraryManager libraryManager,
 053        ILogger<UniversalAudioController> logger,
 054        MediaInfoHelper mediaInfoHelper,
 055        AudioHelper audioHelper,
 056        DynamicHlsHelper dynamicHlsHelper,
 057        IUserManager userManager)
 58    {
 059        _libraryManager = libraryManager;
 060        _logger = logger;
 061        _mediaInfoHelper = mediaInfoHelper;
 062        _audioHelper = audioHelper;
 063        _dynamicHlsHelper = dynamicHlsHelper;
 064        _userManager = userManager;
 065    }
 66
 67    /// <summary>
 68    /// Gets an audio stream.
 69    /// </summary>
 70    /// <param name="itemId">The item id.</param>
 71    /// <param name="container">Optional. The audio container.</param>
 72    /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
 73    /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</par
 74    /// <param name="userId">Optional. The user id.</param>
 75    /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
 76    /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
 77    /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
 78    /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
 79    /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be
 80    /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
 81    /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
 82    /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
 83    /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
 84    /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
 85    /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
 86    /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
 87    /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
 88    /// <response code="200">Audio stream returned.</response>
 89    /// <response code="302">Redirected to remote audio stream.</response>
 90    /// <response code="404">Item not found.</response>
 91    /// <returns>A <see cref="Task"/> containing the audio file.</returns>
 92    [HttpGet("Audio/{itemId}/universal")]
 93    [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
 94    [Authorize]
 95    [ProducesResponseType(StatusCodes.Status200OK)]
 96    [ProducesResponseType(StatusCodes.Status302Found)]
 97    [ProducesResponseType(StatusCodes.Status404NotFound)]
 98    [ProducesAudioFile]
 99    public async Task<ActionResult> GetUniversalAudioStream(
 100        [FromRoute, Required] Guid itemId,
 101        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] container,
 102        [FromQuery] string? mediaSourceId,
 103        [FromQuery] string? deviceId,
 104        [FromQuery] Guid? userId,
 105        [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec,
 106        [FromQuery] int? maxAudioChannels,
 107        [FromQuery] int? transcodingAudioChannels,
 108        [FromQuery] int? maxStreamingBitrate,
 109        [FromQuery] int? audioBitRate,
 110        [FromQuery] long? startTimeTicks,
 111        [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer,
 112        [FromQuery] MediaStreamProtocol? transcodingProtocol,
 113        [FromQuery] int? maxAudioSampleRate,
 114        [FromQuery] int? maxAudioBitDepth,
 115        [FromQuery] bool? enableRemoteMedia,
 116        [FromQuery] bool enableAudioVbrEncoding = true,
 117        [FromQuery] bool enableRedirection = true)
 118    {
 0119        userId = RequestHelpers.GetUserId(User, userId);
 0120        var user = userId.IsNullOrEmpty()
 0121            ? null
 0122            : _userManager.GetUserById(userId.Value);
 0123        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 0124        if (item is null)
 125        {
 0126            return NotFound();
 127        }
 128
 0129        var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, transcodi
 130
 0131        _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 132
 0133        var info = await _mediaInfoHelper.GetPlaybackInfo(
 0134                item,
 0135                user,
 0136                mediaSourceId)
 0137            .ConfigureAwait(false);
 138
 139        // set device specific data
 0140        foreach (var sourceInfo in info.MediaSources)
 141        {
 0142            sourceInfo.TranscodingContainer = transcodingContainer;
 0143            sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol;
 0144            _mediaInfoHelper.SetDeviceSpecificData(
 0145                item,
 0146                sourceInfo,
 0147                deviceProfile,
 0148                User,
 0149                maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
 0150                startTimeTicks ?? 0,
 0151                mediaSourceId ?? string.Empty,
 0152                null,
 0153                null,
 0154                maxAudioChannels,
 0155                info.PlaySessionId!,
 0156                userId ?? Guid.Empty,
 0157                true,
 0158                true,
 0159                true,
 0160                true,
 0161                true,
 0162                false,
 0163                Request.HttpContext.GetNormalizedRemoteIP());
 164        }
 165
 0166        _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
 167
 0168        foreach (var source in info.MediaSources)
 169        {
 0170            _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
 171        }
 172
 0173        var mediaSource = info.MediaSources[0];
 0174        if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSo
 175        {
 0176            return Redirect(mediaSource.Path);
 177        }
 178
 179        // This one is currently very misleading as the SupportsDirectStream actually means "can direct play"
 180        // The definition of DirectStream also seems changed during development
 0181        var isStatic = mediaSource.SupportsDirectStream;
 0182        if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
 183        {
 184            // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
 185            // ffmpeg option -> file extension
 186            //        mpegts -> ts
 187            //          fmp4 -> mp4
 0188            var supportedHlsContainers = new[] { "ts", "mp4" };
 189
 190            // fallback to mpegts if device reports some weird value unsupported by hls
 0191            var requestedSegmentContainer = Array.Exists(
 0192                supportedHlsContainers,
 0193                element => string.Equals(element, transcodingContainer, StringComparison.OrdinalIgnoreCase)) ? transcodi
 0194            var segmentContainer = Array.Exists(
 0195                supportedHlsContainers,
 0196                element => string.Equals(element, mediaSource.TranscodingContainer, StringComparison.OrdinalIgnoreCase))
 0197            var dynamicHlsRequestDto = new HlsAudioRequestDto
 0198            {
 0199                Id = itemId,
 0200                Container = ".m3u8",
 0201                Static = isStatic,
 0202                PlaySessionId = info.PlaySessionId,
 0203                SegmentContainer = segmentContainer,
 0204                MediaSourceId = mediaSourceId,
 0205                DeviceId = deviceId,
 0206                AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec
 0207                EnableAutoStreamCopy = true,
 0208                AllowAudioStreamCopy = true,
 0209                AllowVideoStreamCopy = true,
 0210                AudioSampleRate = maxAudioSampleRate,
 0211                MaxAudioChannels = maxAudioChannels,
 0212                MaxAudioBitDepth = maxAudioBitDepth,
 0213                AudioBitRate = audioBitRate ?? maxStreamingBitrate,
 0214                StartTimeTicks = startTimeTicks,
 0215                SubtitleMethod = SubtitleDeliveryMethod.Hls,
 0216                RequireAvc = false,
 0217                DeInterlace = false,
 0218                RequireNonAnamorphic = false,
 0219                EnableMpegtsM2TsMode = false,
 0220                TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 0221                Context = EncodingContext.Static,
 0222                StreamOptions = new Dictionary<string, string>(),
 0223                EnableAdaptiveBitrateStreaming = false,
 0224                EnableAudioVbrEncoding = enableAudioVbrEncoding
 0225            };
 226
 0227            return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
 0228                .ConfigureAwait(false);
 229        }
 230
 0231        var audioStreamingDto = new StreamingRequestDto
 0232        {
 0233            Id = itemId,
 0234            Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
 0235            Static = isStatic,
 0236            PlaySessionId = info.PlaySessionId,
 0237            MediaSourceId = mediaSourceId,
 0238            DeviceId = deviceId,
 0239            AudioCodec = audioCodec,
 0240            EnableAutoStreamCopy = true,
 0241            AllowAudioStreamCopy = true,
 0242            AllowVideoStreamCopy = true,
 0243            AudioSampleRate = maxAudioSampleRate,
 0244            MaxAudioChannels = maxAudioChannels,
 0245            AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
 0246            MaxAudioBitDepth = maxAudioBitDepth,
 0247            AudioChannels = maxAudioChannels,
 0248            CopyTimestamps = true,
 0249            StartTimeTicks = startTimeTicks,
 0250            SubtitleMethod = SubtitleDeliveryMethod.Embed,
 0251            TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 0252            Context = EncodingContext.Static
 0253        };
 254
 0255        return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false
 0256    }
 257
 258    private DeviceProfile GetDeviceProfile(
 259        string[] containers,
 260        string? transcodingContainer,
 261        string? audioCodec,
 262        MediaStreamProtocol? transcodingProtocol,
 263        int? transcodingAudioChannels,
 264        int? maxAudioSampleRate,
 265        int? maxAudioBitDepth,
 266        int? maxAudioChannels)
 267    {
 0268        var deviceProfile = new DeviceProfile();
 269
 0270        int len = containers.Length;
 0271        var directPlayProfiles = new DirectPlayProfile[len];
 0272        for (int i = 0; i < len; i++)
 273        {
 0274            var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
 275
 0276            var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
 277
 0278            directPlayProfiles[i] = new DirectPlayProfile
 0279            {
 0280                Type = DlnaProfileType.Audio,
 0281                Container = parts[0],
 0282                AudioCodec = audioCodecs
 0283            };
 284        }
 285
 0286        deviceProfile.DirectPlayProfiles = directPlayProfiles;
 287
 0288        deviceProfile.TranscodingProfiles = new[]
 0289        {
 0290            new TranscodingProfile
 0291            {
 0292                Type = DlnaProfileType.Audio,
 0293                Context = EncodingContext.Streaming,
 0294                Container = transcodingContainer ?? "mp3",
 0295                AudioCodec = audioCodec ?? "mp3",
 0296                Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
 0297                MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
 0298            }
 0299        };
 300
 0301        var codecProfiles = new List<CodecProfile>();
 0302        var conditions = new List<ProfileCondition>();
 303
 0304        if (maxAudioSampleRate.HasValue)
 305        {
 306            // codec profile
 0307            conditions.Add(
 0308                new ProfileCondition
 0309                {
 0310                    Condition = ProfileConditionType.LessThanEqual,
 0311                    IsRequired = false,
 0312                    Property = ProfileConditionValue.AudioSampleRate,
 0313                    Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
 0314                });
 315        }
 316
 0317        if (maxAudioBitDepth.HasValue)
 318        {
 319            // codec profile
 0320            conditions.Add(
 0321                new ProfileCondition
 0322                {
 0323                    Condition = ProfileConditionType.LessThanEqual,
 0324                    IsRequired = false,
 0325                    Property = ProfileConditionValue.AudioBitDepth,
 0326                    Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
 0327                });
 328        }
 329
 0330        if (maxAudioChannels.HasValue)
 331        {
 332            // codec profile
 0333            conditions.Add(
 0334                new ProfileCondition
 0335                {
 0336                    Condition = ProfileConditionType.LessThanEqual,
 0337                    IsRequired = false,
 0338                    Property = ProfileConditionValue.AudioChannels,
 0339                    Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
 0340                });
 341        }
 342
 0343        if (conditions.Count > 0)
 344        {
 345            // codec profile
 0346            codecProfiles.Add(
 0347                new CodecProfile
 0348                {
 0349                    Type = CodecType.Audio,
 0350                    Container = string.Join(',', containers),
 0351                    Conditions = conditions.ToArray()
 0352                });
 353        }
 354
 0355        deviceProfile.CodecProfiles = codecProfiles.ToArray();
 356
 0357        return deviceProfile;
 358    }
 359}