mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-04 03:27:21 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			342 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			342 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
using System;
 | 
						|
using System.Collections.Generic;
 | 
						|
using System.ComponentModel.DataAnnotations;
 | 
						|
using System.Globalization;
 | 
						|
using System.Linq;
 | 
						|
using System.Threading.Tasks;
 | 
						|
using Jellyfin.Api.Attributes;
 | 
						|
using Jellyfin.Api.Constants;
 | 
						|
using Jellyfin.Api.Helpers;
 | 
						|
using Jellyfin.Api.ModelBinders;
 | 
						|
using Jellyfin.Api.Models.StreamingDtos;
 | 
						|
using MediaBrowser.Common.Extensions;
 | 
						|
using MediaBrowser.Controller.Devices;
 | 
						|
using MediaBrowser.Controller.Library;
 | 
						|
using MediaBrowser.Controller.MediaEncoding;
 | 
						|
using MediaBrowser.Controller.Net;
 | 
						|
using MediaBrowser.Model.Dlna;
 | 
						|
using MediaBrowser.Model.MediaInfo;
 | 
						|
using Microsoft.AspNetCore.Authorization;
 | 
						|
using Microsoft.AspNetCore.Http;
 | 
						|
using Microsoft.AspNetCore.Mvc;
 | 
						|
using Microsoft.Extensions.Logging;
 | 
						|
 | 
						|
