< 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: 187
Coverable lines: 187
Total lines: 360
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 5/6/2026 - 12:15:23 AM Line coverage: 0% (0/186) Branch coverage: 0% (0/68) Total lines: 3598/3/2026 - 12:16:46 AM Line coverage: 0% (0/187) Branch coverage: 0% (0/68) Total lines: 360 5/6/2026 - 12:15:23 AM Line coverage: 0% (0/186) Branch coverage: 0% (0/68) Total lines: 3598/3/2026 - 12:16:46 AM Line coverage: 0% (0/187) Branch coverage: 0% (0/68) Total lines: 360

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                Request,
 0137                mediaSourceId)
 0138            .ConfigureAwait(false);
 139
 140        // set device specific data
 0141        foreach (var sourceInfo in info.MediaSources)
 142        {
 0143            sourceInfo.TranscodingContainer = transcodingContainer;
 0144            sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol;
 0145            _mediaInfoHelper.SetDeviceSpecificData(
 0146                item,
 0147                sourceInfo,
 0148                deviceProfile,
 0149                User,
 0150                maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
 0151                startTimeTicks ?? 0,
 0152                mediaSourceId ?? string.Empty,
 0153                null,
 0154                null,
 0155                maxAudioChannels,
 0156                info.PlaySessionId!,
 0157                userId ?? Guid.Empty,
 0158                true,
 0159                true,
 0160                true,
 0161                true,
 0162                true,
 0163                false,
 0164                Request.HttpContext.GetNormalizedRemoteIP());
 165        }
 166
 0167        _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate, item.Id);
 168
 0169        foreach (var source in info.MediaSources)
 170        {
 0171            _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
 172        }
 173
 0174        var mediaSource = info.MediaSources[0];
 0175        if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSo
 176        {
 0177            return Redirect(mediaSource.Path);
 178        }
 179
 180        // This one is currently very misleading as the SupportsDirectStream actually means "can direct play"
 181        // The definition of DirectStream also seems changed during development
 0182        var isStatic = mediaSource.SupportsDirectStream;
 0183        if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
 184        {
 185            // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
 186            // ffmpeg option -> file extension
 187            //        mpegts -> ts
 188            //          fmp4 -> mp4
 0189            var supportedHlsContainers = new[] { "ts", "mp4" };
 190
 191            // fallback to mpegts if device reports some weird value unsupported by hls
 0192            var requestedSegmentContainer = Array.Exists(
 0193                supportedHlsContainers,
 0194                element => string.Equals(element, transcodingContainer, StringComparison.OrdinalIgnoreCase)) ? transcodi
 0195            var segmentContainer = Array.Exists(
 0196                supportedHlsContainers,
 0197                element => string.Equals(element, mediaSource.TranscodingContainer, StringComparison.OrdinalIgnoreCase))
 0198            var dynamicHlsRequestDto = new HlsAudioRequestDto
 0199            {
 0200                Id = itemId,
 0201                Container = ".m3u8",
 0202                Static = isStatic,
 0203                PlaySessionId = info.PlaySessionId,
 0204                SegmentContainer = segmentContainer,
 0205                MediaSourceId = mediaSourceId,
 0206                DeviceId = deviceId,
 0207                AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec
 0208                EnableAutoStreamCopy = true,
 0209                AllowAudioStreamCopy = true,
 0210                AllowVideoStreamCopy = true,
 0211                AudioSampleRate = maxAudioSampleRate,
 0212                MaxAudioChannels = maxAudioChannels,
 0213                MaxAudioBitDepth = maxAudioBitDepth,
 0214                AudioBitRate = audioBitRate ?? maxStreamingBitrate,
 0215                StartTimeTicks = startTimeTicks,
 0216                SubtitleMethod = SubtitleDeliveryMethod.Hls,
 0217                RequireAvc = false,
 0218                DeInterlace = false,
 0219                RequireNonAnamorphic = false,
 0220                EnableMpegtsM2TsMode = false,
 0221                TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 0222                Context = EncodingContext.Static,
 0223                StreamOptions = new Dictionary<string, string>(),
 0224                EnableAdaptiveBitrateStreaming = false,
 0225                EnableAudioVbrEncoding = enableAudioVbrEncoding
 0226            };
 227
 0228            return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
 0229                .ConfigureAwait(false);
 230        }
 231
 0232        var audioStreamingDto = new StreamingRequestDto
 0233        {
 0234            Id = itemId,
 0235            Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
 0236            Static = isStatic,
 0237            PlaySessionId = info.PlaySessionId,
 0238            MediaSourceId = mediaSourceId,
 0239            DeviceId = deviceId,
 0240            AudioCodec = audioCodec,
 0241            EnableAutoStreamCopy = true,
 0242            AllowAudioStreamCopy = true,
 0243            AllowVideoStreamCopy = true,
 0244            AudioSampleRate = maxAudioSampleRate,
 0245            MaxAudioChannels = maxAudioChannels,
 0246            AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
 0247            MaxAudioBitDepth = maxAudioBitDepth,
 0248            AudioChannels = maxAudioChannels,
 0249            CopyTimestamps = true,
 0250            StartTimeTicks = startTimeTicks,
 0251            SubtitleMethod = SubtitleDeliveryMethod.Embed,
 0252            TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 0253            Context = EncodingContext.Static
 0254        };
 255
 0256        return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false
 0257    }
 258
 259    private DeviceProfile GetDeviceProfile(
 260        string[] containers,
 261        string? transcodingContainer,
 262        string? audioCodec,
 263        MediaStreamProtocol? transcodingProtocol,
 264        int? transcodingAudioChannels,
 265        int? maxAudioSampleRate,
 266        int? maxAudioBitDepth,
 267        int? maxAudioChannels)
 268    {
 0269        var deviceProfile = new DeviceProfile();
 270
 0271        int len = containers.Length;
 0272        var directPlayProfiles = new DirectPlayProfile[len];
 0273        for (int i = 0; i < len; i++)
 274        {
 0275            var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
 276
 0277            var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
 278
 0279            directPlayProfiles[i] = new DirectPlayProfile
 0280            {
 0281                Type = DlnaProfileType.Audio,
 0282                Container = parts[0],
 0283                AudioCodec = audioCodecs
 0284            };
 285        }
 286
 0287        deviceProfile.DirectPlayProfiles = directPlayProfiles;
 288
 0289        deviceProfile.TranscodingProfiles = new[]
 0290        {
 0291            new TranscodingProfile
 0292            {
 0293                Type = DlnaProfileType.Audio,
 0294                Context = EncodingContext.Streaming,
 0295                Container = transcodingContainer ?? "mp3",
 0296                AudioCodec = audioCodec ?? "mp3",
 0297                Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
 0298                MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
 0299            }
 0300        };
 301
 0302        var codecProfiles = new List<CodecProfile>();
 0303        var conditions = new List<ProfileCondition>();
 304
 0305        if (maxAudioSampleRate.HasValue)
 306        {
 307            // codec profile
 0308            conditions.Add(
 0309                new ProfileCondition
 0310                {
 0311                    Condition = ProfileConditionType.LessThanEqual,
 0312                    IsRequired = false,
 0313                    Property = ProfileConditionValue.AudioSampleRate,
 0314                    Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
 0315                });
 316        }
 317
 0318        if (maxAudioBitDepth.HasValue)
 319        {
 320            // codec profile
 0321            conditions.Add(
 0322                new ProfileCondition
 0323                {
 0324                    Condition = ProfileConditionType.LessThanEqual,
 0325                    IsRequired = false,
 0326                    Property = ProfileConditionValue.AudioBitDepth,
 0327                    Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
 0328                });
 329        }
 330
 0331        if (maxAudioChannels.HasValue)
 332        {
 333            // codec profile
 0334            conditions.Add(
 0335                new ProfileCondition
 0336                {
 0337                    Condition = ProfileConditionType.LessThanEqual,
 0338                    IsRequired = false,
 0339                    Property = ProfileConditionValue.AudioChannels,
 0340                    Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
 0341                });
 342        }
 343
 0344        if (conditions.Count > 0)
 345        {
 346            // codec profile
 0347            codecProfiles.Add(
 0348                new CodecProfile
 0349                {
 0350                    Type = CodecType.Audio,
 0351                    Container = string.Join(',', containers),
 0352                    Conditions = conditions.ToArray()
 0353                });
 354        }
 355
 0356        deviceProfile.CodecProfiles = codecProfiles.ToArray();
 357
 0358        return deviceProfile;
 359    }
 360}