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; using MediaBrowser.Model.System; 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 IsSessionActive { get { return _device != null; } } public bool SupportsMediaControl { get { return IsSessionActive; } } 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. _sessionManager.ReportSessionEnded(_session.Id); } 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 { _sessionManager.ReportSessionEnded(_session.Id); } 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 { 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(); foreach (string id in command.ItemIds) { AddItemFromId(Guid.Parse(id), items); } var playlist = new List(); 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(SystemInfo info, 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 _playlist = new List(); private List Playlist { get { return _playlist; } } private void AddItemFromId(Guid id, List 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(); 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 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."); } /// /// Plays the items. /// /// The items. /// private async Task PlayItems(IEnumerable 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); } } }