< 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: 78
Coverable lines: 78
Total lines: 358
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 18
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 10/18/2025 - 12:10:13 AM Line coverage: 0% (0/79) Branch coverage: 0% (0/18) Total lines: 3641/19/2026 - 12:13:54 AM Line coverage: 0% (0/78) Branch coverage: 0% (0/18) Total lines: 358 10/18/2025 - 12:10:13 AM Line coverage: 0% (0/79) Branch coverage: 0% (0/18) Total lines: 3641/19/2026 - 12:13:54 AM Line coverage: 0% (0/78) Branch coverage: 0% (0/18) Total lines: 358

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
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("")]
 32public class UniversalAudioController : BaseJellyfinApiController
 33{
 34    private readonly ILibraryManager _libraryManager;
 35    private readonly ILogger<UniversalAudioController> _logger;
 36    private readonly MediaInfoHelper _mediaInfoHelper;
 37    private readonly AudioHelper _audioHelper;
 38    private readonly DynamicHlsHelper _dynamicHlsHelper;
 39    private readonly IUserManager _userManager;
 40
 41    /// <summary>
 42    /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
 43    /// </summary>
 44    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 45    /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
 46    /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
 47    /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
 48    /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
 49    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 050    public UniversalAudioController(
 051        ILibraryManager libraryManager,
 052        ILogger<UniversalAudioController> logger,
 053        MediaInfoHelper mediaInfoHelper,
 054        AudioHelper audioHelper,
 055        DynamicHlsHelper dynamicHlsHelper,
 056        IUserManager userManager)
 57    {
 058        _libraryManager = libraryManager;
 059        _logger = logger;
 060        _mediaInfoHelper = mediaInfoHelper;
 061        _audioHelper = audioHelper;
 062        _dynamicHlsHelper = dynamicHlsHelper;
 063        _userManager = userManager;
 064    }
 65
 66    /// <summary>
 67    /// Gets an audio stream.
 68    /// </summary>
 69    /// <param name="itemId">The item id.</param>
 70    /// <param name="container">Optional. The audio container.</param>
 71    /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
 72    /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</par
 73    /// <param name="userId">Optional. The user id.</param>
 74    /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
 75    /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
 76    /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
 77    /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
 78    /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be
 79    /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
 80    /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
 81    /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
 82    /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
 83    /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
 84    /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
 85    /// <param name="enableAudioVbrEncoding">Optional. Whether to enable Audio Encoding.</param>
 86    /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
 87    /// <response code="200">Audio stream returned.</response>
 88    /// <response code="302">Redirected to remote audio stream.</response>
 89    /// <response code="404">Item not found.</response>
 90    /// <returns>A <see cref="Task"/> containing the audio file.</returns>
 91    [HttpGet("Audio/{itemId}/universal")]
 92    [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
 93    [Authorize]
 94    [ProducesResponseType(StatusCodes.Status200OK)]
 95    [ProducesResponseType(StatusCodes.Status302Found)]
 96    [ProducesResponseType(StatusCodes.Status404NotFound)]
 97    [ProducesAudioFile]
 98    public async Task<ActionResult> GetUniversalAudioStream(
 99        [FromRoute, Required] Guid itemId,
 100        [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] container,
 101        [FromQuery] string? mediaSourceId,
 102        [FromQuery] string? deviceId,
 103        [FromQuery] Guid? userId,
 104        [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? audioCodec,
 105        [FromQuery] int? maxAudioChannels,
 106        [FromQuery] int? transcodingAudioChannels,
 107        [FromQuery] int? maxStreamingBitrate,
 108        [FromQuery] int? audioBitRate,
 109        [FromQuery] long? startTimeTicks,
 110        [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegex)] string? transcodingContainer,
 111        [FromQuery] MediaStreamProtocol? transcodingProtocol,
 112        [FromQuery] int? maxAudioSampleRate,
 113        [FromQuery] int? maxAudioBitDepth,
 114        [FromQuery] bool? enableRemoteMedia,
 115        [FromQuery] bool enableAudioVbrEncoding = true,
 116        [FromQuery] bool enableRedirection = true)
 117    {
 118        userId = RequestHelpers.GetUserId(User, userId);
 119        var user = userId.IsNullOrEmpty()
 120            ? null
 121            : _userManager.GetUserById(userId.Value);
 122        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 123        if (item is null)
 124        {
 125            return NotFound();
 126        }
 127
 128        var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, transcodi
 129
 130        _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 131
 132        var info = await _mediaInfoHelper.GetPlaybackInfo(
 133                item,
 134                user,
 135                mediaSourceId)
 136            .ConfigureAwait(false);
 137
 138        // set device specific data
 139        foreach (var sourceInfo in info.MediaSources)
 140        {
 141            sourceInfo.TranscodingContainer = transcodingContainer;
 142            sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol;
 143            _mediaInfoHelper.SetDeviceSpecificData(
 144                item,
 145                sourceInfo,
 146                deviceProfile,
 147                User,
 148                maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
 149                startTimeTicks ?? 0,
 150                mediaSourceId ?? string.Empty,
 151                null,
 152                null,
 153                maxAudioChannels,
 154                info.PlaySessionId!,
 155                userId ?? Guid.Empty,
 156                true,
 157                true,
 158                true,
 159                true,
 160                true,
 161                false,
 162                Request.HttpContext.GetNormalizedRemoteIP());
 163        }
 164
 165        _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
 166
 167        foreach (var source in info.MediaSources)
 168        {
 169            _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
 170        }
 171
 172        var mediaSource = info.MediaSources[0];
 173        if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSo
 174        {
 175            return Redirect(mediaSource.Path);
 176        }
 177
 178        // This one is currently very misleading as the SupportsDirectStream actually means "can direct play"
 179        // The definition of DirectStream also seems changed during development
 180        var isStatic = mediaSource.SupportsDirectStream;
 181        if (!isStatic && mediaSource.TranscodingSubProtocol == MediaStreamProtocol.hls)
 182        {
 183            // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
 184            // ffmpeg option -> file extension
 185            //        mpegts -> ts
 186            //          fmp4 -> mp4
 187            var supportedHlsContainers = new[] { "ts", "mp4" };
 188
 189            // fallback to mpegts if device reports some weird value unsupported by hls
 190            var requestedSegmentContainer = Array.Exists(
 191                supportedHlsContainers,
 192                element => string.Equals(element, transcodingContainer, StringComparison.OrdinalIgnoreCase)) ? transcodi
 193            var segmentContainer = Array.Exists(
 194                supportedHlsContainers,
 195                element => string.Equals(element, mediaSource.TranscodingContainer, StringComparison.OrdinalIgnoreCase))
 196            var dynamicHlsRequestDto = new HlsAudioRequestDto
 197            {
 198                Id = itemId,
 199                Container = ".m3u8",
 200                Static = isStatic,
 201                PlaySessionId = info.PlaySessionId,
 202                SegmentContainer = segmentContainer,
 203                MediaSourceId = mediaSourceId,
 204                DeviceId = deviceId,
 205                AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec
 206                EnableAutoStreamCopy = true,
 207                AllowAudioStreamCopy = true,
 208                AllowVideoStreamCopy = true,
 209                AudioSampleRate = maxAudioSampleRate,
 210                MaxAudioChannels = maxAudioChannels,
 211                MaxAudioBitDepth = maxAudioBitDepth,
 212                AudioBitRate = audioBitRate ?? maxStreamingBitrate,
 213                StartTimeTicks = startTimeTicks,
 214                SubtitleMethod = SubtitleDeliveryMethod.Hls,
 215                RequireAvc = false,
 216                DeInterlace = false,
 217                RequireNonAnamorphic = false,
 218                EnableMpegtsM2TsMode = false,
 219                TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 220                Context = EncodingContext.Static,
 221                StreamOptions = new Dictionary<string, string>(),
 222                EnableAdaptiveBitrateStreaming = false,
 223                EnableAudioVbrEncoding = enableAudioVbrEncoding
 224            };
 225
 226            return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
 227                .ConfigureAwait(false);
 228        }
 229
 230        var audioStreamingDto = new StreamingRequestDto
 231        {
 232            Id = itemId,
 233            Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
 234            Static = isStatic,
 235            PlaySessionId = info.PlaySessionId,
 236            MediaSourceId = mediaSourceId,
 237            DeviceId = deviceId,
 238            AudioCodec = audioCodec,
 239            EnableAutoStreamCopy = true,
 240            AllowAudioStreamCopy = true,
 241            AllowVideoStreamCopy = true,
 242            AudioSampleRate = maxAudioSampleRate,
 243            MaxAudioChannels = maxAudioChannels,
 244            AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
 245            MaxAudioBitDepth = maxAudioBitDepth,
 246            AudioChannels = maxAudioChannels,
 247            CopyTimestamps = true,
 248            StartTimeTicks = startTimeTicks,
 249            SubtitleMethod = SubtitleDeliveryMethod.Embed,
 250            TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 251            Context = EncodingContext.Static
 252        };
 253
 254        return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false
 255    }
 256
 257    private DeviceProfile GetDeviceProfile(
 258        string[] containers,
 259        string? transcodingContainer,
 260        string? audioCodec,
 261        MediaStreamProtocol? transcodingProtocol,
 262        int? transcodingAudioChannels,
 263        int? maxAudioSampleRate,
 264        int? maxAudioBitDepth,
 265        int? maxAudioChannels)
 266    {
 0267        var deviceProfile = new DeviceProfile();
 268
 0269        int len = containers.Length;
 0270        var directPlayProfiles = new DirectPlayProfile[len];
 0271        for (int i = 0; i < len; i++)
 272        {
 0273            var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
 274
 0275            var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
 276
 0277            directPlayProfiles[i] = new DirectPlayProfile
 0278            {
 0279                Type = DlnaProfileType.Audio,
 0280                Container = parts[0],
 0281                AudioCodec = audioCodecs
 0282            };
 283        }
 284
 0285        deviceProfile.DirectPlayProfiles = directPlayProfiles;
 286
 0287        deviceProfile.TranscodingProfiles = new[]
 0288        {
 0289            new TranscodingProfile
 0290            {
 0291                Type = DlnaProfileType.Audio,
 0292                Context = EncodingContext.Streaming,
 0293                Container = transcodingContainer ?? "mp3",
 0294                AudioCodec = audioCodec ?? "mp3",
 0295                Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
 0296                MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
 0297            }
 0298        };
 299
 0300        var codecProfiles = new List<CodecProfile>();
 0301        var conditions = new List<ProfileCondition>();
 302
 0303        if (maxAudioSampleRate.HasValue)
 304        {
 305            // codec profile
 0306            conditions.Add(
 0307                new ProfileCondition
 0308                {
 0309                    Condition = ProfileConditionType.LessThanEqual,
 0310                    IsRequired = false,
 0311                    Property = ProfileConditionValue.AudioSampleRate,
 0312                    Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
 0313                });
 314        }
 315
 0316        if (maxAudioBitDepth.HasValue)
 317        {
 318            // codec profile
 0319            conditions.Add(
 0320                new ProfileCondition
 0321                {
 0322                    Condition = ProfileConditionType.LessThanEqual,
 0323                    IsRequired = false,
 0324                    Property = ProfileConditionValue.AudioBitDepth,
 0325                    Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
 0326                });
 327        }
 328
 0329        if (maxAudioChannels.HasValue)
 330        {
 331            // codec profile
 0332            conditions.Add(
 0333                new ProfileCondition
 0334                {
 0335                    Condition = ProfileConditionType.LessThanEqual,
 0336                    IsRequired = false,
 0337                    Property = ProfileConditionValue.AudioChannels,
 0338                    Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
 0339                });
 340        }
 341
 0342        if (conditions.Count > 0)
 343        {
 344            // codec profile
 0345            codecProfiles.Add(
 0346                new CodecProfile
 0347                {
 0348                    Type = CodecType.Audio,
 0349                    Container = string.Join(',', containers),
 0350                    Conditions = conditions.ToArray()
 0351                });
 352        }
 353
 0354        deviceProfile.CodecProfiles = codecProfiles.ToArray();
 355
 0356        return deviceProfile;
 357    }
 358}