< 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: 79
Coverable lines: 79
Total lines: 364
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

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="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</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(CommaDelimitedArrayModelBinder))] string[] container,
 102        [FromQuery] string? mediaSourceId,
 103        [FromQuery] string? deviceId,
 104        [FromQuery] Guid? userId,
 105        [FromQuery] [RegularExpression(EncodingHelper.ValidationRegex)] 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.ValidationRegex)] 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 breakOnNonKeyFrames = false,
 118        [FromQuery] bool enableRedirection = true)
 119    {
 120        userId = RequestHelpers.GetUserId(User, userId);
 121        var user = userId.IsNullOrEmpty()
 122            ? null
 123            : _userManager.GetUserById(userId.Value);
 124        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 125        if (item is null)
 126        {
 127            return NotFound();
 128        }
 129
 130        var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNo
 131
 132        _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 133
 134        var info = await _mediaInfoHelper.GetPlaybackInfo(
 135                item,
 136                user,
 137                mediaSourceId)
 138            .ConfigureAwait(false);
 139
 140        // set device specific data
 141        foreach (var sourceInfo in info.MediaSources)
 142        {
 143            sourceInfo.TranscodingContainer = transcodingContainer;
 144            sourceInfo.TranscodingSubProtocol = transcodingProtocol ?? sourceInfo.TranscodingSubProtocol;
 145            _mediaInfoHelper.SetDeviceSpecificData(
 146                item,
 147                sourceInfo,
 148                deviceProfile,
 149                User,
 150                maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
 151                startTimeTicks ?? 0,
 152                mediaSourceId ?? string.Empty,
 153                null,
 154                null,
 155                maxAudioChannels,
 156                info.PlaySessionId!,
 157                userId ?? Guid.Empty,
 158                true,
 159                true,
 160                true,
 161                true,
 162                true,
 163                false,
 164                Request.HttpContext.GetNormalizedRemoteIP());
 165        }
 166
 167        _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
 168
 169        foreach (var source in info.MediaSources)
 170        {
 171            _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
 172        }
 173
 174        var mediaSource = info.MediaSources[0];
 175        if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSo
 176        {
 177            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
 182        var isStatic = mediaSource.SupportsDirectStream;
 183        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
 189            var supportedHlsContainers = new[] { "ts", "mp4" };
 190
 191            // fallback to mpegts if device reports some weird value unsupported by hls
 192            var requestedSegmentContainer = Array.Exists(
 193                supportedHlsContainers,
 194                element => string.Equals(element, transcodingContainer, StringComparison.OrdinalIgnoreCase)) ? transcodi
 195            var segmentContainer = Array.Exists(
 196                supportedHlsContainers,
 197                element => string.Equals(element, mediaSource.TranscodingContainer, StringComparison.OrdinalIgnoreCase))
 198            var dynamicHlsRequestDto = new HlsAudioRequestDto
 199            {
 200                Id = itemId,
 201                Container = ".m3u8",
 202                Static = isStatic,
 203                PlaySessionId = info.PlaySessionId,
 204                SegmentContainer = segmentContainer,
 205                MediaSourceId = mediaSourceId,
 206                DeviceId = deviceId,
 207                AudioCodec = mediaSource.TranscodeReasons == TranscodeReason.ContainerNotSupported ? "copy" : audioCodec
 208                EnableAutoStreamCopy = true,
 209                AllowAudioStreamCopy = true,
 210                AllowVideoStreamCopy = true,
 211                BreakOnNonKeyFrames = breakOnNonKeyFrames,
 212                AudioSampleRate = maxAudioSampleRate,
 213                MaxAudioChannels = maxAudioChannels,
 214                MaxAudioBitDepth = maxAudioBitDepth,
 215                AudioBitRate = audioBitRate ?? maxStreamingBitrate,
 216                StartTimeTicks = startTimeTicks,
 217                SubtitleMethod = SubtitleDeliveryMethod.Hls,
 218                RequireAvc = false,
 219                DeInterlace = false,
 220                RequireNonAnamorphic = false,
 221                EnableMpegtsM2TsMode = false,
 222                TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 223                Context = EncodingContext.Static,
 224                StreamOptions = new Dictionary<string, string>(),
 225                EnableAdaptiveBitrateStreaming = true,
 226                EnableAudioVbrEncoding = enableAudioVbrEncoding
 227            };
 228
 229            return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
 230                .ConfigureAwait(false);
 231        }
 232
 233        var audioStreamingDto = new StreamingRequestDto
 234        {
 235            Id = itemId,
 236            Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
 237            Static = isStatic,
 238            PlaySessionId = info.PlaySessionId,
 239            MediaSourceId = mediaSourceId,
 240            DeviceId = deviceId,
 241            AudioCodec = audioCodec,
 242            EnableAutoStreamCopy = true,
 243            AllowAudioStreamCopy = true,
 244            AllowVideoStreamCopy = true,
 245            BreakOnNonKeyFrames = breakOnNonKeyFrames,
 246            AudioSampleRate = maxAudioSampleRate,
 247            MaxAudioChannels = maxAudioChannels,
 248            AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
 249            MaxAudioBitDepth = maxAudioBitDepth,
 250            AudioChannels = maxAudioChannels,
 251            CopyTimestamps = true,
 252            StartTimeTicks = startTimeTicks,
 253            SubtitleMethod = SubtitleDeliveryMethod.Embed,
 254            TranscodeReasons = mediaSource.TranscodeReasons == 0 ? null : mediaSource.TranscodeReasons.ToString(),
 255            Context = EncodingContext.Static
 256        };
 257
 258        return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false
 259    }
 260
 261    private DeviceProfile GetDeviceProfile(
 262        string[] containers,
 263        string? transcodingContainer,
 264        string? audioCodec,
 265        MediaStreamProtocol? transcodingProtocol,
 266        bool? breakOnNonKeyFrames,
 267        int? transcodingAudioChannels,
 268        int? maxAudioSampleRate,
 269        int? maxAudioBitDepth,
 270        int? maxAudioChannels)
 271    {
 0272        var deviceProfile = new DeviceProfile();
 273
 0274        int len = containers.Length;
 0275        var directPlayProfiles = new DirectPlayProfile[len];
 0276        for (int i = 0; i < len; i++)
 277        {
 0278            var parts = containers[i].Split('|', StringSplitOptions.RemoveEmptyEntries);
 279
 0280            var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
 281
 0282            directPlayProfiles[i] = new DirectPlayProfile
 0283            {
 0284                Type = DlnaProfileType.Audio,
 0285                Container = parts[0],
 0286                AudioCodec = audioCodecs
 0287            };
 288        }
 289
 0290        deviceProfile.DirectPlayProfiles = directPlayProfiles;
 291
 0292        deviceProfile.TranscodingProfiles = new[]
 0293        {
 0294            new TranscodingProfile
 0295            {
 0296                Type = DlnaProfileType.Audio,
 0297                Context = EncodingContext.Streaming,
 0298                Container = transcodingContainer ?? "mp3",
 0299                AudioCodec = audioCodec ?? "mp3",
 0300                Protocol = transcodingProtocol ?? MediaStreamProtocol.http,
 0301                BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
 0302                MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
 0303            }
 0304        };
 305
 0306        var codecProfiles = new List<CodecProfile>();
 0307        var conditions = new List<ProfileCondition>();
 308
 0309        if (maxAudioSampleRate.HasValue)
 310        {
 311            // codec profile
 0312            conditions.Add(
 0313                new ProfileCondition
 0314                {
 0315                    Condition = ProfileConditionType.LessThanEqual,
 0316                    IsRequired = false,
 0317                    Property = ProfileConditionValue.AudioSampleRate,
 0318                    Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
 0319                });
 320        }
 321
 0322        if (maxAudioBitDepth.HasValue)
 323        {
 324            // codec profile
 0325            conditions.Add(
 0326                new ProfileCondition
 0327                {
 0328                    Condition = ProfileConditionType.LessThanEqual,
 0329                    IsRequired = false,
 0330                    Property = ProfileConditionValue.AudioBitDepth,
 0331                    Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
 0332                });
 333        }
 334
 0335        if (maxAudioChannels.HasValue)
 336        {
 337            // codec profile
 0338            conditions.Add(
 0339                new ProfileCondition
 0340                {
 0341                    Condition = ProfileConditionType.LessThanEqual,
 0342                    IsRequired = false,
 0343                    Property = ProfileConditionValue.AudioChannels,
 0344                    Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
 0345                });
 346        }
 347
 0348        if (conditions.Count > 0)
 349        {
 350            // codec profile
 0351            codecProfiles.Add(
 0352                new CodecProfile
 0353                {
 0354                    Type = CodecType.Audio,
 0355                    Container = string.Join(',', containers),
 0356                    Conditions = conditions.ToArray()
 0357                });
 358        }
 359
 0360        deviceProfile.CodecProfiles = codecProfiles.ToArray();
 361
 0362        return deviceProfile;
 363    }
 364}