DlnaController.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. using MediaBrowser.Common.Net;
  2. using MediaBrowser.Controller;
  3. using MediaBrowser.Controller.Dlna;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Entities.Audio;
  6. using MediaBrowser.Controller.Library;
  7. using MediaBrowser.Controller.Persistence;
  8. using MediaBrowser.Controller.Session;
  9. using MediaBrowser.Model.Entities;
  10. using MediaBrowser.Model.Logging;
  11. using MediaBrowser.Model.Session;
  12. using System;
  13. using System.Collections.Generic;
  14. using System.Linq;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. namespace MediaBrowser.Dlna.PlayTo
  18. {
  19. public class PlayToController : ISessionController, IDisposable
  20. {
  21. private Device _device;
  22. private BaseItem _currentItem;
  23. private readonly SessionInfo _session;
  24. private readonly ISessionManager _sessionManager;
  25. private readonly IItemRepository _itemRepository;
  26. private readonly ILibraryManager _libraryManager;
  27. private readonly INetworkManager _networkManager;
  28. private readonly ILogger _logger;
  29. private readonly IDlnaManager _dlnaManager;
  30. private readonly IUserManager _userManager;
  31. private readonly IServerApplicationHost _appHost;
  32. private bool _playbackStarted;
  33. private const int UpdateTimerIntervalMs = 1000;
  34. public bool SupportsMediaRemoteControl
  35. {
  36. get { return true; }
  37. }
  38. public bool IsSessionActive
  39. {
  40. get
  41. {
  42. if (_device == null || _device.UpdateTime == default(DateTime))
  43. return false;
  44. return DateTime.UtcNow <= _device.UpdateTime.AddSeconds(30);
  45. }
  46. }
  47. public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, INetworkManager networkManager, IDlnaManager dlnaManager, IUserManager userManager, IServerApplicationHost appHost)
  48. {
  49. _session = session;
  50. _itemRepository = itemRepository;
  51. _sessionManager = sessionManager;
  52. _libraryManager = libraryManager;
  53. _networkManager = networkManager;
  54. _dlnaManager = dlnaManager;
  55. _userManager = userManager;
  56. _appHost = appHost;
  57. _logger = logger;
  58. }
  59. public void Init(Device device)
  60. {
  61. _device = device;
  62. _device.PlaybackChanged += Device_PlaybackChanged;
  63. _device.CurrentIdChanged += Device_CurrentIdChanged;
  64. _device.Start();
  65. _updateTimer = new Timer(updateTimer_Elapsed, null, UpdateTimerIntervalMs, UpdateTimerIntervalMs);
  66. }
  67. #region Device EventHandlers & Update Timer
  68. Timer _updateTimer;
  69. async void Device_PlaybackChanged(object sender, TransportStateEventArgs e)
  70. {
  71. if (_currentItem == null)
  72. return;
  73. if (e.Stopped == false)
  74. await ReportProgress().ConfigureAwait(false);
  75. else if (e.Stopped && _playbackStarted)
  76. {
  77. _playbackStarted = false;
  78. await _sessionManager.OnPlaybackStopped(new Controller.Session.PlaybackStopInfo
  79. {
  80. Item = _currentItem,
  81. SessionId = _session.Id,
  82. PositionTicks = _device.Position.Ticks
  83. }).ConfigureAwait(false);
  84. await SetNext().ConfigureAwait(false);
  85. }
  86. }
  87. async void Device_CurrentIdChanged(object sender, CurrentIdEventArgs e)
  88. {
  89. if (!string.IsNullOrWhiteSpace(e.Id))
  90. {
  91. Guid guid;
  92. if (Guid.TryParse(e.Id, out guid))
  93. {
  94. if (_currentItem != null && _currentItem.Id == guid)
  95. {
  96. return;
  97. }
  98. var item = _libraryManager.GetItemById(guid);
  99. if (item != null)
  100. {
  101. _logger.Debug("{0} - CurrentId {1}", _session.DeviceName, item.Id);
  102. _currentItem = item;
  103. _playbackStarted = false;
  104. await ReportProgress().ConfigureAwait(false);
  105. }
  106. }
  107. }
  108. }
  109. /// <summary>
  110. /// Handles the Elapsed event of the updateTimer control.
  111. /// </summary>
  112. /// <param name="state">The state.</param>
  113. private async void updateTimer_Elapsed(object state)
  114. {
  115. if (_disposed)
  116. return;
  117. if (IsSessionActive)
  118. {
  119. await ReportProgress().ConfigureAwait(false);
  120. }
  121. else
  122. {
  123. _updateTimer.Change(Timeout.Infinite, Timeout.Infinite);
  124. try
  125. {
  126. // Session is inactive, mark it for Disposal and don't start the elapsed timer.
  127. await _sessionManager.ReportSessionEnded(_session.Id);
  128. }
  129. catch (Exception ex)
  130. {
  131. _logger.ErrorException("Error in ReportSessionEnded", ex);
  132. }
  133. }
  134. }
  135. /// <summary>
  136. /// Reports the playback progress.
  137. /// </summary>
  138. /// <returns></returns>
  139. private async Task ReportProgress()
  140. {
  141. if (_currentItem == null || _device.IsStopped)
  142. return;
  143. if (!_playbackStarted)
  144. {
  145. await _sessionManager.OnPlaybackStart(new PlaybackInfo
  146. {
  147. Item = _currentItem,
  148. SessionId = _session.Id,
  149. CanSeek = true,
  150. QueueableMediaTypes = new List<string> { _currentItem.MediaType }
  151. }).ConfigureAwait(false);
  152. _playbackStarted = true;
  153. }
  154. if ((_device.IsPlaying || _device.IsPaused))
  155. {
  156. var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1);
  157. if (playlistItem != null && playlistItem.Transcode)
  158. {
  159. await _sessionManager.OnPlaybackProgress(new Controller.Session.PlaybackProgressInfo
  160. {
  161. Item = _currentItem,
  162. SessionId = _session.Id,
  163. PositionTicks = _device.Position.Ticks + playlistItem.StartPositionTicks,
  164. IsMuted = _device.IsMuted,
  165. IsPaused = _device.IsPaused
  166. }).ConfigureAwait(false);
  167. }
  168. else if (_currentItem != null)
  169. {
  170. await _sessionManager.OnPlaybackProgress(new Controller.Session.PlaybackProgressInfo
  171. {
  172. Item = _currentItem,
  173. SessionId = _session.Id,
  174. PositionTicks = _device.Position.Ticks,
  175. IsMuted = _device.IsMuted,
  176. IsPaused = _device.IsPaused
  177. }).ConfigureAwait(false);
  178. }
  179. }
  180. }
  181. #endregion
  182. #region SendCommands
  183. public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
  184. {
  185. _logger.Debug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
  186. var items = new List<BaseItem>();
  187. foreach (string id in command.ItemIds)
  188. {
  189. AddItemFromId(Guid.Parse(id), items);
  190. }
  191. var playlist = new List<PlaylistItem>();
  192. var isFirst = true;
  193. var serverAddress = GetServerAddress();
  194. foreach (var item in items)
  195. {
  196. if (isFirst && command.StartPositionTicks.HasValue)
  197. {
  198. playlist.Add(CreatePlaylistItem(item, command.StartPositionTicks.Value, serverAddress));
  199. isFirst = false;
  200. }
  201. else
  202. {
  203. playlist.Add(CreatePlaylistItem(item, 0, serverAddress));
  204. }
  205. }
  206. _logger.Debug("{0} - Playlist created", _session.DeviceName);
  207. if (command.PlayCommand == PlayCommand.PlayLast)
  208. {
  209. AddItemsToPlaylist(playlist);
  210. }
  211. if (command.PlayCommand == PlayCommand.PlayNext)
  212. {
  213. AddItemsToPlaylist(playlist);
  214. }
  215. _logger.Debug("{0} - Playing {1} items", _session.DeviceName, playlist.Count);
  216. if (!string.IsNullOrWhiteSpace(command.ControllingUserId))
  217. {
  218. var userId = new Guid(command.ControllingUserId);
  219. var user = _userManager.GetUserById(userId);
  220. await _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
  221. _session.DeviceName, _session.RemoteEndPoint, user).ConfigureAwait(false);
  222. }
  223. await PlayItems(playlist).ConfigureAwait(false);
  224. }
  225. public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
  226. {
  227. switch (command.Command)
  228. {
  229. case PlaystateCommand.Stop:
  230. Playlist.Clear();
  231. return _device.SetStop();
  232. case PlaystateCommand.Pause:
  233. return _device.SetPause();
  234. case PlaystateCommand.Unpause:
  235. return _device.SetPlay();
  236. case PlaystateCommand.Seek:
  237. var playlistItem = Playlist.FirstOrDefault(p => p.PlayState == 1);
  238. if (playlistItem != null && playlistItem.Transcode && playlistItem.MediaType == DlnaProfileType.Video && _currentItem != null)
  239. {
  240. var newItem = CreatePlaylistItem(_currentItem, command.SeekPositionTicks ?? 0, GetServerAddress());
  241. playlistItem.StartPositionTicks = newItem.StartPositionTicks;
  242. playlistItem.StreamUrl = newItem.StreamUrl;
  243. playlistItem.Didl = newItem.Didl;
  244. return _device.SetAvTransport(playlistItem.StreamUrl, GetDlnaHeaders(playlistItem), playlistItem.Didl);
  245. }
  246. return _device.Seek(TimeSpan.FromTicks(command.SeekPositionTicks ?? 0));
  247. case PlaystateCommand.NextTrack:
  248. _currentItem = null;
  249. return SetNext();
  250. case PlaystateCommand.PreviousTrack:
  251. _currentItem = null;
  252. return SetPrevious();
  253. }
  254. return Task.FromResult(true);
  255. }
  256. public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
  257. {
  258. return Task.FromResult(true);
  259. }
  260. public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
  261. {
  262. return Task.FromResult(true);
  263. }
  264. public Task SendServerRestartNotification(CancellationToken cancellationToken)
  265. {
  266. return Task.FromResult(true);
  267. }
  268. public Task SendServerShutdownNotification(CancellationToken cancellationToken)
  269. {
  270. return Task.FromResult(true);
  271. }
  272. public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
  273. {
  274. return Task.FromResult(true);
  275. }
  276. public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
  277. {
  278. return Task.FromResult(true);
  279. }
  280. public Task SendMessageCommand(MessageCommand command, CancellationToken cancellationToken)
  281. {
  282. return Task.FromResult(true);
  283. }
  284. #endregion
  285. #region Playlist
  286. private List<PlaylistItem> _playlist = new List<PlaylistItem>();
  287. private List<PlaylistItem> Playlist
  288. {
  289. get
  290. {
  291. return _playlist;
  292. }
  293. set
  294. {
  295. _playlist = value;
  296. }
  297. }
  298. private void AddItemFromId(Guid id, List<BaseItem> list)
  299. {
  300. var item = _libraryManager.GetItemById(id);
  301. if (item.IsFolder)
  302. {
  303. foreach (var childId in _itemRepository.GetChildren(item.Id))
  304. {
  305. AddItemFromId(childId, list);
  306. }
  307. }
  308. else
  309. {
  310. if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
  311. {
  312. list.Add(item);
  313. }
  314. }
  315. }
  316. private string GetServerAddress()
  317. {
  318. return string.Format("{0}://{1}:{2}/mediabrowser",
  319. "http",
  320. _networkManager.GetLocalIpAddresses().FirstOrDefault() ?? "localhost",
  321. _appHost.HttpServerPort
  322. );
  323. }
  324. private PlaylistItem CreatePlaylistItem(BaseItem item, long startPostionTicks, string serverAddress)
  325. {
  326. var streams = _itemRepository.GetMediaStreams(new MediaStreamQuery
  327. {
  328. ItemId = item.Id
  329. }).ToList();
  330. var deviceInfo = _device.Properties;
  331. var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
  332. _dlnaManager.GetDefaultProfile();
  333. var playlistItem = GetPlaylistItem(item, streams, profile);
  334. playlistItem.StartPositionTicks = startPostionTicks;
  335. playlistItem.DeviceProfileId = profile.Id;
  336. if (playlistItem.MediaType == DlnaProfileType.Audio)
  337. {
  338. playlistItem.StreamUrl = StreamHelper.GetAudioUrl(deviceInfo, playlistItem, streams, serverAddress);
  339. }
  340. else
  341. {
  342. playlistItem.StreamUrl = StreamHelper.GetVideoUrl(_device.Properties, playlistItem, streams, serverAddress);
  343. }
  344. playlistItem.Didl = DidlBuilder.Build(item, _session.UserId.ToString(), serverAddress, playlistItem.StreamUrl, streams, profile.EnableAlbumArtInDidl);
  345. return playlistItem;
  346. }
  347. private string GetDlnaHeaders(PlaylistItem item)
  348. {
  349. var orgOp = item.Transcode ? ";DLNA.ORG_OP=00" : ";DLNA.ORG_OP=01";
  350. var orgCi = item.Transcode ? ";DLNA.ORG_CI=0" : ";DLNA.ORG_CI=1";
  351. const string dlnaflags = ";DLNA.ORG_FLAGS=01500000000000000000000000000000";
  352. string contentFeatures;
  353. var container = item.Container.TrimStart('.');
  354. if (string.Equals(container, "mp3", StringComparison.OrdinalIgnoreCase))
  355. {
  356. contentFeatures = "DLNA.ORG_PN=MP3";
  357. }
  358. else if (string.Equals(container, "wma", StringComparison.OrdinalIgnoreCase))
  359. {
  360. contentFeatures = "DLNA.ORG_PN=WMABASE";
  361. }
  362. else if (string.Equals(container, "wmw", StringComparison.OrdinalIgnoreCase))
  363. {
  364. contentFeatures = "DLNA.ORG_PN=WMVMED_BASE";
  365. }
  366. else if (string.Equals(container, "asf", StringComparison.OrdinalIgnoreCase))
  367. {
  368. contentFeatures = "DLNA.ORG_PN=WMVMED_BASE";
  369. }
  370. else if (string.Equals(container, "avi", StringComparison.OrdinalIgnoreCase))
  371. {
  372. contentFeatures = "DLNA.ORG_PN=AVI";
  373. }
  374. else if (string.Equals(container, "mkv", StringComparison.OrdinalIgnoreCase))
  375. {
  376. contentFeatures = "DLNA.ORG_PN=MATROSKA";
  377. }
  378. else if (string.Equals(container, "mp4", StringComparison.OrdinalIgnoreCase))
  379. {
  380. contentFeatures = "DLNA.ORG_PN=AVC_MP4_MP_HD_720p_AAC";
  381. }
  382. else if (string.Equals(container, "mpeg", StringComparison.OrdinalIgnoreCase))
  383. {
  384. contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL";
  385. }
  386. else if (string.Equals(container, "ts", StringComparison.OrdinalIgnoreCase))
  387. {
  388. contentFeatures = "DLNA.ORG_PN=MPEG_PS_PAL";
  389. }
  390. else if (item.MediaType == DlnaProfileType.Video)
  391. {
  392. // Default to AVI for video
  393. contentFeatures = "DLNA.ORG_PN=AVI";
  394. }
  395. else
  396. {
  397. // Default to MP3 for audio
  398. contentFeatures = "DLNA.ORG_PN=MP3";
  399. }
  400. return (contentFeatures + orgOp + orgCi + dlnaflags).Trim(';');
  401. }
  402. private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaStream> mediaStreams, DeviceProfile profile)
  403. {
  404. var video = item as Video;
  405. if (video != null)
  406. {
  407. return new PlaylistItemFactory().Create(video, mediaStreams, profile);
  408. }
  409. var audio = item as Audio;
  410. if (audio != null)
  411. {
  412. return new PlaylistItemFactory().Create(audio, mediaStreams, profile);
  413. }
  414. var photo = item as Photo;
  415. if (photo != null)
  416. {
  417. return new PlaylistItemFactory().Create(photo, profile);
  418. }
  419. throw new ArgumentException("Unrecognized item type.");
  420. }
  421. /// <summary>
  422. /// Plays the items.
  423. /// </summary>
  424. /// <param name="items">The items.</param>
  425. /// <returns></returns>
  426. private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items)
  427. {
  428. Playlist.Clear();
  429. Playlist.AddRange(items);
  430. await SetNext();
  431. return true;
  432. }
  433. /// <summary>
  434. /// Adds the items to playlist.
  435. /// </summary>
  436. /// <param name="items">The items.</param>
  437. private void AddItemsToPlaylist(IEnumerable<PlaylistItem> items)
  438. {
  439. Playlist.AddRange(items);
  440. }
  441. private async Task<bool> SetNext()
  442. {
  443. if (!Playlist.Any() || Playlist.All(i => i.PlayState != 0))
  444. {
  445. return true;
  446. }
  447. var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1);
  448. if (currentitem != null)
  449. {
  450. currentitem.PlayState = 2;
  451. }
  452. var nextTrack = Playlist.FirstOrDefault(i => i.PlayState == 0);
  453. if (nextTrack == null)
  454. {
  455. await _device.SetStop();
  456. return true;
  457. }
  458. nextTrack.PlayState = 1;
  459. var dlnaheaders = GetDlnaHeaders(nextTrack);
  460. _logger.Debug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", _device.Properties.Name, nextTrack.StreamUrl, dlnaheaders);
  461. await _device.SetAvTransport(nextTrack.StreamUrl, dlnaheaders, nextTrack.Didl);
  462. if (nextTrack.StartPositionTicks > 0 && !nextTrack.Transcode)
  463. await _device.Seek(TimeSpan.FromTicks(nextTrack.StartPositionTicks));
  464. return true;
  465. }
  466. public Task<bool> SetPrevious()
  467. {
  468. if (!Playlist.Any() || Playlist.All(i => i.PlayState != 2))
  469. return Task.FromResult(false);
  470. var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1);
  471. var prevTrack = Playlist.LastOrDefault(i => i.PlayState == 2);
  472. if (currentitem != null)
  473. {
  474. currentitem.PlayState = 0;
  475. }
  476. if (prevTrack == null)
  477. return Task.FromResult(false);
  478. prevTrack.PlayState = 1;
  479. return _device.SetAvTransport(prevTrack.StreamUrl, GetDlnaHeaders(prevTrack), prevTrack.Didl);
  480. }
  481. #endregion
  482. private bool _disposed;
  483. public void Dispose()
  484. {
  485. if (!_disposed)
  486. {
  487. _disposed = true;
  488. _updateTimer.Dispose();
  489. _device.Dispose();
  490. _logger.Log(LogSeverity.Debug, "Controller disposed");
  491. }
  492. }
  493. public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
  494. {
  495. GeneralCommandType commandType;
  496. if (!Enum.TryParse(command.Name, true, out commandType))
  497. {
  498. switch (commandType)
  499. {
  500. case GeneralCommandType.VolumeDown:
  501. return _device.VolumeDown();
  502. case GeneralCommandType.VolumeUp:
  503. return _device.VolumeUp();
  504. case GeneralCommandType.Mute:
  505. return _device.VolumeDown(true);
  506. case GeneralCommandType.Unmute:
  507. return _device.VolumeUp(true);
  508. case GeneralCommandType.ToggleMute:
  509. return _device.ToggleMute();
  510. default:
  511. return Task.FromResult(true);
  512. }
  513. }
  514. return Task.FromResult(true);
  515. }
  516. }
  517. }