jellyfin/Emby.Dlna/PlayTo/PlayToController.cs

981 lines
37 KiB
C#
Raw Normal View History

#pragma warning disable CS1591
using System;
2016-10-30 00:22:20 +02:00
using System.Collections.Generic;
using System.Globalization;
2017-09-25 07:06:15 +02:00
using System.Linq;
2016-10-30 00:22:20 +02:00
using System.Threading;
using System.Threading.Tasks;
2019-01-13 20:16:19 +01:00
using Emby.Dlna.Didl;
2020-05-20 19:07:53 +02:00
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
2019-01-13 20:16:19 +01:00
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
2016-10-30 00:22:20 +02:00
using MediaBrowser.Controller.MediaEncoding;
2019-01-13 20:16:19 +01:00
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
2016-10-30 00:22:20 +02:00
using MediaBrowser.Model.Globalization;
2019-01-13 20:16:19 +01:00
using MediaBrowser.Model.Session;
2019-02-27 12:40:18 +01:00
using Microsoft.AspNetCore.WebUtilities;
2019-01-13 20:16:19 +01:00
using Microsoft.Extensions.Logging;
2020-05-20 19:07:53 +02:00
using Photo = MediaBrowser.Controller.Entities.Photo;
2016-10-30 00:22:20 +02:00
2016-10-30 00:34:54 +02:00
namespace Emby.Dlna.PlayTo
2016-10-30 00:22:20 +02:00
{
public class PlayToController : ISessionController, IDisposable
{
private readonly SessionInfo _session;
private readonly ISessionManager _sessionManager;
private readonly ILibraryManager _libraryManager;
private readonly ILogger _logger;
private readonly IDlnaManager _dlnaManager;
private readonly IUserManager _userManager;
private readonly IImageProcessor _imageProcessor;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly string _serverAddress;
private readonly string _accessToken;
2020-04-02 16:49:58 +02:00
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
2020-08-20 17:01:04 +02:00
private Device _device;
2020-04-02 16:49:58 +02:00
private int _currentPlaylistIndex;
2016-10-30 00:22:20 +02:00
2020-04-02 16:49:58 +02:00
private bool _disposed;
2016-10-30 00:22:20 +02:00
public PlayToController(
SessionInfo session,
ISessionManager sessionManager,
ILibraryManager libraryManager,
ILogger logger,
IDlnaManager dlnaManager,
IUserManager userManager,
IImageProcessor imageProcessor,
string serverAddress,
string accessToken,
IDeviceDiscovery deviceDiscovery,
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
2023-03-07 21:51:48 +01:00
IMediaEncoder mediaEncoder,
Device device)
2016-10-30 00:22:20 +02:00
{
_session = session;
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_logger = logger;
2016-10-30 00:22:20 +02:00
_dlnaManager = dlnaManager;
_userManager = userManager;
_imageProcessor = imageProcessor;
_serverAddress = serverAddress;
_accessToken = accessToken;
2016-10-30 00:22:20 +02:00
_deviceDiscovery = deviceDiscovery;
_userDataManager = userDataManager;
_localization = localization;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
2020-04-02 16:49:58 +02:00
2016-10-30 00:22:20 +02:00
_device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable;
2020-04-02 16:49:58 +02:00
_device.PlaybackStart += OnDevicePlaybackStart;
_device.PlaybackProgress += OnDevicePlaybackProgress;
2020-04-03 17:30:01 +02:00
_device.PlaybackStopped += OnDevicePlaybackStopped;
2020-04-02 16:49:58 +02:00
_device.MediaChanged += OnDeviceMediaChanged;
2016-10-30 00:22:20 +02:00
_device.Start();
2020-04-02 16:49:58 +02:00
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
2016-10-30 00:22:20 +02:00
}
2023-03-07 21:51:48 +01:00
public bool IsSessionActive => !_disposed;
public bool SupportsMediaControl => IsSessionActive;
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
{
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
{
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
var nextItemIndex = currentPlayListItemIndex + 1;
var nextItem = _playlist[nextItemIndex];
// Send the SetNextAvTransport message.
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
}
}
2016-10-30 00:22:20 +02:00
private void OnDeviceUnavailable()
{
try
{
_sessionManager.ReportSessionEnded(_session.Id);
}
2019-03-25 17:27:24 +01:00
catch (Exception ex)
2016-10-30 00:22:20 +02:00
{
// Could throw if the session is already gone
2019-03-25 17:27:24 +01:00
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
2016-10-30 00:22:20 +02:00
}
}
2023-03-07 21:51:48 +01:00
private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
2016-10-30 00:22:20 +02:00
{
var info = e.Argument;
2019-03-25 17:27:24 +01:00
if (!_disposed
2023-03-07 21:51:48 +01:00
&& info.Headers.TryGetValue("USN", out string? usn)
2019-03-25 17:27:24 +01:00
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
2023-03-07 21:51:48 +01:00
|| (info.Headers.TryGetValue("NT", out string? nt)
2019-03-25 17:27:24 +01:00
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
2016-10-30 00:22:20 +02:00
{
2019-03-25 17:27:24 +01:00
OnDeviceUnavailable();
2016-10-30 00:22:20 +02:00
}
}
2023-03-07 21:51:48 +01:00
private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
2016-10-30 00:22:20 +02:00
{
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
2017-01-24 20:54:18 +01:00
{
return;
}
2016-10-30 00:22:20 +02:00
try
{
2018-09-12 19:26:21 +02:00
var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager);
2022-12-05 15:01:13 +01:00
if (streamInfo.Item is not null)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
var positionTicks = GetProgressPositionTicks(streamInfo);
2016-10-30 00:22:20 +02:00
2020-05-25 23:52:51 +02:00
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
2018-09-12 19:26:21 +02:00
streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager);
2022-12-05 15:00:20 +01:00
if (streamInfo.Item is null)
2020-05-25 23:52:51 +02:00
{
return;
}
2016-10-30 00:22:20 +02:00
2020-04-02 16:49:58 +02:00
var newItemProgress = GetProgressInfo(streamInfo);
2016-10-30 00:22:20 +02:00
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the playlist.
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId.Equals(streamInfo.ItemId));
if (currentItemIndex >= 0)
{
_currentPlaylistIndex = currentItemIndex;
}
2022-01-22 23:36:42 +01:00
await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
catch (Exception ex)
{
2018-12-20 13:11:26 +01:00
_logger.LogError(ex, "Error reporting progress");
2016-10-30 00:22:20 +02:00
}
}
2023-03-07 21:51:48 +01:00
private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
2016-10-30 00:22:20 +02:00
{
2017-01-24 20:54:18 +01:00
if (_disposed)
{
return;
}
2016-10-30 00:22:20 +02:00
try
{
2018-09-12 19:26:21 +02:00
var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
2016-10-30 00:22:20 +02:00
2022-12-05 15:00:20 +01:00
if (streamInfo.Item is null)
2020-05-25 23:52:51 +02:00
{
return;
}
2016-10-30 00:22:20 +02:00
2020-04-02 16:49:58 +02:00
var positionTicks = GetProgressPositionTicks(streamInfo);
2016-10-30 00:22:20 +02:00
2020-05-25 23:52:51 +02:00
await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
2018-09-12 19:26:21 +02:00
var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false);
2022-12-05 15:00:20 +01:00
var duration = mediaSource is null
2021-12-15 18:25:36 +01:00
? _device.Duration?.Ticks
: mediaSource.RunTimeTicks;
2016-10-30 00:22:20 +02:00
2020-05-25 23:52:51 +02:00
var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0;
2016-10-30 00:22:20 +02:00
if (!playedToCompletion && duration.HasValue && positionTicks.HasValue)
{
double percent = positionTicks.Value;
percent /= duration.Value;
playedToCompletion = Math.Abs(1 - percent) <= .1;
}
if (playedToCompletion)
{
await SetPlaylistIndex(_currentPlaylistIndex + 1).ConfigureAwait(false);
}
else
{
2020-04-02 16:49:58 +02:00
_playlist.Clear();
2016-10-30 00:22:20 +02:00
}
}
catch (Exception ex)
{
2018-12-20 13:11:26 +01:00
_logger.LogError(ex, "Error reporting playback stopped");
2016-10-30 00:22:20 +02:00
}
}
2020-05-25 23:52:51 +02:00
private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks)
2016-10-30 00:22:20 +02:00
{
try
{
await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo
{
ItemId = streamInfo.ItemId,
SessionId = _session.Id,
PositionTicks = positionTicks,
MediaSourceId = streamInfo.MediaSourceId
}).ConfigureAwait(false);
}
catch (Exception ex)
{
2018-12-20 13:11:26 +01:00
_logger.LogError(ex, "Error reporting progress");
2016-10-30 00:22:20 +02:00
}
}
2023-03-07 21:51:48 +01:00
private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
2016-10-30 00:22:20 +02:00
{
2017-01-24 20:54:18 +01:00
if (_disposed)
{
return;
}
2016-10-30 00:22:20 +02:00
try
{
2018-09-12 19:26:21 +02:00
var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager);
2016-10-30 00:22:20 +02:00
2022-12-05 15:01:13 +01:00
if (info.Item is not null)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
var progress = GetProgressInfo(info);
2016-10-30 00:22:20 +02:00
await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false);
}
}
catch (Exception ex)
{
2018-12-20 13:11:26 +01:00
_logger.LogError(ex, "Error reporting progress");
2016-10-30 00:22:20 +02:00
}
}
2023-03-07 21:51:48 +01:00
private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
2016-10-30 00:22:20 +02:00
{
2017-01-24 20:54:18 +01:00
if (_disposed)
{
return;
}
2016-10-30 00:22:20 +02:00
try
{
2018-09-12 19:26:21 +02:00
var mediaUrl = e.MediaInfo.Url;
if (string.IsNullOrWhiteSpace(mediaUrl))
{
return;
}
var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager);
2016-10-30 00:22:20 +02:00
2022-12-05 15:01:13 +01:00
if (info.Item is not null)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
var progress = GetProgressInfo(info);
2016-10-30 00:22:20 +02:00
await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false);
}
}
catch (Exception ex)
{
2018-12-20 13:11:26 +01:00
_logger.LogError(ex, "Error reporting progress");
2016-10-30 00:22:20 +02:00
}
}
2020-04-02 16:49:58 +02:00
private long? GetProgressPositionTicks(StreamParams info)
2016-10-30 00:22:20 +02:00
{
var ticks = _device.Position.Ticks;
if (!EnableClientSideSeek(info))
{
ticks += info.StartPositionTicks;
}
2018-09-12 19:26:21 +02:00
return ticks;
}
2020-04-02 16:49:58 +02:00
private PlaybackStartInfo GetProgressInfo(StreamParams info)
2018-09-12 19:26:21 +02:00
{
2016-10-30 00:22:20 +02:00
return new PlaybackStartInfo
{
ItemId = info.ItemId,
SessionId = _session.Id,
2020-04-02 16:49:58 +02:00
PositionTicks = GetProgressPositionTicks(info),
2016-10-30 00:22:20 +02:00
IsMuted = _device.IsMuted,
IsPaused = _device.IsPaused,
MediaSourceId = info.MediaSourceId,
AudioStreamIndex = info.AudioStreamIndex,
SubtitleStreamIndex = info.SubtitleStreamIndex,
VolumeLevel = _device.Volume,
2018-09-12 19:26:21 +02:00
CanSeek = true,
2016-10-30 00:22:20 +02:00
2017-04-17 22:33:07 +02:00
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode
2016-10-30 00:22:20 +02:00
};
}
2020-04-02 16:49:58 +02:00
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
2016-10-30 00:22:20 +02:00
{
2020-11-06 16:15:30 +01:00
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
2016-10-30 00:22:20 +02:00
var user = command.ControllingUserId.Equals(default)
? null :
_userManager.GetUserById(command.ControllingUserId);
2016-10-30 00:22:20 +02:00
var items = new List<BaseItem>();
2018-09-12 19:26:21 +02:00
foreach (var id in command.ItemIds)
2016-10-30 00:22:20 +02:00
{
2018-09-12 19:26:21 +02:00
AddItemFromId(id, items);
2016-10-30 00:22:20 +02:00
}
2017-11-21 23:14:56 +01:00
var startIndex = command.StartIndex ?? 0;
2020-12-02 15:38:52 +01:00
int len = items.Count - startIndex;
2017-11-21 23:14:56 +01:00
if (startIndex > 0)
{
2020-12-02 15:38:52 +01:00
items = items.GetRange(startIndex, len);
2017-11-21 23:14:56 +01:00
}
2020-12-02 15:38:52 +01:00
var playlist = new PlaylistItem[len];
2020-12-07 19:27:22 +01:00
// Not nullable enabled - so this is required.
playlist[0] = CreatePlaylistItem(
items[0],
user,
2020-12-07 19:31:45 +01:00
command.StartPositionTicks ?? 0,
2020-12-07 19:27:22 +01:00
command.MediaSourceId ?? string.Empty,
command.AudioStreamIndex,
command.SubtitleStreamIndex);
2020-12-02 15:38:52 +01:00
for (int i = 1; i < len; i++)
2016-10-30 00:22:20 +02:00
{
2020-12-07 19:27:22 +01:00
playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
2016-10-30 00:22:20 +02:00
}
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
2016-10-30 00:22:20 +02:00
if (command.PlayCommand == PlayCommand.PlayLast)
{
2020-04-02 16:49:58 +02:00
_playlist.AddRange(playlist);
2016-10-30 00:22:20 +02:00
}
2020-04-02 16:49:58 +02:00
2016-10-30 00:22:20 +02:00
if (command.PlayCommand == PlayCommand.PlayNext)
{
2020-04-02 16:49:58 +02:00
_playlist.AddRange(playlist);
2016-10-30 00:22:20 +02:00
}
if (!command.ControllingUserId.Equals(default))
2016-10-30 00:22:20 +02:00
{
2020-08-20 17:01:04 +02:00
_sessionManager.LogSessionActivity(
_session.Client,
_session.ApplicationVersion,
_session.DeviceId,
_session.DeviceName,
2020-08-20 21:04:57 +02:00
_session.RemoteEndPoint,
user);
2016-10-30 00:22:20 +02:00
}
2020-04-02 16:49:58 +02:00
return PlayItems(playlist, cancellationToken);
2016-10-30 00:22:20 +02:00
}
2018-09-12 19:26:21 +02:00
private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
2016-10-30 00:22:20 +02:00
{
switch (command.Command)
{
case PlaystateCommand.Stop:
2020-04-02 16:49:58 +02:00
_playlist.Clear();
2018-09-12 19:26:21 +02:00
return _device.SetStop(CancellationToken.None);
2016-10-30 00:22:20 +02:00
case PlaystateCommand.Pause:
2018-09-12 19:26:21 +02:00
return _device.SetPause(CancellationToken.None);
2016-10-30 00:22:20 +02:00
case PlaystateCommand.Unpause:
2018-09-12 19:26:21 +02:00
return _device.SetPlay(CancellationToken.None);
2016-10-30 00:22:20 +02:00
2017-08-28 02:33:05 +02:00
case PlaystateCommand.PlayPause:
2018-09-12 19:26:21 +02:00
return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None);
2017-08-28 02:33:05 +02:00
2016-10-30 00:22:20 +02:00
case PlaystateCommand.Seek:
return Seek(command.SeekPositionTicks ?? 0);
2016-10-30 00:22:20 +02:00
case PlaystateCommand.NextTrack:
2020-04-02 16:49:58 +02:00
return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken);
2016-10-30 00:22:20 +02:00
case PlaystateCommand.PreviousTrack:
2020-04-02 16:49:58 +02:00
return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken);
2016-10-30 00:22:20 +02:00
}
2018-09-12 19:26:21 +02:00
return Task.CompletedTask;
2016-10-30 00:22:20 +02:00
}
private async Task Seek(long newPosition)
{
var media = _device.CurrentMediaInfo;
2022-12-05 15:01:13 +01:00
if (media is not null)
2016-10-30 00:22:20 +02:00
{
2018-09-12 19:26:21 +02:00
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
2016-10-30 00:22:20 +02:00
2022-12-05 15:01:13 +01:00
if (info.Item is not null && !EnableClientSideSeek(info))
2016-10-30 00:22:20 +02:00
{
var user = _session.UserId.Equals(default)
? null
: _userManager.GetUserById(_session.UserId);
2016-10-30 00:22:20 +02:00
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
2018-09-12 19:26:21 +02:00
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
2022-01-22 23:36:42 +01:00
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
return;
}
2020-06-15 23:43:52 +02:00
2018-09-12 19:26:21 +02:00
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
}
private bool EnableClientSideSeek(StreamParams info)
{
return info.IsDirectStream;
}
private bool EnableClientSideSeek(StreamInfo info)
{
return info.IsDirectStream;
}
private void AddItemFromId(Guid id, List<BaseItem> list)
{
var item = _libraryManager.GetItemById(id);
if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
{
list.Add(item);
}
}
2020-05-13 04:10:35 +02:00
private PlaylistItem CreatePlaylistItem(
BaseItem item,
2023-03-07 21:51:48 +01:00
User? user,
2020-05-13 04:10:35 +02:00
long startPostionTicks,
2023-03-07 21:51:48 +01:00
string? mediaSourceId,
2020-05-13 04:10:35 +02:00
int? audioStreamIndex,
int? subtitleStreamIndex)
2016-10-30 00:22:20 +02:00
{
var deviceInfo = _device.Properties;
var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile();
var mediaSources = item is IHasMediaSources
2020-12-02 15:38:52 +01:00
? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray()
: Array.Empty<MediaSourceInfo>();
2016-10-30 00:22:20 +02:00
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
2018-09-12 19:26:21 +02:00
playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken));
2016-10-30 00:22:20 +02:00
2020-04-02 16:49:58 +02:00
var itemXml = new DidlBuilder(
profile,
user,
_imageProcessor,
_serverAddress,
_accessToken,
_userDataManager,
_localization,
_mediaSourceManager,
_logger,
_mediaEncoder,
_libraryManager)
.GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo);
2016-10-30 00:22:20 +02:00
playlistItem.Didl = itemXml;
return playlistItem;
}
2023-03-07 21:51:48 +01:00
private string? GetDlnaHeaders(PlaylistItem item)
2016-10-30 00:22:20 +02:00
{
var profile = item.Profile;
var streamInfo = item.StreamInfo;
if (streamInfo.MediaType == DlnaProfileType.Audio)
{
2021-04-02 20:06:38 +02:00
return ContentFeatureBuilder.BuildAudioHeader(
profile,
2020-08-20 21:04:57 +02:00
streamInfo.Container,
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetAudioBitrate,
streamInfo.TargetAudioSampleRate,
streamInfo.TargetAudioChannels,
streamInfo.TargetAudioBitDepth,
streamInfo.IsDirectStream,
streamInfo.RunTimeTicks ?? 0,
streamInfo.TranscodeSeekInfo);
2016-10-30 00:22:20 +02:00
}
if (streamInfo.MediaType == DlnaProfileType.Video)
{
2021-04-02 20:06:38 +02:00
var list = ContentFeatureBuilder.BuildVideoHeader(
profile,
2020-08-20 21:04:57 +02:00
streamInfo.Container,
streamInfo.TargetVideoCodec.FirstOrDefault(),
streamInfo.TargetAudioCodec.FirstOrDefault(),
streamInfo.TargetWidth,
streamInfo.TargetHeight,
streamInfo.TargetVideoBitDepth,
streamInfo.TargetVideoBitrate,
streamInfo.TargetTimestamp,
streamInfo.IsDirectStream,
streamInfo.RunTimeTicks ?? 0,
streamInfo.TargetVideoProfile,
streamInfo.TargetVideoRangeType,
2020-08-20 21:04:57 +02:00
streamInfo.TargetVideoLevel,
streamInfo.TargetFramerate ?? 0,
streamInfo.TargetPacketLength,
streamInfo.TranscodeSeekInfo,
streamInfo.IsTargetAnamorphic,
streamInfo.IsTargetInterlaced,
streamInfo.TargetRefFrames,
streamInfo.TargetVideoStreamCount,
streamInfo.TargetAudioStreamCount,
streamInfo.TargetVideoCodecTag,
streamInfo.IsTargetAVC);
2016-10-30 00:22:20 +02:00
2022-01-22 15:40:05 +01:00
return list.FirstOrDefault();
2016-10-30 00:22:20 +02:00
}
return null;
}
2023-03-07 21:51:48 +01:00
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
2016-10-30 00:22:20 +02:00
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
return new PlaylistItem
{
2022-03-26 12:11:00 +01:00
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions
2016-10-30 00:22:20 +02:00
{
2018-09-12 19:26:21 +02:00
ItemId = item.Id,
2020-12-02 15:38:52 +01:00
MediaSources = mediaSources,
2016-10-30 00:22:20 +02:00
Profile = profile,
DeviceId = deviceId,
MaxBitrate = profile.MaxStreamingBitrate,
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex
}),
Profile = profile
};
}
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
return new PlaylistItem
{
2022-03-26 12:11:00 +01:00
StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions
2016-10-30 00:22:20 +02:00
{
2018-09-12 19:26:21 +02:00
ItemId = item.Id,
2020-12-02 15:38:52 +01:00
MediaSources = mediaSources,
2016-10-30 00:22:20 +02:00
Profile = profile,
DeviceId = deviceId,
MaxBitrate = profile.MaxStreamingBitrate,
MediaSourceId = mediaSourceId
}),
Profile = profile
};
}
if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{
2020-12-02 15:38:52 +01:00
return PlaylistItemFactory.Create((Photo)item, profile);
2016-10-30 00:22:20 +02:00
}
throw new ArgumentException("Unrecognized item type.");
}
/// <summary>
/// Plays the items.
/// </summary>
/// <param name="items">The items.</param>
2020-04-02 16:49:58 +02:00
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><c>true</c> on success.</returns>
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
_playlist.Clear();
_playlist.AddRange(items);
_logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count);
2016-10-30 00:22:20 +02:00
2020-04-02 16:49:58 +02:00
await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
return true;
}
2020-04-02 16:49:58 +02:00
private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
if (index < 0 || index >= _playlist.Count)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
_playlist.Clear();
await _device.SetStop(cancellationToken).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
return;
}
_currentPlaylistIndex = index;
2020-04-02 16:49:58 +02:00
var currentitem = _playlist[index];
2016-10-30 00:22:20 +02:00
2020-04-02 16:49:58 +02:00
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
// Send a message to the DLNA device to notify what is the next track in the play list.
2022-01-22 23:36:42 +01:00
await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
var streamInfo = currentitem.StreamInfo;
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
{
2018-09-12 19:26:21 +02:00
await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
}
2020-04-02 16:49:58 +02:00
/// <inheritdoc />
2016-10-30 00:22:20 +02:00
public void Dispose()
{
2019-03-25 17:32:27 +01:00
Dispose(true);
GC.SuppressFinalize(this);
}
2016-10-30 00:22:20 +02:00
2020-08-20 17:59:27 +02:00
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
2019-03-25 17:32:27 +01:00
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
2016-10-30 00:22:20 +02:00
2019-03-25 17:32:27 +01:00
if (disposing)
{
2016-10-30 00:22:20 +02:00
_device.Dispose();
}
2019-03-25 17:32:27 +01:00
2020-04-02 16:49:58 +02:00
_device.PlaybackStart -= OnDevicePlaybackStart;
_device.PlaybackProgress -= OnDevicePlaybackProgress;
2020-04-03 17:30:01 +02:00
_device.PlaybackStopped -= OnDevicePlaybackStopped;
2020-04-02 16:49:58 +02:00
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
2019-03-25 17:32:27 +01:00
_device.OnDeviceUnavailable = null;
_disposed = true;
2016-10-30 00:22:20 +02:00
}
2018-09-12 19:26:21 +02:00
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
2016-10-30 00:22:20 +02:00
{
switch (command.Name)
{
case GeneralCommandType.VolumeDown:
return _device.VolumeDown(cancellationToken);
case GeneralCommandType.VolumeUp:
return _device.VolumeUp(cancellationToken);
case GeneralCommandType.Mute:
return _device.Mute(cancellationToken);
case GeneralCommandType.Unmute:
return _device.Unmute(cancellationToken);
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
2023-03-07 21:51:48 +01:00
if (command.Arguments.TryGetValue("Index", out string? index))
{
2021-09-26 16:14:36 +02:00
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
2016-10-30 00:22:20 +02:00
{
return SetAudioStreamIndex(val);
2016-10-30 00:22:20 +02:00
}
2020-08-20 21:04:57 +02:00
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
}
2016-10-30 00:22:20 +02:00
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
case GeneralCommandType.SetSubtitleStreamIndex:
if (command.Arguments.TryGetValue("Index", out index))
{
2021-09-26 16:14:36 +02:00
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return SetSubtitleStreamIndex(val);
2016-10-30 00:22:20 +02:00
}
2020-08-20 21:04:57 +02:00
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
}
2016-10-30 00:22:20 +02:00
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
2023-03-07 21:51:48 +01:00
if (command.Arguments.TryGetValue("Volume", out string? vol))
{
2021-09-26 16:14:36 +02:00
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
return _device.SetVolume(volume, cancellationToken);
2016-10-30 00:22:20 +02:00
}
2020-06-15 23:43:52 +02:00
throw new ArgumentException("Unsupported volume value supplied.");
}
2016-10-30 00:22:20 +02:00
throw new ArgumentException("Volume argument cannot be null");
default:
return Task.CompletedTask;
}
2016-10-30 00:22:20 +02:00
}
private async Task SetAudioStreamIndex(int? newIndex)
{
var media = _device.CurrentMediaInfo;
2022-12-05 15:01:13 +01:00
if (media is not null)
2016-10-30 00:22:20 +02:00
{
2018-09-12 19:26:21 +02:00
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
2016-10-30 00:22:20 +02:00
2022-12-05 15:01:13 +01:00
if (info.Item is not null)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
var newPosition = GetProgressPositionTicks(info) ?? 0;
2016-10-30 00:22:20 +02:00
var user = _session.UserId.Equals(default)
? null
: _userManager.GetUserById(_session.UserId);
2016-10-30 00:22:20 +02:00
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex);
2018-09-12 19:26:21 +02:00
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
2022-01-22 23:36:42 +01:00
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
if (EnableClientSideSeek(newItem.StreamInfo))
{
2018-09-12 19:26:21 +02:00
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
}
}
}
private async Task SetSubtitleStreamIndex(int? newIndex)
{
var media = _device.CurrentMediaInfo;
2022-12-05 15:01:13 +01:00
if (media is not null)
2016-10-30 00:22:20 +02:00
{
2018-09-12 19:26:21 +02:00
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
2016-10-30 00:22:20 +02:00
2022-12-05 15:01:13 +01:00
if (info.Item is not null)
2016-10-30 00:22:20 +02:00
{
2020-04-02 16:49:58 +02:00
var newPosition = GetProgressPositionTicks(info) ?? 0;
2016-10-30 00:22:20 +02:00
var user = _session.UserId.Equals(default)
? null
: _userManager.GetUserById(_session.UserId);
2016-10-30 00:22:20 +02:00
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex);
2018-09-12 19:26:21 +02:00
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
// Send a message to the DLNA device to notify what is the next track in the play list.
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
2022-01-22 23:36:42 +01:00
await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
{
2018-09-12 19:26:21 +02:00
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
}
}
}
2018-09-12 19:26:21 +02:00
private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken)
2016-10-30 00:22:20 +02:00
{
2020-12-02 15:38:52 +01:00
const int MaxWait = 15000000;
const int Interval = 500;
2016-10-30 00:22:20 +02:00
var currentWait = 0;
while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait)
2016-10-30 00:22:20 +02:00
{
2021-02-15 14:19:08 +01:00
await Task.Delay(Interval, cancellationToken).ConfigureAwait(false);
2020-12-02 15:38:52 +01:00
currentWait += Interval;
2016-10-30 00:22:20 +02:00
}
2018-09-12 19:26:21 +02:00
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
2016-10-30 00:22:20 +02:00
}
2020-08-20 21:04:57 +02:00
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return null;
}
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
{
var value = values.GetValueOrDefault(name);
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
{
return result;
}
return 0;
}
/// <inheritdoc />
public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
2020-08-20 21:04:57 +02:00
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
2023-03-07 21:51:48 +01:00
return name switch
2020-08-20 21:04:57 +02:00
{
2023-03-07 21:51:48 +01:00
SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
_ => Task.CompletedTask // Not supported or needed right now
};
2020-08-20 21:04:57 +02:00
}
2016-10-30 00:22:20 +02:00
private class StreamParams
{
2023-03-07 21:51:48 +01:00
private MediaSourceInfo? _mediaSource;
private IMediaSourceManager? _mediaSourceManager;
2020-08-20 17:01:04 +02:00
2018-09-12 19:26:21 +02:00
public Guid ItemId { get; set; }
2016-10-30 00:22:20 +02:00
public bool IsDirectStream { get; set; }
public long StartPositionTicks { get; set; }
public int? AudioStreamIndex { get; set; }
public int? SubtitleStreamIndex { get; set; }
2023-03-07 21:51:48 +01:00
public string? DeviceProfileId { get; set; }
2020-06-15 23:43:52 +02:00
2023-03-07 21:51:48 +01:00
public string? DeviceId { get; set; }
2016-10-30 00:22:20 +02:00
2023-03-07 21:51:48 +01:00
public string? MediaSourceId { get; set; }
2020-06-15 23:43:52 +02:00
2023-03-07 21:51:48 +01:00
public string? LiveStreamId { get; set; }
2016-10-30 00:22:20 +02:00
2023-03-07 21:51:48 +01:00
public BaseItem? Item { get; set; }
2020-06-15 23:43:52 +02:00
2023-03-07 21:51:48 +01:00
public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
2016-10-30 00:22:20 +02:00
{
2022-12-05 15:01:13 +01:00
if (_mediaSource is not null)
2018-09-12 19:26:21 +02:00
{
2022-01-22 15:40:05 +01:00
return _mediaSource;
2018-09-12 19:26:21 +02:00
}
2022-01-22 15:40:05 +01:00
if (Item is not IHasMediaSources)
2018-09-12 19:26:21 +02:00
{
return null;
}
2022-12-05 15:01:13 +01:00
if (_mediaSourceManager is not null)
2020-09-25 18:25:50 +02:00
{
2022-01-22 15:40:05 +01:00
_mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
2020-09-25 18:25:50 +02:00
}
2018-09-12 19:26:21 +02:00
2022-01-22 15:40:05 +01:00
return _mediaSource;
2018-09-12 19:26:21 +02:00
}
private static Guid GetItemId(string url)
{
ArgumentException.ThrowIfNullOrEmpty(url);
2018-09-12 19:26:21 +02:00
2016-10-30 00:22:20 +02:00
var parts = url.Split('/');
2021-01-16 21:08:41 +01:00
for (var i = 0; i < parts.Length - 1; i++)
2016-10-30 00:22:20 +02:00
{
var part = parts[i];
2023-03-07 21:51:48 +01:00
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
|| string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
2016-10-30 00:22:20 +02:00
{
2021-01-16 21:08:41 +01:00
if (Guid.TryParse(parts[i + 1], out var result))
2016-10-30 00:22:20 +02:00
{
2021-01-16 21:08:41 +01:00
return result;
2016-10-30 00:22:20 +02:00
}
}
}
return default;
2016-10-30 00:22:20 +02:00
}
2018-09-12 19:26:21 +02:00
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
2016-10-30 00:22:20 +02:00
{
ArgumentException.ThrowIfNullOrEmpty(url);
2018-09-12 19:26:21 +02:00
2016-10-30 00:22:20 +02:00
var request = new StreamParams
{
ItemId = GetItemId(url)
};
if (request.ItemId.Equals(default))
2016-10-30 00:22:20 +02:00
{
return request;
}
2020-04-02 16:49:58 +02:00
var index = url.IndexOf('?', StringComparison.Ordinal);
if (index == -1)
{
return request;
}
2016-10-30 00:22:20 +02:00
2018-09-12 19:26:21 +02:00
var query = url.Substring(index + 1);
Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
2016-10-30 00:22:20 +02:00
request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId");
request.DeviceId = values.GetValueOrDefault("DeviceId");
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
2021-03-22 22:34:47 +01:00
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
2018-09-12 19:26:21 +02:00
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
2016-10-30 00:22:20 +02:00
2018-09-12 19:26:21 +02:00
request.Item = libraryManager.GetItemById(request.ItemId);
2016-10-30 00:22:20 +02:00
2018-09-12 19:26:21 +02:00
request._mediaSourceManager = mediaSourceManager;
2016-10-30 00:22:20 +02:00
2018-09-12 19:26:21 +02:00
return request;
}
}
2016-10-30 00:22:20 +02:00
}
}