diff --git a/MediaBrowser.Api/Dlna/DlnaServerService.cs b/MediaBrowser.Api/Dlna/DlnaServerService.cs index 097552ab4..0dd7ee7d1 100644 --- a/MediaBrowser.Api/Dlna/DlnaServerService.cs +++ b/MediaBrowser.Api/Dlna/DlnaServerService.cs @@ -1,5 +1,7 @@ -using MediaBrowser.Controller.Dlna; +using System; +using MediaBrowser.Controller.Dlna; using ServiceStack; +using ServiceStack.Text.Controller; using ServiceStack.Web; using System.Collections.Generic; using System.IO; @@ -21,9 +23,12 @@ namespace MediaBrowser.Api.Dlna { } - [Route("/Dlna/control", "POST", Summary = "Processes a control request")] + [Route("/Dlna/{UuId}/control", "POST", Summary = "Processes a control request")] public class ProcessControlRequest : IRequiresRequestStream { + [ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")] + public string UuId { get; set; } + public Stream RequestStream { get; set; } } @@ -66,12 +71,16 @@ namespace MediaBrowser.Api.Dlna private async Task PostAsync(ProcessControlRequest request) { + var pathInfo = PathInfo.Parse(Request.PathInfo); + var id = pathInfo.GetArgumentValue(1); + using (var reader = new StreamReader(request.RequestStream)) { return _dlnaManager.ProcessControlRequest(new ControlRequest { Headers = GetRequestHeaders(), - InputXml = await reader.ReadToEndAsync().ConfigureAwait(false) + InputXml = await reader.ReadToEndAsync().ConfigureAwait(false), + TargetServerUuId = id }); } } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 15cac7e3b..04deea3cf 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -300,7 +300,7 @@ namespace MediaBrowser.Api.Playback case EncodingQuality.HighSpeed: return 2; case EncodingQuality.HighQuality: - return isWebm ? Math.Max((int)((Environment.ProcessorCount -1) / 2) , 2) : 0; + return 2; case EncodingQuality.MaxQuality: return isWebm ? Math.Max(Environment.ProcessorCount - 1, 2) : 0; default: @@ -373,7 +373,6 @@ namespace MediaBrowser.Api.Playback break; case EncodingQuality.MaxQuality: crf = "4"; - //profilescore aready set to 0 break; default: throw new ArgumentException("Unrecognized quality setting"); @@ -381,7 +380,9 @@ namespace MediaBrowser.Api.Playback if (isVc1) { - profileScore = 1; + profileScore ++; + // Max of 2 + profileScore = Math.Min(profileScore, 2); } // http://www.webmproject.org/docs/encoder-parameters/ @@ -1713,33 +1714,19 @@ namespace MediaBrowser.Api.Playback var extension = GetOutputFileExtension(state); // first bit means Time based seek supported, second byte range seek supported (not sure about the order now), so 01 = only byte seek, 10 = time based, 11 = both, 00 = none - var orgOp = ";DLNA.ORG_OP="; + var orgOp = ";DLNA.ORG_OP=" + DlnaMaps.GetOrgOpValue(state.RunTimeTicks.HasValue, isStaticallyStreamed, state.TranscodeSeekInfo); - if (state.RunTimeTicks.HasValue) + if (state.RunTimeTicks.HasValue && !isStaticallyStreamed) { - // Time-based seeking currently only possible when transcoding - orgOp += isStaticallyStreamed ? "0" : "1"; - - // Byte-based seeking only possible when not transcoding - orgOp += isStaticallyStreamed || state.TranscodeSeekInfo == TranscodeSeekInfo.Bytes ? "1" : "0"; - - if (!isStaticallyStreamed) - { - AddTimeSeekResponseHeaders(state, responseHeaders); - } - } - else - { - // No seeking is available if we don't know the content runtime - orgOp += "00"; + AddTimeSeekResponseHeaders(state, responseHeaders); } // 0 = native, 1 = transcoded var orgCi = isStaticallyStreamed ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; - var flagValue = DlnaFlags.DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE | - DlnaFlags.DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | - DlnaFlags.DLNA_ORG_FLAG_DLNA_V15; + var flagValue = DlnaFlags.StreamingTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.DlnaV15; if (isStaticallyStreamed) { @@ -1801,23 +1788,6 @@ namespace MediaBrowser.Api.Playback } } - [Flags] - private enum DlnaFlags - { - DLNA_ORG_FLAG_SENDER_PACED = (1 << 31), - DLNA_ORG_FLAG_TIME_BASED_SEEK = (1 << 30), - DLNA_ORG_FLAG_BYTE_BASED_SEEK = (1 << 29), - DLNA_ORG_FLAG_PLAY_CONTAINER = (1 << 28), - DLNA_ORG_FLAG_S0_INCREASE = (1 << 27), - DLNA_ORG_FLAG_SN_INCREASE = (1 << 26), - DLNA_ORG_FLAG_RTSP_PAUSE = (1 << 25), - DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE = (1 << 24), - DLNA_ORG_FLAG_INTERACTIVE_TRANSFERT_MODE = (1 << 23), - DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE = (1 << 22), - DLNA_ORG_FLAG_CONNECTION_STALL = (1 << 21), - DLNA_ORG_FLAG_DLNA_V15 = (1 << 20), - }; - private void AddTimeSeekResponseHeaders(StreamState state, IDictionary responseHeaders) { var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture); diff --git a/MediaBrowser.Controller/Dlna/ControlRequest.cs b/MediaBrowser.Controller/Dlna/ControlRequest.cs index 74e68b7d0..a2b9f7a92 100644 --- a/MediaBrowser.Controller/Dlna/ControlRequest.cs +++ b/MediaBrowser.Controller/Dlna/ControlRequest.cs @@ -8,6 +8,8 @@ namespace MediaBrowser.Controller.Dlna public string InputXml { get; set; } + public string TargetServerUuId { get; set; } + public ControlRequest() { Headers = new Dictionary(); @@ -20,6 +22,8 @@ namespace MediaBrowser.Controller.Dlna public string Xml { get; set; } + public bool IsSuccessful { get; set; } + public ControlResponse() { Headers = new Dictionary(); diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 0ca1ffde0..ef12d8816 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -12,6 +12,10 @@ namespace MediaBrowser.Controller.Entities.Audio /// public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo { + public string FormatName { get; set; } + public long? Size { get; set; } + public string Container { get; set; } + public Audio() { Artists = new List(); diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 1a02eb056..3ec9e7650 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -26,6 +26,10 @@ namespace MediaBrowser.Controller.Entities public List AdditionalPartIds { get; set; } public List LocalAlternateVersionIds { get; set; } + public string FormatName { get; set; } + public long? Size { get; set; } + public string Container { get; set; } + public Video() { PlayableStreamFileNames = new List(); diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/MediaBrowser.Dlna/DlnaManager.cs index 83e3df798..9993b0e21 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/MediaBrowser.Dlna/DlnaManager.cs @@ -3,6 +3,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Dlna.Profiles; using MediaBrowser.Dlna.Server; @@ -27,8 +28,11 @@ namespace MediaBrowser.Dlna private readonly IJsonSerializer _jsonSerializer; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; + private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; - public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager) + public DlnaManager(IXmlSerializer xmlSerializer, IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, IJsonSerializer jsonSerializer, IUserManager userManager, ILibraryManager libraryManager, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; @@ -37,6 +41,9 @@ namespace MediaBrowser.Dlna _jsonSerializer = jsonSerializer; _userManager = userManager; _libraryManager = libraryManager; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + _userDataManager = userDataManager; //DumpProfiles(); } @@ -502,7 +509,14 @@ namespace MediaBrowser.Dlna public ControlResponse ProcessControlRequest(ControlRequest request) { - return new ControlHandler(_logger, _userManager, _libraryManager) + var profile = GetProfile(request.Headers) + ?? GetDefaultProfile(); + + var device = DlnaServerEntryPoint.Instance.GetServerUpnpDevice(request.TargetServerUuId); + + var serverAddress = device.Descriptor.ToString().Substring(0, device.Descriptor.ToString().IndexOf("/dlna", StringComparison.OrdinalIgnoreCase)); + + return new ControlHandler(_logger, _userManager, _libraryManager, profile, serverAddress, _dtoService, _imageProcessor, _userDataManager) .ProcessControlRequest(request); } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index bf38f19dc..016cac4d6 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -81,7 +81,6 @@ - diff --git a/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs b/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs index 853064c60..0304008fc 100644 --- a/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs +++ b/MediaBrowser.Dlna/PlayTo/DidlBuilder.cs @@ -39,6 +39,7 @@ namespace MediaBrowser.Dlna.PlayTo /// The server address. /// The stream URL. /// The streams. + /// if set to true [include image resource]. /// System.String. public static string Build(BaseItem dto, string userId, string serverAddress, string streamUrl, IEnumerable streams, bool includeImageRes) { diff --git a/MediaBrowser.Dlna/PlayTo/DlnaController.cs b/MediaBrowser.Dlna/PlayTo/DlnaController.cs index 75667d54e..183a20447 100644 --- a/MediaBrowser.Dlna/PlayTo/DlnaController.cs +++ b/MediaBrowser.Dlna/PlayTo/DlnaController.cs @@ -1,12 +1,14 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; @@ -32,6 +34,7 @@ namespace MediaBrowser.Dlna.PlayTo private readonly IDlnaManager _dlnaManager; private readonly IUserManager _userManager; private readonly IServerApplicationHost _appHost; + private readonly IDtoService _dtoService; private bool _playbackStarted; private const int UpdateTimerIntervalMs = 1000; @@ -52,7 +55,7 @@ namespace MediaBrowser.Dlna.PlayTo } } - public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, INetworkManager networkManager, IDlnaManager dlnaManager, IUserManager userManager, IServerApplicationHost appHost) + public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, INetworkManager networkManager, IDlnaManager dlnaManager, IUserManager userManager, IServerApplicationHost appHost, IDtoService dtoService) { _session = session; _itemRepository = itemRepository; @@ -62,6 +65,7 @@ namespace MediaBrowser.Dlna.PlayTo _dlnaManager = dlnaManager; _userManager = userManager; _appHost = appHost; + _dtoService = dtoService; _logger = logger; } @@ -172,20 +176,23 @@ namespace MediaBrowser.Dlna.PlayTo if (playlistItem != null) { + var streamInfo = playlistItem.StreamInfo; + if (!_playbackStarted) { await _sessionManager.OnPlaybackStart(new PlaybackStartInfo { ItemId = _currentItem.Id.ToString("N"), SessionId = _session.Id, - CanSeek = true, + CanSeek = streamInfo.RunTimeTicks.HasValue, QueueableMediaTypes = new List { _currentItem.MediaType }, - MediaSourceId = playlistItem.MediaSourceId, - AudioStreamIndex = playlistItem.AudioStreamIndex, - SubtitleStreamIndex = playlistItem.SubtitleStreamIndex, + MediaSourceId = streamInfo.MediaSourceId, + AudioStreamIndex = streamInfo.AudioStreamIndex, + SubtitleStreamIndex = streamInfo.SubtitleStreamIndex, IsMuted = _device.IsMuted, IsPaused = _device.IsPaused, - VolumeLevel = _device.Volume + VolumeLevel = _device.Volume, + PlayMethod = streamInfo.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode }).ConfigureAwait(false); @@ -196,9 +203,9 @@ namespace MediaBrowser.Dlna.PlayTo { var ticks = _device.Position.Ticks; - if (playlistItem.Transcode) + if (!streamInfo.IsDirectStream) { - ticks += playlistItem.StartPositionTicks; + ticks += streamInfo.StartPositionTicks; } await _sessionManager.OnPlaybackProgress(new PlaybackProgressInfo @@ -208,11 +215,12 @@ namespace MediaBrowser.Dlna.PlayTo PositionTicks = ticks, IsMuted = _device.IsMuted, IsPaused = _device.IsPaused, - MediaSourceId = playlistItem.MediaSourceId, - AudioStreamIndex = playlistItem.AudioStreamIndex, - SubtitleStreamIndex = playlistItem.SubtitleStreamIndex, + MediaSourceId = streamInfo.MediaSourceId, + AudioStreamIndex = streamInfo.AudioStreamIndex, + SubtitleStreamIndex = streamInfo.SubtitleStreamIndex, VolumeLevel = _device.Volume, - CanSeek = true + CanSeek = streamInfo.RunTimeTicks.HasValue, + PlayMethod = streamInfo.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode }).ConfigureAwait(false); } @@ -411,46 +419,43 @@ namespace MediaBrowser.Dlna.PlayTo private PlaylistItem CreatePlaylistItem(BaseItem item, long startPostionTicks, string serverAddress) { - var streams = _itemRepository.GetMediaStreams(new MediaStreamQuery - { - ItemId = item.Id - - }).ToList(); - var deviceInfo = _device.Properties; var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ?? _dlnaManager.GetDefaultProfile(); - var playlistItem = GetPlaylistItem(item, streams, profile); - playlistItem.StartPositionTicks = startPostionTicks; - playlistItem.DeviceProfileId = profile.Id; + var mediaSources = item is Audio || item is Video + ? _dtoService.GetMediaSources(item) + : new List(); - if (playlistItem.MediaType == DlnaProfileType.Audio) - { - playlistItem.StreamUrl = StreamHelper.GetAudioUrl(deviceInfo, playlistItem, streams, serverAddress); - } - else - { - playlistItem.StreamUrl = StreamHelper.GetVideoUrl(_device.Properties, playlistItem, streams, serverAddress); - } + var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId); + playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; - playlistItem.Didl = DidlBuilder.Build(item, _session.UserId.ToString(), serverAddress, playlistItem.StreamUrl, streams, profile.EnableAlbumArtInDidl); + playlistItem.StreamUrl = playlistItem.StreamInfo.ToUrl(serverAddress); + + var mediaStreams = mediaSources + .Where(i => string.Equals(i.Id, playlistItem.StreamInfo.MediaSourceId)) + .SelectMany(i => i.MediaStreams) + .ToList(); + + playlistItem.Didl = DidlBuilder.Build(item, _session.UserId.ToString(), serverAddress, playlistItem.StreamUrl, mediaStreams, profile.EnableAlbumArtInDidl); return playlistItem; } private string GetDlnaHeaders(PlaylistItem item) { - var orgOp = item.Transcode ? ";DLNA.ORG_OP=00" : ";DLNA.ORG_OP=01"; + var streamInfo = item.StreamInfo; - var orgCi = item.Transcode ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + var orgOp = !streamInfo.IsDirectStream ? ";DLNA.ORG_OP=00" : ";DLNA.ORG_OP=01"; + + var orgCi = !streamInfo.IsDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; const string dlnaflags = ";DLNA.ORG_FLAGS=01500000000000000000000000000000"; string contentFeatures; - var container = item.Container.TrimStart('.'); + var container = streamInfo.Container.TrimStart('.'); if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase)) { @@ -488,7 +493,7 @@ namespace MediaBrowser.Dlna.PlayTo { contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL"; } - else if (item.MediaType == DlnaProfileType.Video) + else if (streamInfo.MediaType == DlnaProfileType.Video) { // Default to AVI for video contentFeatures = "DLNA.ORG_PN=AVI"; @@ -502,20 +507,38 @@ namespace MediaBrowser.Dlna.PlayTo return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';'); } - private PlaylistItem GetPlaylistItem(BaseItem item, List mediaStreams, DeviceProfile profile) + private PlaylistItem GetPlaylistItem(BaseItem item, List mediaSources, DeviceProfile profile, string deviceId) { var video = item as Video; if (video != null) { - return new PlaylistItemFactory().Create(video, mediaStreams, profile); + return new PlaylistItem + { + StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions + { + ItemId = item.Id.ToString("N"), + MediaSources = mediaSources, + Profile = profile, + DeviceId = deviceId + }) + }; } var audio = item as Audio; if (audio != null) { - return new PlaylistItemFactory().Create(audio, mediaStreams, profile); + return new PlaylistItem + { + StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions + { + ItemId = item.Id.ToString("N"), + MediaSources = mediaSources, + Profile = profile, + DeviceId = deviceId + }) + }; } var photo = item as Photo; @@ -578,8 +601,9 @@ namespace MediaBrowser.Dlna.PlayTo await _device.SetAvTransport(nextTrack.StreamUrl, dlnaheaders, nextTrack.Didl); - if (nextTrack.StartPositionTicks > 0 && !nextTrack.Transcode) - await _device.Seek(TimeSpan.FromTicks(nextTrack.StartPositionTicks)); + var streamInfo = nextTrack.StreamInfo; + if (streamInfo.StartPositionTicks > 0 && streamInfo.IsDirectStream) + await _device.Seek(TimeSpan.FromTicks(streamInfo.StartPositionTicks)); return true; } @@ -602,7 +626,7 @@ namespace MediaBrowser.Dlna.PlayTo return Task.FromResult(false); prevTrack.PlayState = 1; - return _device.SetAvTransport(prevTrack.StreamUrl, GetDlnaHeaders(prevTrack), prevTrack.Didl); + return _device.SetAvTransport(prevTrack.StreamInfo.ToDlnaUrl(GetServerAddress()), GetDlnaHeaders(prevTrack), prevTrack.Didl); } #endregion diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs index 5cc3f13e5..655a11693 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -2,11 +2,11 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Session; using MediaBrowser.Dlna.Ssdp; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; using System; @@ -37,8 +37,9 @@ namespace MediaBrowser.Dlna.PlayTo private readonly IDlnaManager _dlnaManager; private readonly IServerConfigurationManager _config; private readonly IServerApplicationHost _appHost; + private readonly IDtoService _dtoService; - public PlayToManager(ILogger logger, IServerConfigurationManager config, ISessionManager sessionManager, IHttpClient httpClient, IItemRepository itemRepository, ILibraryManager libraryManager, INetworkManager networkManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost) + public PlayToManager(ILogger logger, IServerConfigurationManager config, ISessionManager sessionManager, IHttpClient httpClient, IItemRepository itemRepository, ILibraryManager libraryManager, INetworkManager networkManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IDtoService dtoService) { _locations = new ConcurrentDictionary(); _tokenSource = new CancellationTokenSource(); @@ -52,6 +53,7 @@ namespace MediaBrowser.Dlna.PlayTo _userManager = userManager; _dlnaManager = dlnaManager; _appHost = appHost; + _dtoService = dtoService; _config = config; } @@ -248,7 +250,7 @@ namespace MediaBrowser.Dlna.PlayTo if (controller == null) { - sessionInfo.SessionController = controller = new PlayToController(sessionInfo, _sessionManager, _itemRepository, _libraryManager, _logger, _networkManager, _dlnaManager, _userManager, _appHost); + sessionInfo.SessionController = controller = new PlayToController(sessionInfo, _sessionManager, _itemRepository, _libraryManager, _logger, _networkManager, _dlnaManager, _userManager, _appHost, _dtoService); controller.Init(device); diff --git a/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs b/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs index 91b03bc23..c0af1295f 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToServerEntryPoint.cs @@ -2,6 +2,7 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; @@ -24,8 +25,9 @@ namespace MediaBrowser.Dlna.PlayTo private readonly IUserManager _userManager; private readonly IDlnaManager _dlnaManager; private readonly IServerApplicationHost _appHost; + private readonly IDtoService _dtoService; - public PlayToServerEntryPoint(ILogManager logManager, IServerConfigurationManager config, ISessionManager sessionManager, IHttpClient httpClient, IItemRepository itemRepo, ILibraryManager libraryManager, INetworkManager networkManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost) + public PlayToServerEntryPoint(ILogManager logManager, IServerConfigurationManager config, ISessionManager sessionManager, IHttpClient httpClient, IItemRepository itemRepo, ILibraryManager libraryManager, INetworkManager networkManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IDtoService dtoService) { _config = config; _sessionManager = sessionManager; @@ -36,6 +38,7 @@ namespace MediaBrowser.Dlna.PlayTo _userManager = userManager; _dlnaManager = dlnaManager; _appHost = appHost; + _dtoService = dtoService; _logger = logManager.GetLogger("PlayTo"); } @@ -72,7 +75,18 @@ namespace MediaBrowser.Dlna.PlayTo { try { - _manager = new PlayToManager(_logger, _config, _sessionManager, _httpClient, _itemRepo, _libraryManager, _networkManager, _userManager, _dlnaManager, _appHost); + _manager = new PlayToManager(_logger, + _config, + _sessionManager, + _httpClient, + _itemRepo, + _libraryManager, + _networkManager, + _userManager, + _dlnaManager, + _appHost, + _dtoService); + _manager.Start(); } catch (Exception ex) diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs index be6e9a0c9..95167cfe9 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs +++ b/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs @@ -4,45 +4,12 @@ namespace MediaBrowser.Dlna.PlayTo { public class PlaylistItem { - public string ItemId { get; set; } - - public string MediaSourceId { get; set; } - - public bool Transcode { get; set; } - - public DlnaProfileType MediaType { get; set; } - - public string Container { get; set; } - public int PlayState { get; set; } public string StreamUrl { get; set; } public string Didl { get; set; } - public long StartPositionTicks { get; set; } - - public string VideoCodec { get; set; } - - public string AudioCodec { get; set; } - - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public int? MaxAudioChannels { get; set; } - - public int? AudioBitrate { get; set; } - - public int? VideoBitrate { get; set; } - - public int? VideoLevel { get; set; } - - public int? MaxWidth { get; set; } - public int? MaxHeight { get; set; } - - public int? MaxFramerate { get; set; } - - public string DeviceProfileId { get; set; } + public StreamInfo StreamInfo { get; set; } } } \ No newline at end of file diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs index 34394fa32..29d44bca0 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs @@ -1,10 +1,6 @@ -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Entities; using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -15,235 +11,40 @@ namespace MediaBrowser.Dlna.PlayTo { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public PlaylistItem Create(Audio item, List mediaStreams, DeviceProfile profile) - { - var playlistItem = new PlaylistItem - { - ItemId = item.Id.ToString("N"), - MediaType = DlnaProfileType.Audio - }; - - var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - - var directPlay = profile.DirectPlayProfiles - .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsSupported(i, item, audioStream)); - - if (directPlay != null) - { - var audioCodec = audioStream == null ? null : audioStream.Codec; - - // Make sure audio codec profiles are satisfied - if (!string.IsNullOrEmpty(audioCodec) && profile.CodecProfiles.Where(i => i.Type == CodecType.Audio && i.ContainsCodec(audioCodec)) - .All(i => AreConditionsSatisfied(i.Conditions, item.Path, null, audioStream))) - { - playlistItem.Transcode = false; - playlistItem.Container = Path.GetExtension(item.Path); - - return playlistItem; - } - } - - var transcodingProfile = profile.TranscodingProfiles - .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsSupported(profile, i, item)); - - if (transcodingProfile != null) - { - playlistItem.Transcode = true; - playlistItem.Container = "." + transcodingProfile.Container.TrimStart('.'); - playlistItem.AudioCodec = transcodingProfile.AudioCodec; - - var audioTranscodingConditions = profile.CodecProfiles - .Where(i => i.Type == CodecType.Audio && i.ContainsCodec(transcodingProfile.AudioCodec)) - .Take(1) - .SelectMany(i => i.Conditions); - - ApplyTranscodingConditions(playlistItem, audioTranscodingConditions); - } - - return playlistItem; - } - public PlaylistItem Create(Photo item, DeviceProfile profile) { var playlistItem = new PlaylistItem { - ItemId = item.Id.ToString("N"), - MediaType = DlnaProfileType.Photo + StreamInfo = new StreamInfo + { + ItemId = item.Id.ToString("N"), + MediaType = DlnaProfileType.Photo, + } }; var directPlay = profile.DirectPlayProfiles - .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsSupported(i, item)); + .FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item)); if (directPlay != null) { - playlistItem.Transcode = false; - playlistItem.Container = Path.GetExtension(item.Path); + playlistItem.StreamInfo.IsDirectStream = true; + playlistItem.StreamInfo.Container = Path.GetExtension(item.Path); return playlistItem; } var transcodingProfile = profile.TranscodingProfiles - .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsSupported(profile, i, item)); + .FirstOrDefault(i => i.Type == DlnaProfileType.Photo); if (transcodingProfile != null) { - playlistItem.Transcode = true; - playlistItem.Container = "." + transcodingProfile.Container.TrimStart('.'); + playlistItem.StreamInfo.IsDirectStream = true; + playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.'); } return playlistItem; } - public PlaylistItem Create(Video item, List mediaStreams, DeviceProfile profile) - { - var playlistItem = new PlaylistItem - { - ItemId = item.Id.ToString("N"), - MediaType = DlnaProfileType.Video - }; - - var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); - - var directPlay = profile.DirectPlayProfiles - .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsSupported(i, item, videoStream, audioStream)); - - if (directPlay != null) - { - var videoCodec = videoStream == null ? null : videoStream.Codec; - - // Make sure video codec profiles are satisfied - if (!string.IsNullOrEmpty(videoCodec) && profile.CodecProfiles.Where(i => i.Type == CodecType.Video && i.ContainsCodec(videoCodec)) - .All(i => AreConditionsSatisfied(i.Conditions, item.Path, videoStream, audioStream))) - { - var audioCodec = audioStream == null ? null : audioStream.Codec; - - // Make sure audio codec profiles are satisfied - if (string.IsNullOrEmpty(audioCodec) || profile.CodecProfiles.Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(audioCodec)) - .All(i => AreConditionsSatisfied(i.Conditions, item.Path, videoStream, audioStream))) - { - playlistItem.Transcode = false; - playlistItem.Container = Path.GetExtension(item.Path); - - return playlistItem; - } - } - } - - var transcodingProfile = profile.TranscodingProfiles - .FirstOrDefault(i => i.Type == playlistItem.MediaType && IsSupported(profile, i, item)); - - if (transcodingProfile != null) - { - playlistItem.Transcode = true; - playlistItem.Container = "." + transcodingProfile.Container.TrimStart('.'); - playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',').FirstOrDefault(); - playlistItem.VideoCodec = transcodingProfile.VideoCodec; - - var videoTranscodingConditions = profile.CodecProfiles - .Where(i => i.Type == CodecType.Video && i.ContainsCodec(transcodingProfile.VideoCodec)) - .Take(1) - .SelectMany(i => i.Conditions); - - ApplyTranscodingConditions(playlistItem, videoTranscodingConditions); - - var audioTranscodingConditions = profile.CodecProfiles - .Where(i => i.Type == CodecType.VideoAudio && i.ContainsCodec(transcodingProfile.AudioCodec)) - .Take(1) - .SelectMany(i => i.Conditions); - - ApplyTranscodingConditions(playlistItem, audioTranscodingConditions); - } - - return playlistItem; - } - - private void ApplyTranscodingConditions(PlaylistItem item, IEnumerable conditions) - { - foreach (var condition in conditions - .Where(i => !string.IsNullOrEmpty(i.Value))) - { - var value = condition.Value; - - switch (condition.Property) - { - case ProfileConditionValue.AudioBitrate: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.AudioBitrate = num; - } - break; - } - case ProfileConditionValue.AudioChannels: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.MaxAudioChannels = num; - } - break; - } - case ProfileConditionValue.AudioProfile: - case ProfileConditionValue.Has64BitOffsets: - case ProfileConditionValue.VideoBitDepth: - case ProfileConditionValue.VideoProfile: - { - // Not supported yet - break; - } - case ProfileConditionValue.Height: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.MaxHeight = num; - } - break; - } - case ProfileConditionValue.VideoBitrate: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.VideoBitrate = num; - } - break; - } - case ProfileConditionValue.VideoFramerate: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.MaxFramerate = num; - } - break; - } - case ProfileConditionValue.VideoLevel: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.VideoLevel = num; - } - break; - } - case ProfileConditionValue.Width: - { - int num; - if (int.TryParse(value, NumberStyles.Any, _usCulture, out num)) - { - item.MaxWidth = num; - } - break; - } - default: - throw new ArgumentException("Unrecognized ProfileConditionValue"); - } - } - } - private bool IsSupported(DirectPlayProfile profile, Photo item) { var mediaPath = item.Path; @@ -260,223 +61,5 @@ namespace MediaBrowser.Dlna.PlayTo return true; } - - private bool IsSupported(DirectPlayProfile profile, Audio item, MediaStream audioStream) - { - var mediaPath = item.Path; - - if (profile.Container.Length > 0) - { - // Check container type - var mediaContainer = Path.GetExtension(mediaPath); - if (!profile.GetContainers().Any(i => string.Equals("." + i.TrimStart('.'), mediaContainer, StringComparison.OrdinalIgnoreCase))) - { - return false; - } - } - - return true; - } - - private bool IsSupported(DirectPlayProfile profile, Video item, MediaStream videoStream, MediaStream audioStream) - { - if (item.VideoType != VideoType.VideoFile) - { - return false; - } - - var mediaPath = item.Path; - - if (profile.Container.Length > 0) - { - // Check container type - var mediaContainer = Path.GetExtension(mediaPath); - if (!profile.GetContainers().Any(i => string.Equals("." + i.TrimStart('.'), mediaContainer, StringComparison.OrdinalIgnoreCase))) - { - return false; - } - } - - // Check video codec - var videoCodecs = profile.GetVideoCodecs(); - if (videoCodecs.Count > 0) - { - var videoCodec = videoStream == null ? null : videoStream.Codec; - if (string.IsNullOrWhiteSpace(videoCodec) || !videoCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - } - - var audioCodecs = profile.GetAudioCodecs(); - if (audioCodecs.Count > 0) - { - // Check audio codecs - var audioCodec = audioStream == null ? null : audioStream.Codec; - if (string.IsNullOrWhiteSpace(audioCodec) || !audioCodecs.Contains(audioCodec, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - } - - return true; - } - - private bool IsSupported(DeviceProfile profile, TranscodingProfile transcodingProfile, Audio item) - { - // Placeholder for future conditions - return true; - } - - private bool IsSupported(DeviceProfile profile, TranscodingProfile transcodingProfile, Photo item) - { - // Placeholder for future conditions - return true; - } - - private bool IsSupported(DeviceProfile profile, TranscodingProfile transcodingProfile, Video item) - { - // Placeholder for future conditions - return true; - } - - private bool AreConditionsSatisfied(IEnumerable conditions, string mediaPath, MediaStream videoStream, MediaStream audioStream) - { - return conditions.All(i => IsConditionSatisfied(i, mediaPath, videoStream, audioStream)); - } - - /// - /// Determines whether [is condition satisfied] [the specified condition]. - /// - /// The condition. - /// The media path. - /// The video stream. - /// The audio stream. - /// true if [is condition satisfied] [the specified condition]; otherwise, false. - /// Unexpected ProfileConditionType - private bool IsConditionSatisfied(ProfileCondition condition, string mediaPath, MediaStream videoStream, MediaStream audioStream) - { - if (condition.Property == ProfileConditionValue.Has64BitOffsets) - { - // TODO: Determine how to evaluate this - } - - if (condition.Property == ProfileConditionValue.VideoProfile) - { - var profile = videoStream == null ? null : videoStream.Profile; - - if (!string.IsNullOrWhiteSpace(profile)) - { - switch (condition.Condition) - { - case ProfileConditionType.Equals: - return string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase); - case ProfileConditionType.NotEquals: - return !string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase); - default: - throw new InvalidOperationException("Unexpected ProfileConditionType"); - } - } - } - - else if (condition.Property == ProfileConditionValue.AudioProfile) - { - var profile = audioStream == null ? null : audioStream.Profile; - - if (!string.IsNullOrWhiteSpace(profile)) - { - switch (condition.Condition) - { - case ProfileConditionType.Equals: - return string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase); - case ProfileConditionType.NotEquals: - return !string.Equals(profile, condition.Value, StringComparison.OrdinalIgnoreCase); - default: - throw new InvalidOperationException("Unexpected ProfileConditionType"); - } - } - } - - else - { - var actualValue = GetConditionValue(condition, mediaPath, videoStream, audioStream); - - if (actualValue.HasValue) - { - long expected; - if (long.TryParse(condition.Value, NumberStyles.Any, _usCulture, out expected)) - { - switch (condition.Condition) - { - case ProfileConditionType.Equals: - return actualValue.Value == expected; - case ProfileConditionType.GreaterThanEqual: - return actualValue.Value >= expected; - case ProfileConditionType.LessThanEqual: - return actualValue.Value <= expected; - case ProfileConditionType.NotEquals: - return actualValue.Value != expected; - default: - throw new InvalidOperationException("Unexpected ProfileConditionType"); - } - } - } - } - - // Value doesn't exist in metadata. Fail it if required. - return !condition.IsRequired; - } - - /// - /// Gets the condition value. - /// - /// The condition. - /// The media path. - /// The video stream. - /// The audio stream. - /// System.Nullable{System.Int64}. - /// Unexpected Property - private long? GetConditionValue(ProfileCondition condition, string mediaPath, MediaStream videoStream, MediaStream audioStream) - { - switch (condition.Property) - { - case ProfileConditionValue.AudioBitrate: - return audioStream == null ? null : audioStream.BitRate; - case ProfileConditionValue.AudioChannels: - return audioStream == null ? null : audioStream.Channels; - case ProfileConditionValue.VideoBitrate: - return videoStream == null ? null : videoStream.BitRate; - case ProfileConditionValue.VideoFramerate: - return videoStream == null ? null : (ConvertToLong(videoStream.AverageFrameRate ?? videoStream.RealFrameRate)); - case ProfileConditionValue.Height: - return videoStream == null ? null : videoStream.Height; - case ProfileConditionValue.Width: - return videoStream == null ? null : videoStream.Width; - case ProfileConditionValue.VideoLevel: - return videoStream == null ? null : ConvertToLong(videoStream.Level); - default: - throw new InvalidOperationException("Unexpected Property"); - } - } - - /// - /// Converts to long. - /// - /// The value. - /// System.Nullable{System.Int64}. - private long? ConvertToLong(float? val) - { - return val.HasValue ? Convert.ToInt64(val.Value) : (long?)null; - } - - /// - /// Converts to long. - /// - /// The value. - /// System.Nullable{System.Int64}. - private long? ConvertToLong(double? val) - { - return val.HasValue ? Convert.ToInt64(val.Value) : (long?)null; - } } } diff --git a/MediaBrowser.Dlna/PlayTo/StreamHelper.cs b/MediaBrowser.Dlna/PlayTo/StreamHelper.cs deleted file mode 100644 index b65e94fd1..000000000 --- a/MediaBrowser.Dlna/PlayTo/StreamHelper.cs +++ /dev/null @@ -1,69 +0,0 @@ -using MediaBrowser.Model.Entities; -using System.Collections.Generic; -using System.Globalization; - -namespace MediaBrowser.Dlna.PlayTo -{ - class StreamHelper - { - /// - /// Gets the audio URL. - /// - /// The device properties. - /// The item. - /// The streams. - /// The server address. - /// System.String. - internal static string GetAudioUrl(DeviceInfo deviceProperties, PlaylistItem item, List streams, string serverAddress) - { - var dlnaCommand = BuildDlnaUrl(deviceProperties, item); - - return string.Format("{0}/audio/{1}/stream{2}?{3}", serverAddress, item.ItemId, "." + item.Container.TrimStart('.'), dlnaCommand); - } - - /// - /// Gets the video URL. - /// - /// The device properties. - /// The item. - /// The streams. - /// The server address. - /// The url to send to the device - internal static string GetVideoUrl(DeviceInfo deviceProperties, PlaylistItem item, List streams, string serverAddress) - { - var dlnaCommand = BuildDlnaUrl(deviceProperties, item); - - return string.Format("{0}/Videos/{1}/stream{2}?{3}", serverAddress, item.ItemId, item.Container, dlnaCommand); - } - - /// - /// Builds the dlna URL. - /// - private static string BuildDlnaUrl(DeviceInfo deviceProperties, PlaylistItem item) - { - var usCulture = new CultureInfo("en-US"); - - var list = new List - { - item.DeviceProfileId ?? string.Empty, - deviceProperties.UUID ?? string.Empty, - item.MediaSourceId ?? string.Empty, - (!item.Transcode).ToString().ToLower(), - item.VideoCodec ?? string.Empty, - item.AudioCodec ?? string.Empty, - item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(usCulture) : string.Empty, - item.SubtitleStreamIndex.HasValue ? item.SubtitleStreamIndex.Value.ToString(usCulture) : string.Empty, - item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(usCulture) : string.Empty, - item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(usCulture) : string.Empty, - item.MaxAudioChannels.HasValue ? item.MaxAudioChannels.Value.ToString(usCulture) : string.Empty, - item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(usCulture) : string.Empty, - item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(usCulture) : string.Empty, - item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(usCulture) : string.Empty, - item.StartPositionTicks.ToString(usCulture), - item.VideoLevel.HasValue ? item.VideoLevel.Value.ToString(usCulture) : string.Empty - }; - - return string.Format("Params={0}", string.Join(";", list.ToArray())); - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Dlna/Server/ControlHandler.cs b/MediaBrowser.Dlna/Server/ControlHandler.cs index d05062f38..049eaff65 100644 --- a/MediaBrowser.Dlna/Server/ControlHandler.cs +++ b/MediaBrowser.Dlna/Server/ControlHandler.cs @@ -1,15 +1,23 @@ using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using System.Threading; using System.Xml; namespace MediaBrowser.Dlna.Server @@ -19,7 +27,12 @@ namespace MediaBrowser.Dlna.Server private readonly ILogger _logger; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; - private DeviceProfile _profile; + private readonly DeviceProfile _profile; + private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; + + private readonly string _serverAddress; private const string NS_DC = "http://purl.org/dc/elements/1.1/"; private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; @@ -31,11 +44,16 @@ namespace MediaBrowser.Dlna.Server private int systemID = 0; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public ControlHandler(ILogger logger, IUserManager userManager, ILibraryManager libraryManager) + public ControlHandler(ILogger logger, IUserManager userManager, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, IDtoService dtoService, IImageProcessor imageProcessor, IUserDataManager userDataManager) { _logger = logger; _userManager = userManager; _libraryManager = libraryManager; + _profile = profile; + _serverAddress = serverAddress; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + _userDataManager = userDataManager; } public ControlResponse ProcessControlRequest(ControlRequest request) @@ -46,6 +64,8 @@ namespace MediaBrowser.Dlna.Server } catch (Exception ex) { + _logger.ErrorException("Error processing control request", ex); + return GetErrorResponse(ex); } } @@ -120,7 +140,8 @@ namespace MediaBrowser.Dlna.Server var controlResponse = new ControlResponse { - Xml = env.OuterXml + Xml = env.OuterXml, + IsSuccessful = true }; controlResponse.Headers.Add("EXT", string.Empty); @@ -153,7 +174,8 @@ namespace MediaBrowser.Dlna.Server return new ControlResponse { - Xml = env.OuterXml + Xml = env.OuterXml, + IsSuccessful = false }; } @@ -161,7 +183,16 @@ namespace MediaBrowser.Dlna.Server { var id = sparams["ObjectID"]; - var newbookmark = long.Parse(sparams["PosSecond"]); + var item = _libraryManager.GetItemById(new Guid(id)); + + var newbookmark = int.Parse(sparams["PosSecond"], _usCulture); + + var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + + userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks; + + _userDataManager.SaveUserData(user.Id, item, userdata, UserDataSaveReason.TogglePlayed, + CancellationToken.None); return new Headers(); } @@ -209,13 +240,13 @@ namespace MediaBrowser.Dlna.Server var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; - int requested = 20; var provided = 0; + int requested = 0; int start = 0; if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requested) && requested <= 0) { - requested = 20; + requested = 0; } if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out start) && start <= 0) { @@ -232,11 +263,11 @@ namespace MediaBrowser.Dlna.Server didl.SetAttribute("xmlns:sec", NS_SEC); result.AppendChild(didl); - var folder = string.IsNullOrWhiteSpace(id) + var folder = string.IsNullOrWhiteSpace(id) || string.Equals(id, "0", StringComparison.OrdinalIgnoreCase) ? user.RootFolder : (Folder)_libraryManager.GetItemById(new Guid(id)); - var children = folder.GetChildren(user, true).ToList(); + var children = GetChildrenSorted(folder, user).ToList(); if (string.Equals(flag, "BrowseMetadata")) { @@ -245,40 +276,29 @@ namespace MediaBrowser.Dlna.Server } else { - foreach (var i in children.OfType()) + if (start > 0) { - if (start > 0) - { - start--; - continue; - } - - var childCount = i.GetChildren(user, true).Count(); - - Browse_AddFolder(result, i, childCount); - - if (++provided == requested) - { - break; - } + children = children.Skip(start).ToList(); + } + if (requested > 0) + { + children = children.Take(requested).ToList(); } - if (provided != requested) + provided = children.Count; + + foreach (var i in children) { - foreach (var i in children.Where(i => !i.IsFolder)) + if (i.IsFolder) { - if (start > 0) - { - start--; - continue; - } + var f = (Folder)i; + var childCount = GetChildrenSorted(f, user).Count(); + Browse_AddFolder(result, f, childCount); + } + else + { Browse_AddItem(result, i, user); - - if (++provided == requested) - { - break; - } } } } @@ -294,10 +314,23 @@ namespace MediaBrowser.Dlna.Server }; } + private IEnumerable GetChildrenSorted(Folder folder, User user) + { + var children = folder.GetChildren(user, true).Where(i => i.LocationType != LocationType.Virtual); + + if (folder is Series || folder is Season || folder is BoxSet) + { + return children; + } + + return _libraryManager.Sort(children, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending); + } + private void Browse_AddFolder(XmlDocument result, Folder f, int childCount) { var container = result.CreateElement(string.Empty, "container", NS_DIDL); container.SetAttribute("restricted", "0"); + container.SetAttribute("searchable", "1"); container.SetAttribute("childCount", childCount.ToString(_usCulture)); container.SetAttribute("id", f.Id.ToString("N")); @@ -311,20 +344,28 @@ namespace MediaBrowser.Dlna.Server container.SetAttribute("parentID", parent.Id.ToString("N")); } - var title = result.CreateElement("dc", "title", NS_DC); - title.InnerText = f.Name; - container.AppendChild(title); + AddCommonFields(f, container); - var date = result.CreateElement("dc", "date", NS_DC); - date.InnerText = f.DateModified.ToString("o"); - container.AppendChild(date); + AddCover(f, container); - var objectClass = result.CreateElement("upnp", "class", NS_UPNP); - objectClass.InnerText = "object.container.storageFolder"; - container.AppendChild(objectClass); + container.AppendChild(CreateObjectClass(result, f)); result.DocumentElement.AppendChild(container); } + private void AddValue(XmlElement elem, string prefix, string name, string value, string namespaceUri) + { + try + { + var date = elem.OwnerDocument.CreateElement(prefix, name, namespaceUri); + date.InnerText = value; + elem.AppendChild(date); + } + catch (XmlException) + { + //_logger.Error("Error adding xml value: " + value); + } + } + private void Browse_AddItem(XmlDocument result, BaseItem item, User user) { var element = result.CreateElement(string.Empty, "item", NS_DIDL); @@ -342,52 +383,217 @@ namespace MediaBrowser.Dlna.Server AddGeneralProperties(item, element); - AddActors(item, element); + // refID? + // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); - var title = result.CreateElement("dc", "title", NS_DC); - title.InnerText = item.Name; - element.AppendChild(title); + var audio = item as Audio; + if (audio != null) + { + AddAudioResource(element, audio); + } - var res = result.CreateElement(string.Empty, "res", NS_DIDL); - - //res.InnerText = String.Format( - // "http://{0}:{1}{2}file/{3}", - // request.LocalEndPoint.Address, - // request.LocalEndPoint.Port, - // prefix, - // resource.Id - // ); - - //if (props.TryGetValue("SizeRaw", out prop)) - //{ - // res.SetAttribute("size", prop); - //} - //if (props.TryGetValue("Resolution", out prop)) - //{ - // res.SetAttribute("resolution", prop); - //} - //if (props.TryGetValue("Duration", out prop)) - //{ - // res.SetAttribute("duration", prop); - //} - - //res.SetAttribute("protocolInfo", String.Format( - // "http-get:*:{1}:{0};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS={2}", - // resource.PN, DlnaMaps.Mime[resource.Type], DlnaMaps.DefaultStreaming - // )); - - element.AppendChild(res); + var video = item as Video; + if (video != null) + { + AddVideoResource(element, video); + } AddCover(item, element); result.DocumentElement.AppendChild(element); } + private string GetDeviceId() + { + return "erer"; + } + + private void AddVideoResource(XmlElement container, Video video) + { + var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); + + var sources = _dtoService.GetMediaSources(video); + + int? maxBitrateSetting = null; + + var streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions + { + ItemId = video.Id.ToString("N"), + MediaSources = sources, + Profile = _profile, + DeviceId = GetDeviceId(), + MaxBitrate = maxBitrateSetting + }); + + var url = streamInfo.ToDlnaUrl(_serverAddress); + res.InnerText = url; + + var mediaSource = sources.First(i => string.Equals(i.Id, streamInfo.MediaSourceId)); + + if (mediaSource.RunTimeTicks.HasValue) + { + res.SetAttribute("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + } + + if (streamInfo.IsDirectStream && mediaSource.Size.HasValue) + { + res.SetAttribute("size", mediaSource.Size.Value.ToString(_usCulture)); + } + + var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video && !string.Equals(i.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)); + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + var targetAudioBitrate = streamInfo.AudioBitrate ?? (audioStream == null ? null : audioStream.BitRate); + var targetSampleRate = audioStream == null ? null : audioStream.SampleRate; + var targetChannels = streamInfo.MaxAudioChannels ?? (audioStream == null ? null : audioStream.Channels); + + var targetWidth = streamInfo.MaxWidth ?? (videoStream == null ? null : videoStream.Width); + var targetHeight = streamInfo.MaxHeight ?? (videoStream == null ? null : videoStream.Height); + + var targetVideoCodec = streamInfo.IsDirectStream + ? (videoStream == null ? null : videoStream.Codec) + : streamInfo.VideoCodec; + + var targetAudioCodec = streamInfo.IsDirectStream + ? (audioStream == null ? null : audioStream.Codec) + : streamInfo.AudioCodec; + + var targetBitrate = maxBitrateSetting ?? mediaSource.Bitrate; + + if (targetChannels.HasValue) + { + res.SetAttribute("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + } + + if (targetSampleRate.HasValue) + { + res.SetAttribute("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + } + + if (targetAudioBitrate.HasValue) + { + res.SetAttribute("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); + } + + var formatProfile = new MediaFormatProfileResolver().ResolveVideoFormat(streamInfo.Container, + targetVideoCodec, + targetAudioCodec, + targetWidth, + targetHeight, + targetBitrate, + TransportStreamTimestamp.NONE); + + var filename = url.Substring(0, url.IndexOf('?')); + + var orgOpValue = DlnaMaps.GetOrgOpValue(mediaSource.RunTimeTicks.HasValue, streamInfo.IsDirectStream, streamInfo.TranscodeSeekInfo); + + var orgCi = streamInfo.IsDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + + res.SetAttribute("protocolInfo", String.Format( + "http-get:*:{0}:DLNA.ORG_PN={1};DLNA.ORG_OP={2};DLNA.ORG_CI={3};DLNA.ORG_FLAGS={4}", + MimeTypes.GetMimeType(filename), + formatProfile, + orgOpValue, + orgCi, + DlnaMaps.DefaultStreaming + )); + + container.AppendChild(res); + } + + private void AddAudioResource(XmlElement container, Audio audio) + { + var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); + + var sources = _dtoService.GetMediaSources(audio); + + var streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions + { + ItemId = audio.Id.ToString("N"), + MediaSources = sources, + Profile = _profile, + DeviceId = GetDeviceId() + }); + + var url = streamInfo.ToDlnaUrl(_serverAddress); + res.InnerText = url; + + var mediaSource = sources.First(i => string.Equals(i.Id, streamInfo.MediaSourceId)); + + if (mediaSource.RunTimeTicks.HasValue) + { + res.SetAttribute("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + } + + if (streamInfo.IsDirectStream && mediaSource.Size.HasValue) + { + res.SetAttribute("size", mediaSource.Size.Value.ToString(_usCulture)); + } + + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + var targetAudioBitrate = streamInfo.AudioBitrate ?? (audioStream == null ? null : audioStream.BitRate); + var targetSampleRate = audioStream == null ? null : audioStream.SampleRate; + var targetChannels = streamInfo.MaxAudioChannels ?? (audioStream == null ? null : audioStream.Channels); + + if (targetChannels.HasValue) + { + res.SetAttribute("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + } + + if (targetSampleRate.HasValue) + { + res.SetAttribute("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + } + + if (targetAudioBitrate.HasValue) + { + res.SetAttribute("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); + } + + var formatProfile = new MediaFormatProfileResolver().ResolveAudioFormat(streamInfo.Container, targetAudioBitrate, targetSampleRate, targetChannels); + + var filename = url.Substring(0, url.IndexOf('?')); + + var orgOpValue = DlnaMaps.GetOrgOpValue(mediaSource.RunTimeTicks.HasValue, streamInfo.IsDirectStream, streamInfo.TranscodeSeekInfo); + + var orgCi = streamInfo.IsDirectStream ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1"; + + res.SetAttribute("protocolInfo", String.Format( + "http-get:*:{0}:DLNA.ORG_PN={1};DLNA.ORG_OP={2};DLNA.ORG_CI={3};DLNA.ORG_FLAGS={4}", + MimeTypes.GetMimeType(filename), + formatProfile, + orgOpValue, + orgCi, + DlnaMaps.DefaultStreaming + )); + + container.AppendChild(res); + } + private XmlElement CreateObjectClass(XmlDocument result, BaseItem item) { var objectClass = result.CreateElement("upnp", "class", NS_UPNP); - if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) + if (item.IsFolder) + { + string classType = null; + + if (!_profile.RequiresPlainFolders) + { + if (item is MusicAlbum) + { + classType = "object.container.musicAlbum"; + } + if (item is MusicArtist) + { + classType = "object.container.musicArtist"; + } + } + + objectClass.InnerText = classType ?? "object.container.storageFolder"; + } + else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { objectClass.InnerText = "object.item.audioItem.musicTrack"; } @@ -397,7 +603,14 @@ namespace MediaBrowser.Dlna.Server } else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { - objectClass.InnerText = "object.item.videoItem.movie"; + if (!_profile.RequiresPlainVideoItems && item is Movie) + { + objectClass.InnerText = "object.item.videoItem.movie"; + } + else + { + objectClass.InnerText = "object.item.videoItem"; + } } else { @@ -407,152 +620,259 @@ namespace MediaBrowser.Dlna.Server return objectClass; } - private void AddActors(BaseItem item, XmlElement element) + private void AddPeople(BaseItem item, XmlElement element) { foreach (var actor in item.People) { - var e = element.OwnerDocument.CreateElement("upnp", "actor", NS_UPNP); - e.InnerText = actor.Name; - element.AppendChild(e); + AddValue(element, "upnp", (actor.Type ?? PersonType.Actor).ToLower(), actor.Name, NS_UPNP); } } private void AddBookmarkInfo(BaseItem item, User user, XmlElement element) { - //var bookmark = bookmarkable.Bookmark; - //if (bookmark.HasValue) - //{ - // var dcmInfo = item.OwnerDocument.CreateElement("sec", "dcmInfo", NS_SEC); - // dcmInfo.InnerText = string.Format("BM={0}", bookmark.Value); - // item.AppendChild(dcmInfo); - //} + var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + + if (userdata.PlaybackPositionTicks > 0) + { + var dcmInfo = element.OwnerDocument.CreateElement("sec", "dcmInfo", NS_SEC); + dcmInfo.InnerText = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds).ToString(_usCulture)); + element.AppendChild(dcmInfo); + } } - private void AddGeneralProperties(BaseItem item, XmlElement element) + /// + /// Adds fields used by both items and folders + /// + /// + /// + private void AddCommonFields(BaseItem item, XmlElement element) { - //var prop = string.Empty; - //if (props.TryGetValue("DateO", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("dc", "date", NS_DC); - // e.InnerText = prop; - // item.AppendChild(e); - //} - //if (props.TryGetValue("Genre", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("upnp", "genre", NS_UPNP); - // e.InnerText = prop; - // item.AppendChild(e); - //} + if (item.PremiereDate.HasValue) + { + AddValue(element, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC); + } + + if (item.Genres.Count > 0) + { + AddValue(element, "upnp", "genre", item.Genres[0], NS_UPNP); + } + + if (item.Studios.Count > 0) + { + AddValue(element, "upnp", "publisher", item.Studios[0], NS_UPNP); + } + + AddValue(element, "dc", "title", item.Name, NS_DC); if (!string.IsNullOrWhiteSpace(item.Overview)) { - var e = element.OwnerDocument.CreateElement("dc", "description", NS_DC); - e.InnerText = item.Overview; - element.AppendChild(e); + AddValue(element, "dc", "description", item.Overview, NS_DC); } - //if (props.TryGetValue("Artist", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("upnp", "artist", NS_UPNP); - // e.SetAttribute("role", "AlbumArtist"); - // e.InnerText = prop; - // item.AppendChild(e); - //} - //if (props.TryGetValue("Performer", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("upnp", "artist", NS_UPNP); - // e.SetAttribute("role", "Performer"); - // e.InnerText = prop; - // item.AppendChild(e); - // e = item.OwnerDocument.CreateElement("dc", "creator", NS_DC); - // e.InnerText = prop; - // item.AppendChild(e); - //} - //if (props.TryGetValue("Album", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("upnp", "album", NS_UPNP); - // e.InnerText = prop; - // item.AppendChild(e); - //} - //if (props.TryGetValue("Track", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("upnp", "originalTrackNumber", NS_UPNP); - // e.InnerText = prop; - // item.AppendChild(e); - //} - //if (props.TryGetValue("Creator", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("dc", "creator", NS_DC); - // e.InnerText = prop; - // item.AppendChild(e); - //} + if (!string.IsNullOrEmpty(item.OfficialRating)) + { + AddValue(element, "dc", "rating", item.OfficialRating, NS_DC); + } - //if (props.TryGetValue("Director", out prop)) - //{ - // var e = item.OwnerDocument.CreateElement("upnp", "director", NS_UPNP); - // e.InnerText = prop; - // item.AppendChild(e); - //} + AddPeople(item, element); + } + + private void AddGeneralProperties(BaseItem item, XmlElement element) + { + AddCommonFields(item, element); + + var audio = item as Audio; + + if (audio != null) + { + if (audio.Artists.Count > 0) + { + AddValue(element, "upnp", "artist", audio.Artists[0], NS_UPNP); + } + + if (!string.IsNullOrEmpty(audio.Album)) + { + AddValue(element, "upnp", "album", audio.Album, NS_UPNP); + } + + if (!string.IsNullOrEmpty(audio.AlbumArtist)) + { + AddValue(element, "upnp", "albumArtist", audio.AlbumArtist, NS_UPNP); + } + } + + var album = item as MusicAlbum; + + if (album != null) + { + if (!string.IsNullOrEmpty(album.AlbumArtist)) + { + AddValue(element, "upnp", "artist", album.AlbumArtist, NS_UPNP); + AddValue(element, "upnp", "albumArtist", album.AlbumArtist, NS_UPNP); + } + } + + var musicVideo = item as MusicVideo; + + if (musicVideo != null) + { + if (!string.IsNullOrEmpty(musicVideo.Artist)) + { + AddValue(element, "upnp", "artist", musicVideo.Artist, NS_UPNP); + } + + if (!string.IsNullOrEmpty(musicVideo.Album)) + { + AddValue(element, "upnp", "album", musicVideo.Album, NS_UPNP); + } + } + + if (item.IndexNumber.HasValue) + { + AddValue(element, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + } } private void AddCover(BaseItem item, XmlElement element) { - //var result = item.OwnerDocument; - //var cover = resource as IMediaCover; - //if (cover == null) - //{ - // return; - //} - //try - //{ - // var c = cover.Cover; - // var curl = String.Format( - // "http://{0}:{1}{2}cover/{3}", - // request.LocalEndPoint.Address, - // request.LocalEndPoint.Port, - // prefix, - // resource.Id - // ); - // var icon = result.CreateElement("upnp", "albumArtURI", NS_UPNP); - // var profile = result.CreateAttribute("dlna", "profileID", NS_DLNA); - // profile.InnerText = "JPEG_TN"; - // icon.SetAttributeNode(profile); - // icon.InnerText = curl; - // item.AppendChild(icon); - // icon = result.CreateElement("upnp", "icon", NS_UPNP); - // profile = result.CreateAttribute("dlna", "profileID", NS_DLNA); - // profile.InnerText = "JPEG_TN"; - // icon.SetAttributeNode(profile); - // icon.InnerText = curl; - // item.AppendChild(icon); + var imageInfo = GetImageInfo(item); - // var res = result.CreateElement(string.Empty, "res", NS_DIDL); - // res.InnerText = curl; + if (imageInfo == null) + { + return; + } - // res.SetAttribute("protocolInfo", string.Format( - // "http-get:*:{1}:{0};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS={2}", - // c.PN, DlnaMaps.Mime[c.Type], DlnaMaps.DefaultStreaming - // )); - // var width = c.MetaWidth; - // var height = c.MetaHeight; - // if (width.HasValue && height.HasValue) - // { - // res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value)); - // } - // else - // { - // res.SetAttribute("resolution", "200x200"); - // } - // res.SetAttribute("protocolInfo", string.Format( - // "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=01;DLNA.ORG_CI=1;DLNA.ORG_FLAGS={0}", - // DlnaMaps.DefaultInteractive - // )); - // item.AppendChild(res); - //} - //catch (Exception) - //{ - // return; - //} + var result = element.OwnerDocument; + + var curl = GetImageUrl(imageInfo); + + var icon = result.CreateElement("upnp", "albumArtURI", NS_UPNP); + var profile = result.CreateAttribute("dlna", "profileID", NS_DLNA); + profile.InnerText = "JPEG_TN"; + icon.SetAttributeNode(profile); + icon.InnerText = curl; + element.AppendChild(icon); + + icon = result.CreateElement("upnp", "icon", NS_UPNP); + profile = result.CreateAttribute("dlna", "profileID", NS_DLNA); + profile.InnerText = "JPEG_TN"; + icon.SetAttributeNode(profile); + icon.InnerText = curl; + element.AppendChild(icon); + + if (!_profile.EnableAlbumArtInDidl) + { + return; + } + + var res = result.CreateElement(string.Empty, "res", NS_DIDL); + res.InnerText = curl; + + int? width = imageInfo.Width; + int? height = imageInfo.Height; + + var mediaProfile = new MediaFormatProfileResolver().ResolveImageFormat("jpg", width, height); + + res.SetAttribute("protocolInfo", string.Format( + "http-get:*:{1}DLNA.ORG_PN=:{0};DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS={2}", + mediaProfile, "image/jpeg", DlnaMaps.DefaultStreaming + )); + + if (width.HasValue && height.HasValue) + { + res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value)); + } + else + { + // TODO: Devices need to see something here? + res.SetAttribute("resolution", "200x200"); + } + + element.AppendChild(res); + } + + private ImageDownloadInfo GetImageInfo(BaseItem item) + { + if (item.HasImage(ImageType.Primary)) + { + return GetImageInfo(item, ImageType.Primary); + } + if (item.HasImage(ImageType.Thumb)) + { + return GetImageInfo(item, ImageType.Thumb); + } + + if (item is Audio || item is Episode) + { + item = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Primary)); + + if (item != null) + { + return GetImageInfo(item, ImageType.Primary); + } + } + + return null; + } + + private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) + { + var imageInfo = item.GetImageInfo(type, 0); + string tag = null; + + try + { + var guid = _imageProcessor.GetImageCacheTag(item, ImageType.Primary); + + tag = guid.HasValue ? guid.Value.ToString("N") : null; + } + catch + { + + } + + int? width = null; + int? height = null; + + try + { + var size = _imageProcessor.GetImageSize(imageInfo.Path, imageInfo.DateModified); + + width = Convert.ToInt32(size.Width); + height = Convert.ToInt32(size.Height); + } + catch + { + + } + + return new ImageDownloadInfo + { + ItemId = item.Id.ToString("N"), + Type = ImageType.Primary, + ImageTag = tag, + Width = width, + Height = height + }; + } + + class ImageDownloadInfo + { + internal string ItemId; + internal string ImageTag; + internal ImageType Type; + + internal int? Width; + internal int? Height; + } + + private string GetImageUrl(ImageDownloadInfo info) + { + return string.Format("{0}/Items/{1}/Images/{2}?tag={3}&format=jpg", + _serverAddress, + info.ItemId, + info.Type, + info.ImageTag); } } } diff --git a/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs b/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs index ee7fae6f9..33286a474 100644 --- a/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs +++ b/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs @@ -176,7 +176,7 @@ namespace MediaBrowser.Dlna.Server ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1", ServiceId = "urn:upnp-org:serviceId:ContentDirectory", ScpdUrl = "/mediabrowser/dlna/contentdirectory.xml", - ControlUrl = "/mediabrowser/dlna/control" + ControlUrl = "/mediabrowser/dlna/" + _serverUdn + "/control" }); return list; diff --git a/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs b/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs index 702aa6f99..050d89f49 100644 --- a/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs +++ b/MediaBrowser.Dlna/Server/DlnaServerEntryPoint.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common; +using System.Linq; +using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -18,8 +19,12 @@ namespace MediaBrowser.Dlna.Server private readonly IApplicationHost _appHost; private readonly INetworkManager _network; + public static DlnaServerEntryPoint Instance; + public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost, INetworkManager network) { + Instance = this; + _config = config; _appHost = appHost; _network = network; @@ -86,6 +91,11 @@ namespace MediaBrowser.Dlna.Server } } + public UpnpDevice GetServerUpnpDevice(string uuid) + { + return _ssdpHandler.Devices.FirstOrDefault(i => string.Equals(uuid, i.Uuid.ToString("N"), StringComparison.OrdinalIgnoreCase)); + } + private void DisposeServer() { lock (_syncLock) diff --git a/MediaBrowser.Dlna/Server/SsdpHandler.cs b/MediaBrowser.Dlna/Server/SsdpHandler.cs index c908eb4a8..0c3e6c735 100644 --- a/MediaBrowser.Dlna/Server/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Server/SsdpHandler.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.Dlna.Server Start(); } - private IEnumerable Devices + public IEnumerable Devices { get { diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index cdd9df49b..a7fde61f8 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -224,7 +224,7 @@ namespace MediaBrowser.MediaEncoding.Encoder case EncodingQuality.HighSpeed: return 2; case EncodingQuality.HighQuality: - return isWebm ? Math.Max((int)((Environment.ProcessorCount -1) / 2) , 2) : 0; + return 2; case EncodingQuality.MaxQuality: return isWebm ? Math.Max(Environment.ProcessorCount - 1, 2) : 0; default: diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 361e09b14..389046c9e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -768,7 +768,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. - var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail=40\" -f image2 \"{1}\"", inputPath, "-", vf) : + var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2},thumbnail=30\" -f image2 \"{1}\"", inputPath, "-", vf) : string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, "-", vf); var probeSize = GetProbeSizeArgument(type); diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index 99928bcba..2e27015ee 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -119,6 +119,15 @@ Dlna\DirectPlayProfile.cs + + Dlna\DlnaMaps.cs + + + Dlna\MediaFormatProfile.cs + + + Dlna\MediaFormatProfileResolver.cs + Dlna\ResponseProfile.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index 3738decff..1a38398da 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -106,6 +106,15 @@ Dlna\DirectPlayProfile.cs + + Dlna\DlnaMaps.cs + + + Dlna\MediaFormatProfile.cs + + + Dlna\MediaFormatProfileResolver.cs + Dlna\ResponseProfile.cs diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs new file mode 100644 index 000000000..eb0c33315 --- /dev/null +++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs @@ -0,0 +1,62 @@ +using System; + +namespace MediaBrowser.Model.Dlna +{ + public class DlnaMaps + { + public static readonly string DefaultStreaming = + FlagsToString(DlnaFlags.StreamingTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.ConnectionStall | + DlnaFlags.ByteBasedSeek | + DlnaFlags.DlnaV15); + + public static readonly string DefaultInteractive = + FlagsToString(DlnaFlags.InteractiveTransferMode | + DlnaFlags.BackgroundTransferMode | + DlnaFlags.ConnectionStall | + DlnaFlags.ByteBasedSeek | + DlnaFlags.DlnaV15); + + public static string FlagsToString(DlnaFlags flags) + { + return string.Format("{0:X8}{1:D24}", (ulong)flags, 0); + } + + public static string GetOrgOpValue(bool hasKnownRuntime, bool isDirectStream, TranscodeSeekInfo profileTranscodeSeekInfo) + { + if (hasKnownRuntime) + { + var orgOp = string.Empty; + + // Time-based seeking currently only possible when transcoding + orgOp += isDirectStream ? "0" : "1"; + + // Byte-based seeking only possible when not transcoding + orgOp += isDirectStream || profileTranscodeSeekInfo == TranscodeSeekInfo.Bytes ? "1" : "0"; + + return orgOp; + } + + // No seeking is available if we don't know the content runtime + return "00"; + } + } + + [Flags] + public enum DlnaFlags : ulong + { + BackgroundTransferMode = (1 << 22), + ByteBasedSeek = (1 << 29), + ConnectionStall = (1 << 21), + DlnaV15 = (1 << 20), + InteractiveTransferMode = (1 << 23), + PlayContainer = (1 << 28), + RtspPause = (1 << 25), + S0Increase = (1 << 27), + SenderPaced = (1L << 31), + SnIncrease = (1 << 26), + StreamingTransferMode = (1 << 24), + TimeBasedSeek = (1 << 30) + } +} diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfile.cs b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs new file mode 100644 index 000000000..e686fe932 --- /dev/null +++ b/MediaBrowser.Model/Dlna/MediaFormatProfile.cs @@ -0,0 +1,122 @@ + +using System; + +namespace MediaBrowser.Model.Dlna +{ + public enum MediaFormatProfile + { + MP3, + WMA_BASE, + WMA_FULL, + LPCM16_44_MONO, + LPCM16_44_STEREO, + LPCM16_48_MONO, + LPCM16_48_STEREO, + AAC_ISO, + AAC_ISO_320, + AAC_ADTS, + AAC_ADTS_320, + FLAC, + OGG, + + JPEG_SM, + JPEG_MED, + JPEG_LRG, + JPEG_TN, + PNG_LRG, + PNG_TN, + GIF_LRG, + RAW, + + MPEG1, + MPEG_PS_PAL, + MPEG_PS_NTSC, + MPEG_TS_SD_EU, + MPEG_TS_SD_EU_ISO, + MPEG_TS_SD_EU_T, + MPEG_TS_SD_NA, + MPEG_TS_SD_NA_ISO, + MPEG_TS_SD_NA_T, + MPEG_TS_SD_KO, + MPEG_TS_SD_KO_ISO, + MPEG_TS_SD_KO_T, + MPEG_TS_JP_T, + AVI, + MATROSKA, + FLV, + DVR_MS, + WTV, + OGV, + AVC_MP4_MP_SD_AAC_MULT5, + AVC_MP4_MP_SD_MPEG1_L3, + AVC_MP4_MP_SD_AC3, + AVC_MP4_MP_HD_720p_AAC, + AVC_MP4_MP_HD_1080i_AAC, + AVC_MP4_HP_HD_AAC, + AVC_TS_MP_HD_AAC_MULT5, + AVC_TS_MP_HD_AAC_MULT5_T, + AVC_TS_MP_HD_AAC_MULT5_ISO, + AVC_TS_MP_HD_MPEG1_L3, + AVC_TS_MP_HD_MPEG1_L3_T, + AVC_TS_MP_HD_MPEG1_L3_ISO, + AVC_TS_MP_HD_AC3, + AVC_TS_MP_HD_AC3_T, + AVC_TS_MP_HD_AC3_ISO, + AVC_TS_HP_HD_MPEG1_L2_T, + AVC_TS_HP_HD_MPEG1_L2_ISO, + AVC_TS_MP_SD_AAC_MULT5, + AVC_TS_MP_SD_AAC_MULT5_T, + AVC_TS_MP_SD_AAC_MULT5_ISO, + AVC_TS_MP_SD_MPEG1_L3, + AVC_TS_MP_SD_MPEG1_L3_T, + AVC_TS_MP_SD_MPEG1_L3_ISO, + AVC_TS_HP_SD_MPEG1_L2_T, + AVC_TS_HP_SD_MPEG1_L2_ISO, + AVC_TS_MP_SD_AC3, + AVC_TS_MP_SD_AC3_T, + AVC_TS_MP_SD_AC3_ISO, + AVC_TS_HD_DTS_T, + AVC_TS_HD_DTS_ISO, + WMVMED_BASE, + WMVMED_FULL, + WMVMED_PRO, + WMVHIGH_FULL, + WMVHIGH_PRO, + VC1_ASF_AP_L1_WMA, + VC1_ASF_AP_L2_WMA, + VC1_ASF_AP_L3_WMA, + VC1_TS_AP_L1_AC3_ISO, + VC1_TS_AP_L2_AC3_ISO, + VC1_TS_HD_DTS_ISO, + VC1_TS_HD_DTS_T, + MPEG4_P2_MP4_ASP_AAC, + MPEG4_P2_MP4_SP_L6_AAC, + MPEG4_P2_MP4_NDSD, + MPEG4_P2_TS_ASP_AAC, + MPEG4_P2_TS_ASP_AAC_T, + MPEG4_P2_TS_ASP_AAC_ISO, + MPEG4_P2_TS_ASP_MPEG1_L3, + MPEG4_P2_TS_ASP_MPEG1_L3_T, + MPEG4_P2_TS_ASP_MPEG1_L3_ISO, + MPEG4_P2_TS_ASP_MPEG2_L2, + MPEG4_P2_TS_ASP_MPEG2_L2_T, + MPEG4_P2_TS_ASP_MPEG2_L2_ISO, + MPEG4_P2_TS_ASP_AC3, + MPEG4_P2_TS_ASP_AC3_T, + MPEG4_P2_TS_ASP_AC3_ISO, + AVC_TS_HD_50_LPCM_T, + AVC_MP4_LPCM, + MPEG4_P2_3GPP_SP_L0B_AAC, + MPEG4_P2_3GPP_SP_L0B_AMR, + AVC_3GPP_BL_QCIF15_AAC, + MPEG4_H263_3GPP_P0_L10_AMR, + MPEG4_H263_MP4_P0_L10_AAC + } + + public enum TransportStreamTimestamp + { + NONE, + ZERO, + VALID + } +} diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs new file mode 100644 index 000000000..76480930f --- /dev/null +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -0,0 +1,342 @@ +using System; + +namespace MediaBrowser.Model.Dlna +{ + public class MediaFormatProfileResolver + { + public MediaFormatProfile ResolveVideoFormat(string container, string videoCodec, string audioCodec, int? width, int? height, int? bitrate, TransportStreamTimestamp timestampType) + { + if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) + return ResolveVideoASFFormat(videoCodec, audioCodec, width, height, bitrate); + if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase)) + return ResolveVideoMP4Format(videoCodec, audioCodec, width, height, bitrate); + if (string.Equals(container, "avi", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.AVI; + if (string.Equals(container, "mkv", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.MATROSKA; + if (string.Equals(container, "mpeg2ps", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase)) + // MediaFormatProfile.MPEG_PS_PAL, MediaFormatProfile.MPEG_PS_NTSC + return MediaFormatProfile.MPEG_PS_NTSC; + if (string.Equals(container, "mpeg1video", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.MPEG1; + if (string.Equals(container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase)) + return ResolveVideoMPEG2TSFormat(videoCodec, audioCodec, width, height, bitrate, timestampType); + if (string.Equals(container, "flv", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.FLV; + if (string.Equals(container, "wtv", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.WTV; + if (string.Equals(container, "3gp", StringComparison.OrdinalIgnoreCase)) + return ResolveVideo3GPFormat(videoCodec, audioCodec, width, height, bitrate); + if (string.Equals(container, "ogv", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.OGV; + + throw new ArgumentException("Unsupported container: " + container); + } + + private MediaFormatProfile ResolveVideoMPEG2TSFormat(string videoCodec, string audioCodec, int? width, int? height, int? bitrate, TransportStreamTimestamp timestampType) + { + // String suffix = ""; + // if (isNoTimestamp(timestampType)) + // suffix = "_ISO"; + // else if (timestampType == TransportStreamTimestamp.VALID) { + // suffix = "_T"; + // } + + // String resolution = "S"; + // if ((width.intValue() > 720) || (height.intValue() > 576)) { + // resolution = "H"; + // } + + // if (videoCodec == VideoCodec.MPEG2) + // { + // List!(MediaFormatProfile) profiles = Arrays.asList(cast(MediaFormatProfile[])[ MediaFormatProfile.valueOf("MPEG_TS_SD_EU" + suffix), MediaFormatProfile.valueOf("MPEG_TS_SD_NA" + suffix), MediaFormatProfile.valueOf("MPEG_TS_SD_KO" + suffix) ]); + + // if ((timestampType == TransportStreamTimestamp.VALID) && (audioCodec == AudioCodec.AAC)) { + // profiles.add(MediaFormatProfile.MPEG_TS_JP_T); + // } + // return profiles; + // }if (videoCodec == VideoCodec.H264) + // { + // if (audioCodec == AudioCodec.LPCM) + // return Collections.singletonList(MediaFormatProfile.AVC_TS_HD_50_LPCM_T); + // if (audioCodec == AudioCodec.DTS) { + // if (isNoTimestamp(timestampType)) { + // return Collections.singletonList(MediaFormatProfile.AVC_TS_HD_DTS_ISO); + // } + // return Collections.singletonList(MediaFormatProfile.AVC_TS_HD_DTS_T); + // } + // if (audioCodec == AudioCodec.MP2) { + // if (isNoTimestamp(timestampType)) { + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("AVC_TS_HP_%sD_MPEG1_L2_ISO", cast(Object[])[ resolution ]))); + // } + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("AVC_TS_HP_%sD_MPEG1_L2_T", cast(Object[])[ resolution ]))); + // } + + // if (audioCodec == AudioCodec.AAC) + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("AVC_TS_MP_%sD_AAC_MULT5%s", cast(Object[])[ resolution, suffix ]))); + // if (audioCodec == AudioCodec.MP3) + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("AVC_TS_MP_%sD_MPEG1_L3%s", cast(Object[])[ resolution, suffix ]))); + // if ((audioCodec is null) || (audioCodec == AudioCodec.AC3)) { + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("AVC_TS_MP_%sD_AC3%s", cast(Object[])[ resolution, suffix ]))); + // } + // } + // else if (videoCodec == VideoCodec.VC1) { + // if ((audioCodec is null) || (audioCodec == AudioCodec.AC3)) + // { + // if ((width.intValue() > 720) || (height.intValue() > 576)) { + // return Collections.singletonList(MediaFormatProfile.VC1_TS_AP_L2_AC3_ISO); + // } + // return Collections.singletonList(MediaFormatProfile.VC1_TS_AP_L1_AC3_ISO); + // } + // if (audioCodec == AudioCodec.DTS) { + // suffix = suffix.equals("_ISO") ? suffix : "_T"; + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("VC1_TS_HD_DTS%s", cast(Object[])[ suffix ]))); + // } + // } else if ((videoCodec == VideoCodec.MPEG4) || (videoCodec == VideoCodec.MSMPEG4)) { + // if (audioCodec == AudioCodec.AAC) + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("MPEG4_P2_TS_ASP_AAC%s", cast(Object[])[ suffix ]))); + // if (audioCodec == AudioCodec.MP3) + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("MPEG4_P2_TS_ASP_MPEG1_L3%s", cast(Object[])[ suffix ]))); + // if (audioCodec == AudioCodec.MP2) + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("MPEG4_P2_TS_ASP_MPEG2_L2%s", cast(Object[])[ suffix ]))); + // if ((audioCodec is null) || (audioCodec == AudioCodec.AC3)) { + // return Collections.singletonList(MediaFormatProfile.valueOf(String.format("MPEG4_P2_TS_ASP_AC3%s", cast(Object[])[ suffix ]))); + // } + // } + + throw new ArgumentException("Mpeg video file does not match any supported DLNA profile"); + } + + private MediaFormatProfile ResolveVideoMP4Format(string videoCodec, string audioCodec, int? width, int? height, int? bitrate) + { + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(audioCodec, "lpcm", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.AVC_MP4_LPCM; + if (string.IsNullOrEmpty(audioCodec) || + string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.AVC_MP4_MP_SD_AC3; + } + if (string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.AVC_MP4_MP_SD_MPEG1_L3; + } + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 720) && (height.Value <= 576)) + { + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.AVC_MP4_MP_SD_AAC_MULT5; + } + else if ((width.Value <= 1280) && (height.Value <= 720)) + { + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.AVC_MP4_MP_HD_720p_AAC; + } + else if ((width.Value <= 1920) && (height.Value <= 1080)) + { + if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.AVC_MP4_MP_HD_1080i_AAC; + } + } + } + } + else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) + { + if (width.HasValue && height.HasValue && width.Value <= 720 && height.Value <= 576) + { + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.MPEG4_P2_MP4_ASP_AAC; + if (string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.MPEG4_P2_MP4_NDSD; + } + } + else if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.MPEG4_P2_MP4_SP_L6_AAC; + } + } + else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.MPEG4_H263_MP4_P0_L10_AAC; + } + + throw new ArgumentException("MP4 video file does not match any supported DLNA profile"); + } + + private MediaFormatProfile ResolveVideo3GPFormat(string videoCodec, string audioCodec, int? width, int? height, int? bitrate) + { + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.AVC_3GPP_BL_QCIF15_AAC; + } + else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AAC; + if (string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.MPEG4_P2_3GPP_SP_L0B_AMR; + } + else if (string.Equals(videoCodec, "h263", StringComparison.OrdinalIgnoreCase) && string.Equals(audioCodec, "amrnb", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.MPEG4_H263_3GPP_P0_L10_AMR; + } + + throw new ArgumentException("3GP video file does not match any supported DLNA profile"); + } + private MediaFormatProfile ResolveVideoASFFormat(string videoCodec, string audioCodec, int? width, int? height, int? bitrate) + { + if (string.Equals(videoCodec, "wmv", StringComparison.OrdinalIgnoreCase) && + (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase) || string.Equals(videoCodec, "wmapro", StringComparison.OrdinalIgnoreCase))) + { + + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 720) && (height.Value <= 576)) + { + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.WMVMED_FULL; + } + return MediaFormatProfile.WMVMED_PRO; + } + } + + if (string.IsNullOrEmpty(audioCodec) || string.Equals(audioCodec, "wma", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.WMVHIGH_FULL; + } + return MediaFormatProfile.WMVHIGH_PRO; + } + + if (string.Equals(videoCodec, "vc1", StringComparison.OrdinalIgnoreCase)) + { + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 720) && (height.Value <= 576)) + return MediaFormatProfile.VC1_ASF_AP_L1_WMA; + if ((width.Value <= 1280) && (height.Value <= 720)) + return MediaFormatProfile.VC1_ASF_AP_L2_WMA; + if ((width.Value <= 1920) && (height.Value <= 1080)) + return MediaFormatProfile.VC1_ASF_AP_L3_WMA; + } + } + else if (string.Equals(videoCodec, "mpeg2video", StringComparison.OrdinalIgnoreCase)) + { + return MediaFormatProfile.DVR_MS; + } + + throw new ArgumentException("ASF video file does not match any supported DLNA profile"); + } + + public MediaFormatProfile ResolveAudioFormat(string container, int? bitrate, int? frequency, int? channels) + { + if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase)) + return ResolveAudioASFFormat(bitrate, frequency, channels); + if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.MP3; + if (string.Equals(container, "lpcm", StringComparison.OrdinalIgnoreCase)) + return ResolveAudioLPCMFormat(bitrate, frequency, channels); + if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase)) + return ResolveAudioMP4Format(bitrate, frequency, channels); + if (string.Equals(container, "adts", StringComparison.OrdinalIgnoreCase)) + return ResolveAudioADTSFormat(bitrate, frequency, channels); + if (string.Equals(container, "flac", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.FLAC; + if (string.Equals(container, "oga", StringComparison.OrdinalIgnoreCase) || string.Equals(container, "ogg", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.OGG; + throw new ArgumentException("Unsupported container: " + container); + } + + private MediaFormatProfile ResolveAudioASFFormat(int? bitrate, int? frequency, int? channels) + { + if (bitrate.HasValue && bitrate.Value <= 193) + { + return MediaFormatProfile.WMA_BASE; + } + return MediaFormatProfile.WMA_FULL; + } + + private MediaFormatProfile ResolveAudioLPCMFormat(int? bitrate, int? frequency, int? channels) + { + if (frequency.HasValue && channels.HasValue) + { + if (frequency.Value == 44100 && channels.Value == 1) + { + return MediaFormatProfile.LPCM16_44_MONO; + } + if (frequency.Value == 44100 && channels.Value == 2) + { + return MediaFormatProfile.LPCM16_44_STEREO; + } + if (frequency.Value == 48000 && channels.Value == 1) + { + return MediaFormatProfile.LPCM16_48_MONO; + } + if (frequency.Value == 48000 && channels.Value == 1) + { + return MediaFormatProfile.LPCM16_48_STEREO; + } + + throw new ArgumentException("Unsupported LPCM format of file %s. Only 44100 / 48000 Hz and Mono / Stereo files are allowed."); + } + + return MediaFormatProfile.LPCM16_48_STEREO; + } + + private MediaFormatProfile ResolveAudioMP4Format(int? bitrate, int? frequency, int? channels) + { + if (bitrate.HasValue && bitrate.Value <= 320) + { + return MediaFormatProfile.AAC_ISO_320; + } + return MediaFormatProfile.AAC_ISO; + } + + private MediaFormatProfile ResolveAudioADTSFormat(int? bitrate, int? frequency, int? channels) + { + if (bitrate.HasValue && bitrate.Value <= 320) + { + return MediaFormatProfile.AAC_ADTS_320; + } + return MediaFormatProfile.AAC_ADTS; + } + + public MediaFormatProfile ResolveImageFormat(string container, int? width, int? height) + { + if (string.Equals(container, "jpeg", StringComparison.OrdinalIgnoreCase) || + string.Equals(container, "jpg", StringComparison.OrdinalIgnoreCase)) + return ResolveImageJPGFormat(width, height); + if (string.Equals(container, "png", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.PNG_LRG; + if (string.Equals(container, "gif", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.GIF_LRG; + if (string.Equals(container, "raw", StringComparison.OrdinalIgnoreCase)) + return MediaFormatProfile.RAW; + + throw new ArgumentException("Unsupported container: " + container); + } + + private MediaFormatProfile ResolveImageJPGFormat(int? width, int? height) + { + if (width.HasValue && height.HasValue) + { + if ((width.Value <= 640) && (height.Value <= 480)) + return MediaFormatProfile.JPEG_SM; + + if ((width.Value <= 1024) && (height.Value <= 768)) + { + return MediaFormatProfile.JPEG_MED; + } + } + + return MediaFormatProfile.JPEG_LRG; + } + } +} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 3960b49ae..d975b1c4b 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -81,7 +81,8 @@ namespace MediaBrowser.Model.Dlna { ItemId = options.ItemId, MediaType = DlnaProfileType.Audio, - MediaSourceId = item.Id + MediaSourceId = item.Id, + RunTimeTicks = item.RunTimeTicks }; var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); @@ -114,6 +115,7 @@ namespace MediaBrowser.Model.Dlna if (transcodingProfile != null) { playlistItem.IsDirectStream = false; + playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; playlistItem.Container = transcodingProfile.Container; playlistItem.AudioCodec = transcodingProfile.AudioCodec; @@ -150,7 +152,8 @@ namespace MediaBrowser.Model.Dlna { ItemId = options.ItemId, MediaType = DlnaProfileType.Video, - MediaSourceId = item.Id + MediaSourceId = item.Id, + RunTimeTicks = item.RunTimeTicks }; var audioStream = item.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); @@ -193,6 +196,7 @@ namespace MediaBrowser.Model.Dlna { playlistItem.IsDirectStream = false; playlistItem.Container = transcodingProfile.Container; + playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo; playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',').FirstOrDefault(); playlistItem.VideoCodec = transcodingProfile.VideoCodec; diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 209f33930..6ba7fe399 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -46,6 +46,10 @@ namespace MediaBrowser.Model.Dlna public string DeviceProfileId { get; set; } public string DeviceId { get; set; } + public long? RunTimeTicks { get; set; } + + public TranscodeSeekInfo TranscodeSeekInfo { get; set; } + public string ToUrl(string baseUrl) { return ToDlnaUrl(baseUrl); diff --git a/MediaBrowser.Model/Dto/MediaVersionInfo.cs b/MediaBrowser.Model/Dto/MediaVersionInfo.cs index e174c5d55..cfef83d2c 100644 --- a/MediaBrowser.Model/Dto/MediaVersionInfo.cs +++ b/MediaBrowser.Model/Dto/MediaVersionInfo.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Dto public string Path { get; set; } public string Container { get; set; } + public long? Size { get; set; } public LocationType LocationType { get; set; } @@ -25,6 +26,14 @@ namespace MediaBrowser.Model.Dto public List MediaStreams { get; set; } + public List Formats { get; set; } + public int? Bitrate { get; set; } + + public MediaSourceInfo() + { + Formats = new List(); + MediaStreams = new List(); + } } } diff --git a/MediaBrowser.Model/Entities/BaseItemInfo.cs b/MediaBrowser.Model/Entities/BaseItemInfo.cs index 5554e40d5..a280b1f71 100644 --- a/MediaBrowser.Model/Entities/BaseItemInfo.cs +++ b/MediaBrowser.Model/Entities/BaseItemInfo.cs @@ -52,6 +52,18 @@ namespace MediaBrowser.Model.Entities /// /// The primary image item identifier. public string PrimaryImageItemId { get; set; } + + /// + /// Gets or sets the logo image tag. + /// + /// The logo image tag. + public Guid? LogoImageTag { get; set; } + + /// + /// Gets or sets the logo item identifier. + /// + /// The logo item identifier. + public string LogoItemId { get; set; } /// /// Gets or sets the thumb image tag. diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index ec78a2b1b..5e373441f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -72,6 +72,9 @@ + + + diff --git a/MediaBrowser.Model/Session/PlaybackReports.cs b/MediaBrowser.Model/Session/PlaybackReports.cs index 24594fcb1..80524c06e 100644 --- a/MediaBrowser.Model/Session/PlaybackReports.cs +++ b/MediaBrowser.Model/Session/PlaybackReports.cs @@ -90,6 +90,19 @@ namespace MediaBrowser.Model.Session /// /// The volume level. public int? VolumeLevel { get; set; } + + /// + /// Gets or sets the play method. + /// + /// The play method. + public PlayMethod PlayMethod { get; set; } + } + + public enum PlayMethod + { + Transcode = 0, + DirectStream = 1, + DirectPlay = 2 } /// diff --git a/MediaBrowser.Model/Session/SessionInfoDto.cs b/MediaBrowser.Model/Session/SessionInfoDto.cs index b9dcf996e..686af4849 100644 --- a/MediaBrowser.Model/Session/SessionInfoDto.cs +++ b/MediaBrowser.Model/Session/SessionInfoDto.cs @@ -233,5 +233,11 @@ namespace MediaBrowser.Model.Session /// /// The now playing media version identifier. public string MediaSourceId { get; set; } + + /// + /// Gets or sets the play method. + /// + /// The play method. + public PlayMethod? PlayMethod { get; set; } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs index 75a9d9c36..2725d4e78 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs @@ -121,9 +121,27 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (data.format.tags != null) + if (data.format != null) { - FetchDataFromTags(audio, data.format.tags); + audio.FormatName = data.format.format_name; + + var extension = (Path.GetExtension(audio.Path) ?? string.Empty).TrimStart('.'); + + audio.Container = extension; + + if (!string.IsNullOrEmpty(data.format.size)) + { + audio.Size = long.Parse(data.format.size , _usCulture); + } + else + { + audio.Size = null; + } + + if (data.format.tags != null) + { + FetchDataFromTags(audio, data.format.tags); + } } return _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index cb326c5ad..5e48f24b3 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -18,6 +18,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; namespace MediaBrowser.Providers.MediaInfo { @@ -159,6 +160,29 @@ namespace MediaBrowser.Providers.MediaInfo { video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks; } + + video.FormatName = (data.format.format_name ?? string.Empty) + .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase); + + if (video.VideoType == VideoType.VideoFile) + { + var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.'); + + video.Container = extension; + } + else + { + video.Container = null; + } + + if (!string.IsNullOrEmpty(data.format.size)) + { + video.Size = long.Parse(data.format.size, _usCulture); + } + else + { + video.Size = null; + } } var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams; diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index b625c2d51..166d26b3c 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -1234,15 +1234,21 @@ namespace MediaBrowser.Server.Implementations.Dto Path = GetMappedPath(i), RunTimeTicks = i.RunTimeTicks, Video3DFormat = i.Video3DFormat, - VideoType = i.VideoType + VideoType = i.VideoType, + Container = i.Container, + Size = i.Size, + Formats = (i.FormatName ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() }; - if (i.VideoType == VideoType.VideoFile || i.VideoType == VideoType.Iso) + if (string.IsNullOrEmpty(info.Container)) { - var locationType = i.LocationType; - if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual) + if (i.VideoType == VideoType.VideoFile || i.VideoType == VideoType.Iso) { - info.Container = Path.GetExtension(i.Path).TrimStart('.'); + var locationType = i.LocationType; + if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual) + { + info.Container = Path.GetExtension(i.Path).TrimStart('.'); + } } } @@ -1265,13 +1271,19 @@ namespace MediaBrowser.Server.Implementations.Dto MediaStreams = _itemRepo.GetMediaStreams(new MediaStreamQuery { ItemId = i.Id }).ToList(), Name = i.Name, Path = GetMappedPath(i), - RunTimeTicks = i.RunTimeTicks + RunTimeTicks = i.RunTimeTicks, + Container = i.Container, + Size = i.Size, + Formats = (i.FormatName ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() }; - var locationType = i.LocationType; - if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual) + if (string.IsNullOrEmpty(info.Container)) { - info.Container = Path.GetExtension(i.Path).TrimStart('.'); + var locationType = i.LocationType; + if (!string.IsNullOrWhiteSpace(i.Path) && locationType != LocationType.Remote && locationType != LocationType.Virtual) + { + info.Container = Path.GetExtension(i.Path).TrimStart('.'); + } } var bitrate = info.MediaStreams.Where(m => m.Type == MediaStreamType.Audio).Select(m => m.BitRate ?? 0).Sum(); diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index 7237ffee5..470e58b66 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -158,15 +158,6 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder continue; } - if (video.VideoType == VideoType.BluRay) - { - // Can only extract reliably on single file blurays - if (video.PlayableStreamFileNames == null || video.PlayableStreamFileNames.Count != 1) - { - continue; - } - } - // Add some time for the first chapter to make sure we don't end up with a black image var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 2fc63039d..a05af246c 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -294,6 +294,7 @@ namespace MediaBrowser.Server.Implementations.Session session.PlayState.VolumeLevel = info.VolumeLevel; session.PlayState.AudioStreamIndex = info.AudioStreamIndex; session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex; + session.PlayState.PlayMethod = info.PlayMethod; } /// @@ -1253,8 +1254,8 @@ namespace MediaBrowser.Server.Implementations.Session } var backropItem = item.HasImage(ImageType.Backdrop) ? item : null; - var thumbItem = item.HasImage(ImageType.Thumb) ? item : null; + var logoItem = item.HasImage(ImageType.Logo) ? item : null; if (thumbItem == null) { @@ -1292,6 +1293,11 @@ namespace MediaBrowser.Server.Implementations.Session thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb)); } + if (logoItem == null) + { + logoItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Logo)); + } + if (thumbItem != null) { info.ThumbImageTag = GetImageCacheTag(thumbItem, ImageType.Thumb); @@ -1304,6 +1310,12 @@ namespace MediaBrowser.Server.Implementations.Session info.BackdropItemId = GetDtoId(backropItem); } + if (logoItem != null) + { + info.LogoImageTag = GetImageCacheTag(logoItem, ImageType.Logo); + info.LogoItemId = GetDtoId(logoItem); + } + return info; } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 39c32a856..27e73450c 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -506,7 +506,7 @@ namespace MediaBrowser.ServerApplication var appThemeManager = new AppThemeManager(ApplicationPaths, FileSystemManager, JsonSerializer, Logger); RegisterSingleInstance(appThemeManager); - var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("DLNA"), JsonSerializer, UserManager, LibraryManager); + var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("DLNA"), JsonSerializer, UserManager, LibraryManager, DtoService, ImageProcessor, UserDataManager); RegisterSingleInstance(dlnaManager); var collectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor);