jellyfin-server/MediaBrowser.Api/Playback/UniversalAudioService.cs
dkanada ce7744806c
Merge pull request #2216 from gnattu/master
Let HLS fallback to mpegts in case device reports unsupported container
2020-01-13 03:43:59 +09:00

380 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Api.Playback.Hls;
using MediaBrowser.Api.Playback.Progressive;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Api.Playback
{
public class BaseUniversalRequest
{
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid Id { get; set; }
[ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
public string MediaSourceId { get; set; }
[ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string DeviceId { get; set; }
public Guid UserId { get; set; }
public string AudioCodec { get; set; }
public string Container { get; set; }
public int? MaxAudioChannels { get; set; }
public int? TranscodingAudioChannels { get; set; }
public long? MaxStreamingBitrate { get; set; }
[ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public long? StartTimeTicks { get; set; }
public string TranscodingContainer { get; set; }
public string TranscodingProtocol { get; set; }
public int? MaxAudioSampleRate { get; set; }
public int? MaxAudioBitDepth { get; set; }
public bool EnableRedirection { get; set; }
public bool EnableRemoteMedia { get; set; }
public bool BreakOnNonKeyFrames { get; set; }
public BaseUniversalRequest()
{
EnableRedirection = true;
}
}
[Route("/Audio/{Id}/universal.{Container}", "GET", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/universal", "GET", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/universal.{Container}", "HEAD", Summary = "Gets an audio stream")]
[Route("/Audio/{Id}/universal", "HEAD", Summary = "Gets an audio stream")]
public class GetUniversalAudioStream : BaseUniversalRequest
{
}
[Authenticated]
public class UniversalAudioService : BaseApiService
{
private readonly EncodingHelper _encodingHelper;
public UniversalAudioService(
ILogger<UniversalAudioService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IHttpClient httpClient,
IUserManager userManager,
ILibraryManager libraryManager,
IIsoManager isoManager,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
IDlnaManager dlnaManager,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager,
IJsonSerializer jsonSerializer,
IAuthorizationContext authorizationContext,
INetworkManager networkManager,
EncodingHelper encodingHelper)
: base(logger, serverConfigurationManager, httpResultFactory)
{
HttpClient = httpClient;
UserManager = userManager;
LibraryManager = libraryManager;
IsoManager = isoManager;
MediaEncoder = mediaEncoder;
FileSystem = fileSystem;
DlnaManager = dlnaManager;
DeviceManager = deviceManager;
MediaSourceManager = mediaSourceManager;
JsonSerializer = jsonSerializer;
AuthorizationContext = authorizationContext;
NetworkManager = networkManager;
_encodingHelper = encodingHelper;
}
protected IHttpClient HttpClient { get; private set; }
protected IUserManager UserManager { get; private set; }
protected ILibraryManager LibraryManager { get; private set; }
protected IIsoManager IsoManager { get; private set; }
protected IMediaEncoder MediaEncoder { get; private set; }
protected IFileSystem FileSystem { get; private set; }
protected IDlnaManager DlnaManager { get; private set; }
protected IDeviceManager DeviceManager { get; private set; }
protected IMediaSourceManager MediaSourceManager { get; private set; }
protected IJsonSerializer JsonSerializer { get; private set; }
protected IAuthorizationContext AuthorizationContext { get; private set; }
protected INetworkManager NetworkManager { get; private set; }
public Task<object> Get(GetUniversalAudioStream request)
{
return GetUniversalStream(request, false);
}
public Task<object> Head(GetUniversalAudioStream request)
{
return GetUniversalStream(request, true);
}
private DeviceProfile GetDeviceProfile(GetUniversalAudioStream request)
{
var deviceProfile = new DeviceProfile();
var directPlayProfiles = new List<DirectPlayProfile>();
var containers = (request.Container ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var container in containers)
{
var parts = container.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
directPlayProfiles.Add(new DirectPlayProfile
{
Type = DlnaProfileType.Audio,
Container = parts[0],
AudioCodec = audioCodecs
});
}
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
deviceProfile.TranscodingProfiles = new[]
{
new TranscodingProfile
{
Type = DlnaProfileType.Audio,
Context = EncodingContext.Streaming,
Container = request.TranscodingContainer,
AudioCodec = request.AudioCodec,
Protocol = request.TranscodingProtocol,
BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
MaxAudioChannels = request.TranscodingAudioChannels.HasValue ? request.TranscodingAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : null
}
};
var codecProfiles = new List<CodecProfile>();
var conditions = new List<ProfileCondition>();
if (request.MaxAudioSampleRate.HasValue)
{
// codec profile
conditions.Add(new ProfileCondition
{
Condition = ProfileConditionType.LessThanEqual,
IsRequired = false,
Property = ProfileConditionValue.AudioSampleRate,
Value = request.MaxAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)
});
}
if (request.MaxAudioBitDepth.HasValue)
{
// codec profile
conditions.Add(new ProfileCondition
{
Condition = ProfileConditionType.LessThanEqual,
IsRequired = false,
Property = ProfileConditionValue.AudioBitDepth,
Value = request.MaxAudioBitDepth.Value.ToString(CultureInfo.InvariantCulture)
});
}
if (request.MaxAudioChannels.HasValue)
{
// codec profile
conditions.Add(new ProfileCondition
{
Condition = ProfileConditionType.LessThanEqual,
IsRequired = false,
Property = ProfileConditionValue.AudioChannels,
Value = request.MaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture)
});
}
if (conditions.Count > 0)
{
// codec profile
codecProfiles.Add(new CodecProfile
{
Type = CodecType.Audio,
Container = request.Container,
Conditions = conditions.ToArray()
});
}
deviceProfile.CodecProfiles = codecProfiles.ToArray();
return deviceProfile;
}
private async Task<object> GetUniversalStream(GetUniversalAudioStream request, bool isHeadRequest)
{
var deviceProfile = GetDeviceProfile(request);
AuthorizationContext.GetAuthorizationInfo(Request).DeviceId = request.DeviceId;
var mediaInfoService = new MediaInfoService(
Logger,
ServerConfigurationManager,
ResultFactory,
MediaSourceManager,
DeviceManager,
LibraryManager,
NetworkManager,
MediaEncoder,
UserManager,
AuthorizationContext)
{
Request = Request
};
var playbackInfoResult = await mediaInfoService.GetPlaybackInfo(new GetPostedPlaybackInfo
{
Id = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MaxStreamingBitrate = request.MaxStreamingBitrate,
StartTimeTicks = request.StartTimeTicks,
UserId = request.UserId,
DeviceProfile = deviceProfile,
MediaSourceId = request.MediaSourceId
}).ConfigureAwait(false);
var mediaSource = playbackInfoResult.MediaSources[0];
if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
{
if (request.EnableRedirection)
{
if (mediaSource.IsRemote && request.EnableRemoteMedia)
{
return ResultFactory.GetRedirectResult(mediaSource.Path);
}
}
}
var isStatic = mediaSource.SupportsDirectStream;
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{
var service = new DynamicHlsService(
Logger,
ServerConfigurationManager,
ResultFactory,
UserManager,
LibraryManager,
IsoManager,
MediaEncoder,
FileSystem,
DlnaManager,
DeviceManager,
MediaSourceManager,
JsonSerializer,
AuthorizationContext,
NetworkManager,
_encodingHelper)
{
Request = Request
};
var transcodingProfile = deviceProfile.TranscodingProfiles[0];
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
// TODO: remove this when we switch back to the segment muxer
var supportedHLSContainers = new string[] { "mpegts", "fmp4" };
var newRequest = new GetMasterHlsAudioPlaylist
{
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
AudioCodec = transcodingProfile.AudioCodec,
Container = ".m3u8",
DeviceId = request.DeviceId,
Id = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MediaSourceId = mediaSource.Id,
PlaySessionId = playbackInfoResult.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
Static = isStatic,
// fallback to mpegts if device reports some weird value unsupported by hls
SegmentContainer = Array.Exists(supportedHLSContainers, element => element == request.TranscodingContainer) ? request.TranscodingContainer : "mpegts",
AudioSampleRate = request.MaxAudioSampleRate,
MaxAudioBitDepth = request.MaxAudioBitDepth,
BreakOnNonKeyFrames = transcodingProfile.BreakOnNonKeyFrames,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
};
if (isHeadRequest)
{
return await service.Head(newRequest).ConfigureAwait(false);
}
return await service.Get(newRequest).ConfigureAwait(false);
}
else
{
var service = new AudioService(
Logger,
ServerConfigurationManager,
ResultFactory,
HttpClient,
UserManager,
LibraryManager,
IsoManager,
MediaEncoder,
FileSystem,
DlnaManager,
DeviceManager,
MediaSourceManager,
JsonSerializer,
AuthorizationContext,
_encodingHelper)
{
Request = Request
};
var newRequest = new GetAudioStream
{
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(request.MaxStreamingBitrate ?? 192000, int.MaxValue)),
AudioCodec = request.AudioCodec,
Container = isStatic ? null : ("." + mediaSource.TranscodingContainer),
DeviceId = request.DeviceId,
Id = request.Id,
MaxAudioChannels = request.MaxAudioChannels,
MediaSourceId = mediaSource.Id,
PlaySessionId = playbackInfoResult.PlaySessionId,
StartTimeTicks = request.StartTimeTicks,
Static = isStatic,
AudioSampleRate = request.MaxAudioSampleRate,
MaxAudioBitDepth = request.MaxAudioBitDepth,
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray())
};
if (isHeadRequest)
{
return await service.Head(newRequest).ConfigureAwait(false);
}
return await service.Get(newRequest).ConfigureAwait(false);
}
}
}
}