diff options
Diffstat (limited to 'MediaBrowser.Dlna/PlayTo/PlayToController.cs')
| -rw-r--r-- | MediaBrowser.Dlna/PlayTo/PlayToController.cs | 650 |
1 files changed, 650 insertions, 0 deletions
diff --git a/MediaBrowser.Dlna/PlayTo/PlayToController.cs b/MediaBrowser.Dlna/PlayTo/PlayToController.cs new file mode 100644 index 0000000000..ab342d635e --- /dev/null +++ b/MediaBrowser.Dlna/PlayTo/PlayToController.cs @@ -0,0 +1,650 @@ +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +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.Dlna.Didl; +using MediaBrowser.Dlna.Ssdp; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Session; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Dlna.PlayTo +{ + public class PlayToController : ISessionController, IDisposable + { + private Device _device; + private readonly SessionInfo _session; + private readonly ISessionManager _sessionManager; + private readonly IItemRepository _itemRepository; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IDlnaManager _dlnaManager; + private readonly IUserManager _userManager; + private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; + + private readonly SsdpHandler _ssdpHandler; + private readonly string _serverAddress; + + public bool SupportsMediaRemoteControl + { + get { return true; } + } + + public bool IsSessionActive + { + get + { + return _device != null; + } + } + + private Timer _updateTimer; + + public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IDtoService dtoService, IImageProcessor imageProcessor, SsdpHandler ssdpHandler, string serverAddress) + { + _session = session; + _itemRepository = itemRepository; + _sessionManager = sessionManager; + _libraryManager = libraryManager; + _dlnaManager = dlnaManager; + _userManager = userManager; + _dtoService = dtoService; + _imageProcessor = imageProcessor; + _ssdpHandler = ssdpHandler; + _serverAddress = serverAddress; + _logger = logger; + } + + public void Init(Device device) + { + _device = device; + _device.PlaybackStart += _device_PlaybackStart; + _device.PlaybackProgress += _device_PlaybackProgress; + _device.PlaybackStopped += _device_PlaybackStopped; + _device.Start(); + + _ssdpHandler.MessageReceived += _SsdpHandler_MessageReceived; + + _updateTimer = new Timer(updateTimer_Elapsed, null, 60000, 60000); + } + + private async void updateTimer_Elapsed(object state) + { + if (DateTime.UtcNow >= _device.DateLastActivity.AddSeconds(60)) + { + try + { + // Session is inactive, mark it for Disposal and don't start the elapsed timer. + await _sessionManager.ReportSessionEnded(_session.Id).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ReportSessionEnded", ex); + } + } + } + + private string GetServerAddress() + { + return _serverAddress; + } + + async void _SsdpHandler_MessageReceived(object sender, SsdpMessageEventArgs e) + { + string nts; + e.Headers.TryGetValue("NTS", out nts); + + string usn; + if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + + string nt; + if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + + if (string.Equals(e.Method, "NOTIFY", StringComparison.OrdinalIgnoreCase) && + string.Equals(nts, "ssdp:byebye", StringComparison.OrdinalIgnoreCase) && + usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 && + !_disposed) + { + if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 || + nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1) + { + try + { + await _sessionManager.ReportSessionEnded(_session.Id).ConfigureAwait(false); + } + catch + { + // Could throw if the session is already gone + } + } + } + } + + async void _device_PlaybackStopped(object sender, PlaybackStoppedEventArgs e) + { + try + { + await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo + { + ItemId = e.MediaInfo.Id, + SessionId = _session.Id, + PositionTicks = _device.Position.Ticks + + }).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting progress", ex); + } + + await SetNext().ConfigureAwait(false); + } + + async void _device_PlaybackStart(object sender, PlaybackStartEventArgs e) + { + var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1); + + if (playlistItem != null) + { + var streamInfo = playlistItem.StreamInfo; + + var info = GetProgressInfo(streamInfo, e.MediaInfo); + + try + { + await _sessionManager.OnPlaybackStart(info).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting progress", ex); + } + } + } + + async void _device_PlaybackProgress(object sender, PlaybackProgressEventArgs e) + { + var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1); + + if (playlistItem != null) + { + var streamInfo = playlistItem.StreamInfo; + + var info = GetProgressInfo(streamInfo, e.MediaInfo); + + try + { + await _sessionManager.OnPlaybackProgress(info).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error reporting progress", ex); + } + } + } + + private PlaybackStartInfo GetProgressInfo(StreamInfo streamInfo, uBaseObject mediaInfo) + { + var ticks = _device.Position.Ticks; + + if (!streamInfo.IsDirectStream) + { + ticks += streamInfo.StartPositionTicks; + } + + return new PlaybackStartInfo + { + ItemId = mediaInfo.Id, + SessionId = _session.Id, + PositionTicks = ticks, + IsMuted = _device.IsMuted, + IsPaused = _device.IsPaused, + MediaSourceId = streamInfo.MediaSourceId, + AudioStreamIndex = streamInfo.AudioStreamIndex, + SubtitleStreamIndex = streamInfo.SubtitleStreamIndex, + VolumeLevel = _device.Volume, + CanSeek = streamInfo.RunTimeTicks.HasValue, + PlayMethod = streamInfo.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode, + QueueableMediaTypes = new List<string> { mediaInfo.MediaType } + }; + } + + #region SendCommands + + public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + { + _logger.Debug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand); + + var items = new List<BaseItem>(); + foreach (string id in command.ItemIds) + { + AddItemFromId(Guid.Parse(id), items); + } + + var playlist = new List<PlaylistItem>(); + var isFirst = true; + + var serverAddress = GetServerAddress(); + + foreach (var item in items) + { + if (isFirst && command.StartPositionTicks.HasValue) + { + playlist.Add(CreatePlaylistItem(item, command.StartPositionTicks.Value, serverAddress)); + isFirst = false; + } + else + { + playlist.Add(CreatePlaylistItem(item, 0, serverAddress)); + } + } + + _logger.Debug("{0} - Playlist created", _session.DeviceName); + + if (command.PlayCommand == PlayCommand.PlayLast) + { + Playlist.AddRange(playlist); + } + if (command.PlayCommand == PlayCommand.PlayNext) + { + Playlist.AddRange(playlist); + } + + _logger.Debug("{0} - Playing {1} items", _session.DeviceName, playlist.Count); + + if (!string.IsNullOrWhiteSpace(command.ControllingUserId)) + { + var userId = new Guid(command.ControllingUserId); + + var user = _userManager.GetUserById(userId); + + await _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId, + _session.DeviceName, _session.RemoteEndPoint, user).ConfigureAwait(false); + } + + await PlayItems(playlist).ConfigureAwait(false); + } + + public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) + { + switch (command.Command) + { + case PlaystateCommand.Stop: + Playlist.Clear(); + return _device.SetStop(); + + case PlaystateCommand.Pause: + return _device.SetPause(); + + case PlaystateCommand.Unpause: + return _device.SetPlay(); + + case PlaystateCommand.Seek: + //var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1); + //if (playlistItem != null && playlistItem.Transcode && _currentItem != null) + //{ + // var newItem = CreatePlaylistItem(_currentItem, command.SeekPositionTicks ?? 0, GetServerAddress()); + // playlistItem.StartPositionTicks = newItem.StartPositionTicks; + // playlistItem.StreamUrl = newItem.StreamUrl; + // playlistItem.Didl = newItem.Didl; + // return _device.SetAvTransport(playlistItem.StreamUrl, GetDlnaHeaders(playlistItem), playlistItem.Didl); + + //} + return _device.Seek(TimeSpan.FromTicks(command.SeekPositionTicks ?? 0)); + + + case PlaystateCommand.NextTrack: + return SetNext(); + + case PlaystateCommand.PreviousTrack: + return SetPrevious(); + } + + return Task.FromResult(true); + } + + public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendRestartRequiredNotification(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendServerRestartNotification(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendServerShutdownNotification(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken) + { + return Task.FromResult(true); + } + + #endregion + + #region Playlist + + private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); + private List<PlaylistItem> Playlist + { + get + { + return _playlist; + } + } + + private void AddItemFromId(Guid id, List<BaseItem> list) + { + var item = _libraryManager.GetItemById(id); + if (item.IsFolder) + { + foreach (var childId in _itemRepository.GetChildren(item.Id)) + { + AddItemFromId(childId, list); + } + } + else + { + if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video) + { + list.Add(item); + } + } + } + + private PlaylistItem CreatePlaylistItem(BaseItem item, long startPostionTicks, string serverAddress) + { + var deviceInfo = _device.Properties; + + var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ?? + _dlnaManager.GetDefaultProfile(); + + var mediaSources = item is Audio || item is Video + ? _dtoService.GetMediaSources(item) + : new List<MediaSourceInfo>(); + + var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId); + playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; + + playlistItem.StreamUrl = playlistItem.StreamInfo.ToUrl(serverAddress); + + var itemXml = + new DidlBuilder(profile, _imageProcessor, serverAddress, _dtoService).GetItemDidl(item, _session.DeviceId, + new Filter()); + + playlistItem.Didl = itemXml; + + return playlistItem; + } + + private string GetDlnaHeaders(PlaylistItem item) + { + var profile = item.Profile; + var streamInfo = item.StreamInfo; + + if (streamInfo.MediaType == DlnaProfileType.Audio) + { + return new ContentFeatureBuilder(profile) + .BuildAudioHeader(streamInfo.Container, + streamInfo.AudioCodec, + streamInfo.TargetAudioBitrate, + streamInfo.TargetAudioSampleRate, + streamInfo.TargetAudioChannels, + streamInfo.IsDirectStream, + streamInfo.RunTimeTicks, + streamInfo.TranscodeSeekInfo); + } + + if (streamInfo.MediaType == DlnaProfileType.Video) + { + return new ContentFeatureBuilder(profile) + .BuildVideoHeader(streamInfo.Container, + streamInfo.VideoCodec, + streamInfo.AudioCodec, + streamInfo.TargetWidth, + streamInfo.TargetHeight, + streamInfo.TargetVideoBitDepth, + streamInfo.TargetVideoBitrate, + streamInfo.TargetAudioChannels, + streamInfo.TargetAudioBitrate, + streamInfo.TargetTimestamp, + streamInfo.IsDirectStream, + streamInfo.RunTimeTicks, + streamInfo.TargetVideoProfile, + streamInfo.TargetVideoLevel, + streamInfo.TargetFramerate, + streamInfo.TargetPacketLength, + streamInfo.TranscodeSeekInfo); + } + + return null; + } + + private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId) + { + var video = item as Video; + + if (video != null) + { + return new PlaylistItem + { + StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions + { + ItemId = item.Id.ToString("N"), + MediaSources = mediaSources, + Profile = profile, + DeviceId = deviceId + }), + + Profile = profile + }; + } + + var audio = item as Audio; + + if (audio != null) + { + return new PlaylistItem + { + StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions + { + ItemId = item.Id.ToString("N"), + MediaSources = mediaSources, + Profile = profile, + DeviceId = deviceId + }), + + Profile = profile + }; + } + + var photo = item as Photo; + + if (photo != null) + { + return new PlaylistItemFactory().Create(photo, profile); + } + + throw new ArgumentException("Unrecognized item type."); + } + + /// <summary> + /// Plays the items. + /// </summary> + /// <param name="items">The items.</param> + /// <returns></returns> + private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items) + { + Playlist.Clear(); + Playlist.AddRange(items); + await SetNext(); + return true; + } + + private async Task SetNext() + { + if (!Playlist.Any() || Playlist.All(i => i.PlayState != 0)) + { + return; + } + + var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1); + + if (currentitem != null) + { + currentitem.PlayState = 2; + } + + var nextTrack = Playlist.FirstOrDefault(i => i.PlayState == 0); + if (nextTrack == null) + { + await _device.SetStop(); + return; + } + + nextTrack.PlayState = 1; + + var dlnaheaders = GetDlnaHeaders(nextTrack); + + _logger.Debug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", _device.Properties.Name, nextTrack.StreamUrl, dlnaheaders); + + await _device.SetAvTransport(nextTrack.StreamUrl, dlnaheaders, nextTrack.Didl); + + var streamInfo = nextTrack.StreamInfo; + if (streamInfo.StartPositionTicks > 0 && streamInfo.IsDirectStream) + await _device.Seek(TimeSpan.FromTicks(streamInfo.StartPositionTicks)); + } + + public Task SetPrevious() + { + if (!Playlist.Any() || Playlist.All(i => i.PlayState != 2)) + return Task.FromResult(false); + + var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1); + + var prevTrack = Playlist.LastOrDefault(i => i.PlayState == 2); + + if (currentitem != null) + { + currentitem.PlayState = 0; + } + + if (prevTrack == null) + return Task.FromResult(false); + + prevTrack.PlayState = 1; + return _device.SetAvTransport(prevTrack.StreamInfo.ToDlnaUrl(GetServerAddress()), GetDlnaHeaders(prevTrack), prevTrack.Didl); + } + + #endregion + + private bool _disposed; + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + _device.PlaybackStart -= _device_PlaybackStart; + _device.PlaybackProgress -= _device_PlaybackProgress; + _device.PlaybackStopped -= _device_PlaybackStopped; + _ssdpHandler.MessageReceived -= _SsdpHandler_MessageReceived; + + DisposeUpdateTimer(); + + _device.Dispose(); + } + } + + private void DisposeUpdateTimer() + { + if (_updateTimer != null) + { + _updateTimer.Dispose(); + _updateTimer = null; + } + } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) + { + GeneralCommandType commandType; + + if (Enum.TryParse(command.Name, true, out commandType)) + { + switch (commandType) + { + case GeneralCommandType.VolumeDown: + return _device.VolumeDown(); + case GeneralCommandType.VolumeUp: + return _device.VolumeUp(); + case GeneralCommandType.Mute: + return _device.Mute(); + case GeneralCommandType.Unmute: + return _device.Unmute(); + case GeneralCommandType.ToggleMute: + return _device.ToggleMute(); + case GeneralCommandType.SetVolume: + { + string volumeArg; + + if (command.Arguments.TryGetValue("Volume", out volumeArg)) + { + int volume; + + if (int.TryParse(volumeArg, NumberStyles.Any, _usCulture, out volume)) + { + return _device.SetVolume(volume); + } + + throw new ArgumentException("Unsupported volume value supplied."); + } + + throw new ArgumentException("Volume argument cannot be null"); + } + default: + return Task.FromResult(true); + } + } + + return Task.FromResult(true); + } + } +} |
