PlayToController.cs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. #nullable disable
  2. #pragma warning disable CS1591
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Globalization;
  6. using System.Linq;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Emby.Dlna.Didl;
  10. using Jellyfin.Data.Entities;
  11. using Jellyfin.Data.Events;
  12. using MediaBrowser.Controller.Dlna;
  13. using MediaBrowser.Controller.Drawing;
  14. using MediaBrowser.Controller.Entities;
  15. using MediaBrowser.Controller.Library;
  16. using MediaBrowser.Controller.MediaEncoding;
  17. using MediaBrowser.Controller.Session;
  18. using MediaBrowser.Model.Dlna;
  19. using MediaBrowser.Model.Dto;
  20. using MediaBrowser.Model.Entities;
  21. using MediaBrowser.Model.Globalization;
  22. using MediaBrowser.Model.Session;
  23. using Microsoft.AspNetCore.WebUtilities;
  24. using Microsoft.Extensions.Logging;
  25. using Photo = MediaBrowser.Controller.Entities.Photo;
  26. namespace Emby.Dlna.PlayTo
  27. {
  28. public class PlayToController : ISessionController, IDisposable
  29. {
  30. private readonly SessionInfo _session;
  31. private readonly ISessionManager _sessionManager;
  32. private readonly ILibraryManager _libraryManager;
  33. private readonly ILogger _logger;
  34. private readonly IDlnaManager _dlnaManager;
  35. private readonly IUserManager _userManager;
  36. private readonly IImageProcessor _imageProcessor;
  37. private readonly IUserDataManager _userDataManager;
  38. private readonly ILocalizationManager _localization;
  39. private readonly IMediaSourceManager _mediaSourceManager;
  40. private readonly IMediaEncoder _mediaEncoder;
  41. private readonly IDeviceDiscovery _deviceDiscovery;
  42. private readonly string _serverAddress;
  43. private readonly string _accessToken;
  44. private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
  45. private Device _device;
  46. private int _currentPlaylistIndex;
  47. private bool _disposed;
  48. public PlayToController(
  49. SessionInfo session,
  50. ISessionManager sessionManager,
  51. ILibraryManager libraryManager,
  52. ILogger logger,
  53. IDlnaManager dlnaManager,
  54. IUserManager userManager,
  55. IImageProcessor imageProcessor,
  56. string serverAddress,
  57. string accessToken,
  58. IDeviceDiscovery deviceDiscovery,
  59. IUserDataManager userDataManager,
  60. ILocalizationManager localization,
  61. IMediaSourceManager mediaSourceManager,
  62. IMediaEncoder mediaEncoder)
  63. {
  64. _session = session;
  65. _sessionManager = sessionManager;
  66. _libraryManager = libraryManager;
  67. _logger = logger;
  68. _dlnaManager = dlnaManager;
  69. _userManager = userManager;
  70. _imageProcessor = imageProcessor;
  71. _serverAddress = serverAddress;
  72. _accessToken = accessToken;
  73. _deviceDiscovery = deviceDiscovery;
  74. _userDataManager = userDataManager;
  75. _localization = localization;
  76. _mediaSourceManager = mediaSourceManager;
  77. _mediaEncoder = mediaEncoder;
  78. }
  79. public bool IsSessionActive => !_disposed && _device != null;
  80. public bool SupportsMediaControl => IsSessionActive;
  81. public void Init(Device device)
  82. {
  83. _device = device;
  84. _device.OnDeviceUnavailable = OnDeviceUnavailable;
  85. _device.PlaybackStart += OnDevicePlaybackStart;
  86. _device.PlaybackProgress += OnDevicePlaybackProgress;
  87. _device.PlaybackStopped += OnDevicePlaybackStopped;
  88. _device.MediaChanged += OnDeviceMediaChanged;
  89. _device.Start();
  90. _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
  91. }
  92. /*
  93. * Send a message to the DLNA device to notify what is the next track in the playlist.
  94. */
  95. private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
  96. {
  97. if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
  98. {
  99. // The current playing item is indeed in the play list and we are not yet at the end of the playlist.
  100. var nextItemIndex = currentPlayListItemIndex + 1;
  101. var nextItem = _playlist[nextItemIndex];
  102. // Send the SetNextAvTransport message.
  103. await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
  104. }
  105. }
  106. private void OnDeviceUnavailable()
  107. {
  108. try
  109. {
  110. _sessionManager.ReportSessionEnded(_session.Id);
  111. }
  112. catch (Exception ex)
  113. {
  114. // Could throw if the session is already gone
  115. _logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
  116. }
  117. }
  118. private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
  119. {
  120. var info = e.Argument;
  121. if (!_disposed
  122. && info.Headers.TryGetValue("USN", out string usn)
  123. && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
  124. && (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
  125. || (info.Headers.TryGetValue("NT", out string nt)
  126. && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
  127. {
  128. OnDeviceUnavailable();
  129. }
  130. }
  131. private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
  132. {
  133. if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
  134. {
  135. return;
  136. }
  137. try
  138. {
  139. var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
  140. if (streamInfo.Item != null)
  141. {
  142. var positionTicks = GetProgressPositionTicks(streamInfo);
  143. await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
  144. }
  145. streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
  146. if (streamInfo.Item == null)
  147. {
  148. return;
  149. }
  150. var newItemProgress = GetProgressInfo(streamInfo);
  151. await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
  152. // Send a message to the DLNA device to notify what is the next track in the playlist.
  153. var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId));
  154. if (currentItemIndex >= 0)
  155. {
  156. _currentPlaylistIndex = currentItemIndex;
  157. }
  158. await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
  159. }
  160. catch (Exception ex)
  161. {
  162. _logger.LogError(ex, "Error reporting progress");
  163. }
  164. }
  165. private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
  166. {
  167. if (_disposed)
  168. {
  169. return;
  170. }
  171. try
  172. {
  173. var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
  174. if (streamInfo.Item == null)
  175. {
  176. return;
  177. }
  178. var positionTicks = GetProgressPositionTicks(streamInfo);
  179. await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
  180. var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
  181. var duration = mediaSource == null
  182. ? _device.Duration?.Ticks
  183. : mediaSource.RunTimeTicks;
  184. var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
  185. if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
  186. {
  187. double percent = positionTicks.Value;
  188. percent /= duration.Value;
  189. playedToCompletion = Math.Abs(1 - percent) <= .1;
  190. }
  191. if (playedToCompletion)
  192. {
  193. await SetPlaylistIndex(_currentPlaylistIndex + 1).ConfigureAwait(false);
  194. }
  195. else
  196. {
  197. _playlist.Clear();
  198. }
  199. }
  200. catch (Exception ex)
  201. {
  202. _logger.LogError(ex, "Error reporting playback stopped");
  203. }
  204. }
  205. private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
  206. {
  207. try
  208. {
  209. await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo
  210. {
  211. ItemId = streamInfo.ItemId,
  212. SessionId = _session.Id,
  213. PositionTicks = positionTicks,
  214. MediaSourceId = streamInfo.MediaSourceId
  215. }).ConfigureAwait(false);
  216. }
  217. catch (Exception ex)
  218. {
  219. _logger.LogError(ex, "Error reporting progress");
  220. }
  221. }
  222. private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
  223. {
  224. if (_disposed)
  225. {
  226. return;
  227. }
  228. try
  229. {
  230. var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
  231. if (info.Item != null)
  232. {
  233. var progress = GetProgressInfo(info);
  234. await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
  235. }
  236. }
  237. catch (Exception ex)
  238. {
  239. _logger.LogError(ex, "Error reporting progress");
  240. }
  241. }
  242. private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
  243. {
  244. if (_disposed)
  245. {
  246. return;
  247. }
  248. try
  249. {
  250. var mediaUrl = e.MediaInfo.Url;
  251. if (string.IsNullOrWhiteSpace(mediaUrl))
  252. {
  253. return;
  254. }
  255. var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager);
  256. if (info.Item != null)
  257. {
  258. var progress = GetProgressInfo(info);
  259. await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
  260. }
  261. }
  262. catch (Exception ex)
  263. {
  264. _logger.LogError(ex, "Error reporting progress");
  265. }
  266. }
  267. private long? GetProgressPositionTicks(StreamParams info)
  268. {
  269. var ticks = _device.Position.Ticks;
  270. if (!EnableClientSideSeek(info))
  271. {
  272. ticks += info.StartPositionTicks;
  273. }
  274. return ticks;
  275. }
  276. private PlaybackStartInfo GetProgressInfo(StreamParams info)
  277. {
  278. return new PlaybackStartInfo
  279. {
  280. ItemId = info.ItemId,
  281. SessionId = _session.Id,
  282. PositionTicks = GetProgressPositionTicks(info),
  283. IsMuted = _device.IsMuted,
  284. IsPaused = _device.IsPaused,
  285. MediaSourceId = info.MediaSourceId,
  286. AudioStreamIndex = info.AudioStreamIndex,
  287. SubtitleStreamIndex = info.SubtitleStreamIndex,
  288. VolumeLevel = _device.Volume,
  289. // TODO
  290. CanSeek = true,
  291. PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
  292. };
  293. }
  294. public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
  295. {
  296. _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
  297. var user = command.ControllingUserId.Equals(default)
  298. ? null :
  299. _userManager.GetUserById(command.ControllingUserId);
  300. var items = new List<BaseItem>();
  301. foreach (var id in command.ItemIds)
  302. {
  303. AddItemFromId(id, items);
  304. }
  305. var startIndex = command.StartIndex ?? 0;
  306. int len = items.Count - startIndex;
  307. if (startIndex > 0)
  308. {
  309. items = items.GetRange(startIndex, len);
  310. }
  311. var playlist = new PlaylistItem[len];
  312. // Not nullable enabled - so this is required.
  313. playlist[0] = CreatePlaylistItem(
  314. items[0],
  315. user,
  316. command.StartPositionTicks ?? 0,
  317. command.MediaSourceId ?? string.Empty,
  318. command.AudioStreamIndex,
  319. command.SubtitleStreamIndex);
  320. for (int i = 1; i < len; i++)
  321. {
  322. playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
  323. }
  324. _logger.LogDebug("{0} - Playlist created", _session.DeviceName);
  325. if (command.PlayCommand == PlayCommand.PlayLast)
  326. {
  327. _playlist.AddRange(playlist);
  328. }
  329. if (command.PlayCommand == PlayCommand.PlayNext)
  330. {
  331. _playlist.AddRange(playlist);
  332. }
  333. if (!command.ControllingUserId.Equals(default))
  334. {
  335. _sessionManager.LogSessionActivity(
  336. _session.Client,
  337. _session.ApplicationVersion,
  338. _session.DeviceId,
  339. _session.DeviceName,
  340. _session.RemoteEndPoint,
  341. user);
  342. }
  343. return PlayItems(playlist, cancellationToken);
  344. }
  345. private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
  346. {
  347. switch (command.Command)
  348. {
  349. case PlaystateCommand.Stop:
  350. _playlist.Clear();
  351. return _device.SetStop(CancellationToken.None);
  352. case PlaystateCommand.Pause:
  353. return _device.SetPause(CancellationToken.None);
  354. case PlaystateCommand.Unpause:
  355. return _device.SetPlay(CancellationToken.None);
  356. case PlaystateCommand.PlayPause:
  357. return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None);
  358. case PlaystateCommand.Seek:
  359. return Seek(command.SeekPositionTicks ?? 0);
  360. case PlaystateCommand.NextTrack:
  361. return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken);
  362. case PlaystateCommand.PreviousTrack:
  363. return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken);
  364. }
  365. return Task.CompletedTask;
  366. }
  367. private async Task Seek(long newPosition)
  368. {
  369. var media = _device.CurrentMediaInfo;
  370. if (media != null)
  371. {
  372. var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
  373. if (info.Item != null && !EnableClientSideSeek(info))
  374. {
  375. var user = _session.UserId.Equals(default)
  376. ? null
  377. : _userManager.GetUserById(_session.UserId);
  378. var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
  379. await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
  380. // Send a message to the DLNA device to notify what is the next track in the play list.
  381. var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
  382. await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
  383. return;
  384. }
  385. await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
  386. }
  387. }
  388. private bool EnableClientSideSeek(StreamParams info)
  389. {
  390. return info.IsDirectStream;
  391. }
  392. private bool EnableClientSideSeek(StreamInfo info)
  393. {
  394. return info.IsDirectStream;
  395. }
  396. private void AddItemFromId(Guid id, List<BaseItem> list)
  397. {
  398. var item = _libraryManager.GetItemById(id);
  399. if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
  400. {
  401. list.Add(item);
  402. }
  403. }
  404. private PlaylistItem CreatePlaylistItem(
  405. BaseItem item,
  406. User user,
  407. long startPostionTicks,
  408. string mediaSourceId,
  409. int? audioStreamIndex,
  410. int? subtitleStreamIndex)
  411. {
  412. var deviceInfo = _device.Properties;
  413. var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
  414. _dlnaManager.GetDefaultProfile();
  415. var mediaSources = item is IHasMediaSources
  416. ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
  417. : Array.Empty<MediaSourceInfo>();
  418. var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
  419. playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
  420. playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
  421. var itemXml = new DidlBuilder(
  422. profile,
  423. user,
  424. _imageProcessor,
  425. _serverAddress,
  426. _accessToken,
  427. _userDataManager,
  428. _localization,
  429. _mediaSourceManager,
  430. _logger,
  431. _mediaEncoder,
  432. _libraryManager)
  433. .GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
  434. playlistItem.Didl = itemXml;
  435. return playlistItem;
  436. }
  437. private string GetDlnaHeaders(PlaylistItem item)
  438. {
  439. var profile = item.Profile;
  440. var streamInfo = item.StreamInfo;
  441. if (streamInfo.MediaType == DlnaProfileType.Audio)
  442. {
  443. return ContentFeatureBuilder.BuildAudioHeader(
  444. profile,
  445. streamInfo.Container,
  446. streamInfo.TargetAudioCodec.FirstOrDefault(),
  447. streamInfo.TargetAudioBitrate,
  448. streamInfo.TargetAudioSampleRate,
  449. streamInfo.TargetAudioChannels,
  450. streamInfo.TargetAudioBitDepth,
  451. streamInfo.IsDirectStream,
  452. streamInfo.RunTimeTicks ?? 0,
  453. streamInfo.TranscodeSeekInfo);
  454. }
  455. if (streamInfo.MediaType == DlnaProfileType.Video)
  456. {
  457. var list = ContentFeatureBuilder.BuildVideoHeader(
  458. profile,
  459. streamInfo.Container,
  460. streamInfo.TargetVideoCodec.FirstOrDefault(),
  461. streamInfo.TargetAudioCodec.FirstOrDefault(),
  462. streamInfo.TargetWidth,
  463. streamInfo.TargetHeight,
  464. streamInfo.TargetVideoBitDepth,
  465. streamInfo.TargetVideoBitrate,
  466. streamInfo.TargetTimestamp,
  467. streamInfo.IsDirectStream,
  468. streamInfo.RunTimeTicks ?? 0,
  469. streamInfo.TargetVideoProfile,
  470. streamInfo.TargetVideoLevel,
  471. streamInfo.TargetFramerate ?? 0,
  472. streamInfo.TargetPacketLength,
  473. streamInfo.TranscodeSeekInfo,
  474. streamInfo.IsTargetAnamorphic,
  475. streamInfo.IsTargetInterlaced,
  476. streamInfo.TargetRefFrames,
  477. streamInfo.TargetVideoStreamCount,
  478. streamInfo.TargetAudioStreamCount,
  479. streamInfo.TargetVideoCodecTag,
  480. streamInfo.IsTargetAVC);
  481. return list.FirstOrDefault();
  482. }
  483. return null;
  484. }
  485. private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
  486. {
  487. if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
  488. {
  489. return new PlaylistItem
  490. {
  491. StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(new VideoOptions
  492. {
  493. ItemId = item.Id,
  494. MediaSources = mediaSources,
  495. Profile = profile,
  496. DeviceId = deviceId,
  497. MaxBitrate = profile.MaxStreamingBitrate,
  498. MediaSourceId = mediaSourceId,
  499. AudioStreamIndex = audioStreamIndex,
  500. SubtitleStreamIndex = subtitleStreamIndex
  501. }),
  502. Profile = profile
  503. };
  504. }
  505. if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
  506. {
  507. return new PlaylistItem
  508. {
  509. StreamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(new AudioOptions
  510. {
  511. ItemId = item.Id,
  512. MediaSources = mediaSources,
  513. Profile = profile,
  514. DeviceId = deviceId,
  515. MaxBitrate = profile.MaxStreamingBitrate,
  516. MediaSourceId = mediaSourceId
  517. }),
  518. Profile = profile
  519. };
  520. }
  521. if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
  522. {
  523. return PlaylistItemFactory.Create((Photo)item, profile);
  524. }
  525. throw new ArgumentException("Unrecognized item type.");
  526. }
  527. /// <summary>
  528. /// Plays the items.
  529. /// </summary>
  530. /// <param name="items">The items.</param>
  531. /// <param name="cancellationToken">The cancellation token.</param>
  532. /// <returns><c>true</c> on success.</returns>
  533. private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default)
  534. {
  535. _playlist.Clear();
  536. _playlist.AddRange(items);
  537. _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count);
  538. await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false);
  539. return true;
  540. }
  541. private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default)
  542. {
  543. if (index < 0 || index >= _playlist.Count)
  544. {
  545. _playlist.Clear();
  546. await _device.SetStop(cancellationToken).ConfigureAwait(false);
  547. return;
  548. }
  549. _currentPlaylistIndex = index;
  550. var currentitem = _playlist[index];
  551. await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
  552. // Send a message to the DLNA device to notify what is the next track in the play list.
  553. await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false);
  554. var streamInfo = currentitem.StreamInfo;
  555. if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
  556. {
  557. await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false);
  558. }
  559. }
  560. /// <inheritdoc />
  561. public void Dispose()
  562. {
  563. Dispose(true);
  564. GC.SuppressFinalize(this);
  565. }
  566. /// <summary>
  567. /// Releases unmanaged and optionally managed resources.
  568. /// </summary>
  569. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  570. protected virtual void Dispose(bool disposing)
  571. {
  572. if (_disposed)
  573. {
  574. return;
  575. }
  576. if (disposing)
  577. {
  578. _device.Dispose();
  579. }
  580. _device.PlaybackStart -= OnDevicePlaybackStart;
  581. _device.PlaybackProgress -= OnDevicePlaybackProgress;
  582. _device.PlaybackStopped -= OnDevicePlaybackStopped;
  583. _device.MediaChanged -= OnDeviceMediaChanged;
  584. _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
  585. _device.OnDeviceUnavailable = null;
  586. _device = null;
  587. _disposed = true;
  588. }
  589. private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
  590. {
  591. switch (command.Name)
  592. {
  593. case GeneralCommandType.VolumeDown:
  594. return _device.VolumeDown(cancellationToken);
  595. case GeneralCommandType.VolumeUp:
  596. return _device.VolumeUp(cancellationToken);
  597. case GeneralCommandType.Mute:
  598. return _device.Mute(cancellationToken);
  599. case GeneralCommandType.Unmute:
  600. return _device.Unmute(cancellationToken);
  601. case GeneralCommandType.ToggleMute:
  602. return _device.ToggleMute(cancellationToken);
  603. case GeneralCommandType.SetAudioStreamIndex:
  604. if (command.Arguments.TryGetValue("Index", out string index))
  605. {
  606. if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
  607. {
  608. return SetAudioStreamIndex(val);
  609. }
  610. throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
  611. }
  612. throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
  613. case GeneralCommandType.SetSubtitleStreamIndex:
  614. if (command.Arguments.TryGetValue("Index", out index))
  615. {
  616. if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
  617. {
  618. return SetSubtitleStreamIndex(val);
  619. }
  620. throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
  621. }
  622. throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
  623. case GeneralCommandType.SetVolume:
  624. if (command.Arguments.TryGetValue("Volume", out string vol))
  625. {
  626. if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
  627. {
  628. return _device.SetVolume(volume, cancellationToken);
  629. }
  630. throw new ArgumentException("Unsupported volume value supplied.");
  631. }
  632. throw new ArgumentException("Volume argument cannot be null");
  633. default:
  634. return Task.CompletedTask;
  635. }
  636. }
  637. private async Task SetAudioStreamIndex(int? newIndex)
  638. {
  639. var media = _device.CurrentMediaInfo;
  640. if (media != null)
  641. {
  642. var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
  643. if (info.Item != null)
  644. {
  645. var newPosition = GetProgressPositionTicks(info) ?? 0;
  646. var user = _session.UserId.Equals(default)
  647. ? null
  648. : _userManager.GetUserById(_session.UserId);
  649. var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
  650. await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
  651. // Send a message to the DLNA device to notify what is the next track in the play list.
  652. var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
  653. await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
  654. if (EnableClientSideSeek(newItem.StreamInfo))
  655. {
  656. await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
  657. }
  658. }
  659. }
  660. }
  661. private async Task SetSubtitleStreamIndex(int? newIndex)
  662. {
  663. var media = _device.CurrentMediaInfo;
  664. if (media != null)
  665. {
  666. var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
  667. if (info.Item != null)
  668. {
  669. var newPosition = GetProgressPositionTicks(info) ?? 0;
  670. var user = _session.UserId.Equals(default)
  671. ? null
  672. : _userManager.GetUserById(_session.UserId);
  673. var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
  674. await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
  675. // Send a message to the DLNA device to notify what is the next track in the play list.
  676. var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
  677. await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
  678. if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
  679. {
  680. await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
  681. }
  682. }
  683. }
  684. }
  685. private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
  686. {
  687. const int MaxWait = 15000000;
  688. const int Interval = 500;
  689. var currentWait = 0;
  690. while (_device.TransportState != TransportState.Playing && currentWait < MaxWait)
  691. {
  692. await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
  693. currentWait += Interval;
  694. }
  695. await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
  696. }
  697. private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
  698. {
  699. var value = values.GetValueOrDefault(name);
  700. if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
  701. {
  702. return result;
  703. }
  704. return null;
  705. }
  706. private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
  707. {
  708. var value = values.GetValueOrDefault(name);
  709. if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
  710. {
  711. return result;
  712. }
  713. return 0;
  714. }
  715. /// <inheritdoc />
  716. public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
  717. {
  718. if (_disposed)
  719. {
  720. throw new ObjectDisposedException(GetType().Name);
  721. }
  722. if (_device == null)
  723. {
  724. return Task.CompletedTask;
  725. }
  726. if (name == SessionMessageType.Play)
  727. {
  728. return SendPlayCommand(data as PlayRequest, cancellationToken);
  729. }
  730. if (name == SessionMessageType.Playstate)
  731. {
  732. return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
  733. }
  734. if (name == SessionMessageType.GeneralCommand)
  735. {
  736. return SendGeneralCommand(data as GeneralCommand, cancellationToken);
  737. }
  738. // Not supported or needed right now
  739. return Task.CompletedTask;
  740. }
  741. private class StreamParams
  742. {
  743. private MediaSourceInfo _mediaSource;
  744. private IMediaSourceManager _mediaSourceManager;
  745. public Guid ItemId { get; set; }
  746. public bool IsDirectStream { get; set; }
  747. public long StartPositionTicks { get; set; }
  748. public int? AudioStreamIndex { get; set; }
  749. public int? SubtitleStreamIndex { get; set; }
  750. public string DeviceProfileId { get; set; }
  751. public string DeviceId { get; set; }
  752. public string MediaSourceId { get; set; }
  753. public string LiveStreamId { get; set; }
  754. public BaseItem Item { get; set; }
  755. public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
  756. {
  757. if (_mediaSource != null)
  758. {
  759. return _mediaSource;
  760. }
  761. if (Item is not IHasMediaSources)
  762. {
  763. return null;
  764. }
  765. if (_mediaSourceManager != null)
  766. {
  767. _mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
  768. }
  769. return _mediaSource;
  770. }
  771. private static Guid GetItemId(string url)
  772. {
  773. if (string.IsNullOrEmpty(url))
  774. {
  775. throw new ArgumentNullException(nameof(url));
  776. }
  777. var parts = url.Split('/');
  778. for (var i = 0; i < parts.Length - 1; i++)
  779. {
  780. var part = parts[i];
  781. if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
  782. string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
  783. {
  784. if (Guid.TryParse(parts[i + 1], out var result))
  785. {
  786. return result;
  787. }
  788. }
  789. }
  790. return default;
  791. }
  792. public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
  793. {
  794. if (string.IsNullOrEmpty(url))
  795. {
  796. throw new ArgumentNullException(nameof(url));
  797. }
  798. var request = new StreamParams
  799. {
  800. ItemId = GetItemId(url)
  801. };
  802. if (request.ItemId.Equals(default))
  803. {
  804. return request;
  805. }
  806. var index = url.IndexOf('?', StringComparison.Ordinal);
  807. if (index == -1)
  808. {
  809. return request;
  810. }
  811. var query = url.Substring(index + 1);
  812. Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
  813. request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
  814. request.DeviceId = values.GetValueOrDefault("DeviceId");
  815. request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
  816. request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
  817. request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
  818. request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
  819. request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
  820. request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
  821. request.Item = libraryManager.GetItemById(request.ItemId);
  822. request._mediaSourceManager = mediaSourceManager;
  823. return request;
  824. }
  825. }
  826. }
  827. }