namespace Jellyfin.Api.Controllers
 | 
						|
{
 | 
						|
    /// <summary>
 | 
						|
    /// The universal audio controller.
 | 
						|
    /// </summary>
 | 
						|
    [Route("")]
 | 
						|
    public class UniversalAudioController : BaseJellyfinApiController
 | 
						|
    {
 | 
						|
        private readonly IAuthorizationContext _authorizationContext;
 | 
						|
        private readonly IDeviceManager _deviceManager;
 | 
						|
        private readonly ILibraryManager _libraryManager;
 | 
						|
        private readonly ILogger<UniversalAudioController> _logger;
 | 
						|
        private readonly MediaInfoHelper _mediaInfoHelper;
 | 
						|
        private readonly AudioHelper _audioHelper;
 | 
						|
        private readonly DynamicHlsHelper _dynamicHlsHelper;
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
 | 
						|
        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
 | 
						|
        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 | 
						|
        /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
 | 
						|
        /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
 | 
						|
        /// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
 | 
						|
        /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
 | 
						|
        public UniversalAudioController(
 | 
						|
            IAuthorizationContext authorizationContext,
 | 
						|
            IDeviceManager deviceManager,
 | 
						|
            ILibraryManager libraryManager,
 | 
						|
            ILogger<UniversalAudioController> logger,
 | 
						|
            MediaInfoHelper mediaInfoHelper,
 | 
						|
            AudioHelper audioHelper,
 | 
						|
            DynamicHlsHelper dynamicHlsHelper)
 | 
						|
        {
 | 
						|
            _authorizationContext = authorizationContext;
 | 
						|
            _deviceManager = deviceManager;
 | 
						|
            _libraryManager = libraryManager;
 | 
						|
            _logger = logger;
 | 
						|
            _mediaInfoHelper = mediaInfoHelper;
 | 
						|
            _audioHelper = audioHelper;
 | 
						|
            _dynamicHlsHelper = dynamicHlsHelper;
 | 
						|
        }
 | 
						|
 | 
						|
        /// <summary>
 | 
						|
        /// Gets an audio stream.
 | 
						|
        /// </summary>
 | 
						|
        /// <param name="itemId">The item id.</param>
 | 
						|
        /// <param name="container">Optional. The audio container.</param>
 | 
						|
        /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
 | 
						|
        /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
 | 
						|
        /// <param name="userId">Optional. The user id.</param>
 | 
						|
        /// <param name="audioCodec">Optional. The audio codec to transcode to.</param>
 | 
						|
        /// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
 | 
						|
        /// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
 | 
						|
        /// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
 | 
						|
        /// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
 | 
						|
        /// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
 | 
						|
        /// <param name="transcodingContainer">Optional. The container to transcode to.</param>
 | 
						|
        /// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
 | 
						|
        /// <param name="maxAudioSampleRate">Optional. The maximum audio sample rate.</param>
 | 
						|
        /// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
 | 
						|
        /// <param name="enableRemoteMedia">Optional. Whether to enable remote media.</param>
 | 
						|
        /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
 | 
						|
        /// <param name="enableRedirection">Whether to enable redirection. Defaults to true.</param>
 | 
						|
        /// <response code="200">Audio stream returned.</response>
 | 
						|
        /// <response code="302">Redirected to remote audio stream.</response>
 | 
						|
        /// <returns>A <see cref="Task"/> containing the audio file.</returns>
 | 
						|
        [HttpGet("Audio/{itemId}/universal")]
 | 
						|
        [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
 | 
						|
        [Authorize(Policy = Policies.DefaultAuthorization)]
 | 
						|
        [ProducesResponseType(StatusCodes.Status200OK)]
 | 
						|
        [ProducesResponseType(StatusCodes.Status302Found)]
 | 
						|
        [ProducesAudioFile]
 | 
						|
        public async Task<ActionResult> GetUniversalAudioStream(
 | 
						|
            [FromRoute, Required] Guid itemId,
 | 
						|
            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
 | 
						|
            [FromQuery] string? mediaSourceId,
 | 
						|
            [FromQuery] string? deviceId,
 | 
						|
            [FromQuery] Guid? userId,
 | 
						|
            [FromQuery] string? audioCodec,
 | 
						|
            [FromQuery] int? maxAudioChannels,
 | 
						|
            [FromQuery] int? transcodingAudioChannels,
 | 
						|
            [FromQuery] int? maxStreamingBitrate,
 | 
						|
            [FromQuery] int? audioBitRate,
 | 
						|
            [FromQuery] long? startTimeTicks,
 | 
						|
            [FromQuery] string? transcodingContainer,
 | 
						|
            [FromQuery] string? transcodingProtocol,
 | 
						|
            [FromQuery] int? maxAudioSampleRate,
 | 
						|
            [FromQuery] int? maxAudioBitDepth,
 | 
						|
            [FromQuery] bool? enableRemoteMedia,
 | 
						|
            [FromQuery] bool breakOnNonKeyFrames,
 | 
						|
            [FromQuery] bool enableRedirection = true)
 | 
						|
        {
 | 
						|
            var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
 | 
						|
            _authorizationContext.GetAuthorizationInfo(Request).DeviceId = deviceId;
 | 
						|
 | 
						|
            var authInfo = _authorizationContext.GetAuthorizationInfo(Request);
 | 
						|
 | 
						|
            _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 | 
						|
 | 
						|
            if (deviceProfile == null)
 | 
						|
            {
 | 
						|
                var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
 | 
						|
                if (clientCapabilities != null)
 | 
						|
                {
 | 
						|
                    deviceProfile = clientCapabilities.DeviceProfile;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            var info = await _mediaInfoHelper.GetPlaybackInfo(
 | 
						|
                    itemId,
 | 
						|
                    userId,
 | 
						|
                    mediaSourceId)
 | 
						|
                .ConfigureAwait(false);
 | 
						|
 | 
						|
            if (deviceProfile != null)
 | 
						|
            {
 | 
						|
                // set device specific data
 | 
						|
                var item = _libraryManager.GetItemById(itemId);
 | 
						|
 | 
						|
                foreach (var sourceInfo in info.MediaSources)
 | 
						|
                {
 | 
						|
                    _mediaInfoHelper.SetDeviceSpecificData(
 | 
						|
                        item,
 | 
						|
                        sourceInfo,
 | 
						|
                        deviceProfile,
 | 
						|
                        authInfo,
 | 
						|
                        maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
 | 
						|
                        startTimeTicks ?? 0,
 | 
						|
                        mediaSourceId ?? string.Empty,
 | 
						|
                        null,
 | 
						|
                        null,
 | 
						|
                        maxAudioChannels,
 | 
						|
                        info!.PlaySessionId!,
 | 
						|
                        userId ?? Guid.Empty,
 | 
						|
                        true,
 | 
						|
                        true,
 | 
						|
                        true,
 | 
						|
                        true,
 | 
						|
                        true,
 | 
						|
                        Request.HttpContext.GetNormalizedRemoteIp());
 | 
						|
                }
 | 
						|
 | 
						|
                _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
 | 
						|
            }
 | 
						|
 | 
						|
            if (info.MediaSources != null)
 | 
						|
            {
 | 
						|
                foreach (var source in info.MediaSources)
 | 
						|
                {
 | 
						|
                    _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            var mediaSource = info.MediaSources![0];
 | 
						|
            if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
 | 
						|
            {
 | 
						|
                if (enableRedirection)
 | 
						|
                {
 | 
						|
                    if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
 | 
						|
                    {
 | 
						|
                        return Redirect(mediaSource.Path);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            var isStatic = mediaSource.SupportsDirectStream;
 | 
						|
            if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
 | 
						|
            {
 | 
						|
                // hls segment container can only be mpegts or fmp4 per ffmpeg documentation
 | 
						|
                // ffmpeg option -> file extension
 | 
						|
                //        mpegts -> ts
 | 
						|
                //          fmp4 -> mp4
 | 
						|
                // TODO: remove this when we switch back to the segment muxer
 | 
						|
                var supportedHlsContainers = new[] { "ts", "mp4" };
 | 
						|
 | 
						|
                var dynamicHlsRequestDto = new HlsAudioRequestDto
 | 
						|
                {
 | 
						|
                    Id = itemId,
 | 
						|
                    Container = ".m3u8",
 | 
						|
                    Static = isStatic,
 | 
						|
                    PlaySessionId = info.PlaySessionId,
 | 
						|
                    // fallback to mpegts if device reports some weird value unsupported by hls
 | 
						|
                    SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
 | 
						|
                    MediaSourceId = mediaSourceId,
 | 
						|
                    DeviceId = deviceId,
 | 
						|
                    AudioCodec = audioCodec,
 | 
						|
                    EnableAutoStreamCopy = true,
 | 
						|
                    AllowAudioStreamCopy = true,
 | 
						|
                    AllowVideoStreamCopy = true,
 | 
						|
                    BreakOnNonKeyFrames = breakOnNonKeyFrames,
 | 
						|
                    AudioSampleRate = maxAudioSampleRate,
 | 
						|
                    MaxAudioChannels = maxAudioChannels,
 | 
						|
                    MaxAudioBitDepth = maxAudioBitDepth,
 | 
						|
                    AudioBitRate = audioBitRate ?? maxStreamingBitrate,
 | 
						|
                    StartTimeTicks = startTimeTicks,
 | 
						|
                    SubtitleMethod = SubtitleDeliveryMethod.Hls,
 | 
						|
                    RequireAvc = true,
 | 
						|
                    DeInterlace = true,
 | 
						|
                    RequireNonAnamorphic = true,
 | 
						|
                    EnableMpegtsM2TsMode = true,
 | 
						|
                    TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
 | 
						|
                    Context = EncodingContext.Static,
 | 
						|
                    StreamOptions = new Dictionary<string, string>(),
 | 
						|
                    EnableAdaptiveBitrateStreaming = true
 | 
						|
                };
 | 
						|
 | 
						|
                return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType.Hls, dynamicHlsRequestDto, true)
 | 
						|
                    .ConfigureAwait(false);
 | 
						|
            }
 | 
						|
 | 
						|
            var audioStreamingDto = new StreamingRequestDto
 | 
						|
            {
 | 
						|
                Id = itemId,
 | 
						|
                Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
 | 
						|
                Static = isStatic,
 | 
						|
                PlaySessionId = info.PlaySessionId,
 | 
						|
                MediaSourceId = mediaSourceId,
 | 
						|
                DeviceId = deviceId,
 | 
						|
                AudioCodec = audioCodec,
 | 
						|
                EnableAutoStreamCopy = true,
 | 
						|
                AllowAudioStreamCopy = true,
 | 
						|
                AllowVideoStreamCopy = true,
 | 
						|
                BreakOnNonKeyFrames = breakOnNonKeyFrames,
 | 
						|
                AudioSampleRate = maxAudioSampleRate,
 | 
						|
                MaxAudioChannels = maxAudioChannels,
 | 
						|
                AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
 | 
						|
                MaxAudioBitDepth = maxAudioBitDepth,
 | 
						|
                AudioChannels = maxAudioChannels,
 | 
						|
                CopyTimestamps = true,
 | 
						|
                StartTimeTicks = startTimeTicks,
 | 
						|
                SubtitleMethod = SubtitleDeliveryMethod.Embed,
 | 
						|
                TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
 | 
						|
                Context = EncodingContext.Static
 | 
						|
            };
 | 
						|
 | 
						|
            return await _audioHelper.GetAudioStream(TranscodingJobType.Progressive, audioStreamingDto).ConfigureAwait(false);
 | 
						|
        }
 | 
						|
 | 
						|
        private DeviceProfile GetDeviceProfile(
 | 
						|
            string[] containers,
 | 
						|
            string? transcodingContainer,
 | 
						|
            string? audioCodec,
 | 
						|
            string? transcodingProtocol,
 | 
						|
            bool? breakOnNonKeyFrames,
 | 
						|
            int? transcodingAudioChannels,
 | 
						|
            int? maxAudioSampleRate,
 | 
						|
            int? maxAudioBitDepth,
 | 
						|
            int? maxAudioChannels)
 | 
						|
        {
 | 
						|
            var deviceProfile = new DeviceProfile();
 | 
						|
 | 
						|
            int len = containers.Length;
 | 
						|
            var directPlayProfiles = new DirectPlayProfile[len];
 | 
						|
            for (int i = 0; i < len; i++)
 | 
						|
            {
 | 
						|
                var parts = RequestHelpers.Split(containers[i], '|', true);
 | 
						|
 | 
						|
                var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
 | 
						|
 | 
						|
                directPlayProfiles[i] = new DirectPlayProfile
 | 
						|
                {
 | 
						|
                    Type = DlnaProfileType.Audio,
 | 
						|
                    Container = parts[0],
 | 
						|
                    AudioCodec = audioCodecs
 | 
						|
                };
 | 
						|
            }
 | 
						|
 | 
						|
            deviceProfile.DirectPlayProfiles = directPlayProfiles;
 | 
						|
 | 
						|
            deviceProfile.TranscodingProfiles = new[]
 | 
						|
            {
 | 
						|
                new TranscodingProfile
 | 
						|
                {
 | 
						|
                    Type = DlnaProfileType.Audio,
 | 
						|
                    Context = EncodingContext.Streaming,
 | 
						|
                    Container = transcodingContainer,
 | 
						|
                    AudioCodec = audioCodec,
 | 
						|
                    Protocol = transcodingProtocol,
 | 
						|
                    BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
 | 
						|
                    MaxAudioChannels = transcodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
 | 
						|
                }
 | 
						|
            };
 | 
						|
 | 
						|
            var codecProfiles = new List<CodecProfile>();
 | 
						|
            var conditions = new List<ProfileCondition>();
 | 
						|
 | 
						|
            if (maxAudioSampleRate.HasValue)
 | 
						|
            {
 | 
						|
                // codec profile
 | 
						|
                conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioSampleRate, Value = maxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) });
 | 
						|
            }
 | 
						|
 | 
						|
            if (maxAudioBitDepth.HasValue)
 | 
						|
            {
 | 
						|
                // codec profile
 | 
						|
                conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioBitDepth, Value = maxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture) });
 | 
						|
            }
 | 
						|
 | 
						|
            if (maxAudioChannels.HasValue)
 | 
						|
            {
 | 
						|
                // codec profile
 | 
						|
                conditions.Add(new ProfileCondition { Condition = ProfileConditionType.LessThanEqual, IsRequired = false, Property = ProfileConditionValue.AudioChannels, Value = maxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) });
 | 
						|
            }
 | 
						|
 | 
						|
            if (conditions.Count > 0)
 | 
						|
            {
 | 
						|
                // codec profile
 | 
						|
                codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() });
 | 
						|
            }
 | 
						|
 | 
						|
            deviceProfile.CodecProfiles = codecProfiles.ToArray();
 | 
						|
 | 
						|
            return deviceProfile;
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |