diff options
| author | Patrick Barron <barronpm@gmail.com> | 2023-11-09 14:45:16 -0500 |
|---|---|---|
| committer | Patrick Barron <barronpm@gmail.com> | 2023-11-15 20:53:44 -0500 |
| commit | f1aba6b95230474d47c580071370c7dbd00eba13 (patch) | |
| tree | 4fdc0131e7ae17c724e5bcda8d1e8878475a6461 /Emby.Dlna/PlayTo | |
| parent | 01fd42cf9555d85469c07ce3d0c0e5842359eb2b (diff) | |
Remove Emby.Dlna
Diffstat (limited to 'Emby.Dlna/PlayTo')
| -rw-r--r-- | Emby.Dlna/PlayTo/Device.cs | 1264 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/DeviceInfo.cs | 66 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/DlnaHttpClient.cs | 137 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/MediaChangedEventArgs.cs | 19 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlayToController.cs | 980 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlayToManager.cs | 258 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs | 16 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs | 16 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs | 16 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlaylistItem.cs | 19 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/PlaylistItemFactory.cs | 70 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/TransportCommands.cs | 181 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/TransportState.cs | 16 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/UpnpContainer.cs | 25 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/uBaseObject.cs | 63 | ||||
| -rw-r--r-- | Emby.Dlna/PlayTo/uPnpNamespaces.cs | 67 |
16 files changed, 0 insertions, 3213 deletions
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs deleted file mode 100644 index 18fa196508..0000000000 --- a/Emby.Dlna/PlayTo/Device.cs +++ /dev/null @@ -1,1264 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Security; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; -using Emby.Dlna.Common; -using Emby.Dlna.Ssdp; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.PlayTo -{ - public class Device : IDisposable - { - private readonly IHttpClientFactory _httpClientFactory; - - private readonly ILogger _logger; - - private readonly object _timerLock = new object(); - private Timer? _timer; - private int _muteVol; - private int _volume; - private DateTime _lastVolumeRefresh; - private bool _volumeRefreshActive; - private int _connectFailureCount; - private bool _disposed; - - public Device(DeviceInfo deviceProperties, IHttpClientFactory httpClientFactory, ILogger logger) - { - Properties = deviceProperties; - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - public event EventHandler<PlaybackStartEventArgs>? PlaybackStart; - - public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress; - - public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped; - - public event EventHandler<MediaChangedEventArgs>? MediaChanged; - - public DeviceInfo Properties { get; set; } - - public bool IsMuted { get; set; } - - public int Volume - { - get - { - RefreshVolumeIfNeeded().GetAwaiter().GetResult(); - return _volume; - } - - set => _volume = value; - } - - public TimeSpan? Duration { get; set; } - - public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0); - - public TransportState TransportState { get; private set; } - - public bool IsPlaying => TransportState == TransportState.PLAYING; - - public bool IsPaused => TransportState == TransportState.PAUSED_PLAYBACK; - - public bool IsStopped => TransportState == TransportState.STOPPED; - - public Action? OnDeviceUnavailable { get; set; } - - private TransportCommands? AvCommands { get; set; } - - private TransportCommands? RendererCommands { get; set; } - - public UBaseObject? CurrentMediaInfo { get; private set; } - - public void Start() - { - _logger.LogDebug("Dlna Device.Start"); - _timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite); - } - - private Task RefreshVolumeIfNeeded() - { - if (_volumeRefreshActive - && DateTime.UtcNow >= _lastVolumeRefresh.AddSeconds(5)) - { - _lastVolumeRefresh = DateTime.UtcNow; - return RefreshVolume(); - } - - return Task.CompletedTask; - } - - private async Task RefreshVolume(CancellationToken cancellationToken = default) - { - if (_disposed) - { - return; - } - - try - { - await GetVolume(cancellationToken).ConfigureAwait(false); - await GetMute(cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating device volume info for {DeviceName}", Properties.Name); - } - } - - private void RestartTimer(bool immediate = false) - { - lock (_timerLock) - { - if (_disposed) - { - return; - } - - _volumeRefreshActive = true; - - var time = immediate ? 100 : 10000; - _timer?.Change(time, Timeout.Infinite); - } - } - - /// <summary> - /// Restarts the timer in inactive mode. - /// </summary> - private void RestartTimerInactive() - { - lock (_timerLock) - { - if (_disposed) - { - return; - } - - _volumeRefreshActive = false; - - _timer?.Change(Timeout.Infinite, Timeout.Infinite); - } - } - - public Task VolumeDown(CancellationToken cancellationToken) - { - var sendVolume = Math.Max(Volume - 5, 0); - - return SetVolume(sendVolume, cancellationToken); - } - - public Task VolumeUp(CancellationToken cancellationToken) - { - var sendVolume = Math.Min(Volume + 5, 100); - - return SetVolume(sendVolume, cancellationToken); - } - - public Task ToggleMute(CancellationToken cancellationToken) - { - if (IsMuted) - { - return Unmute(cancellationToken); - } - - return Mute(cancellationToken); - } - - public async Task Mute(CancellationToken cancellationToken) - { - var success = await SetMute(true, cancellationToken).ConfigureAwait(true); - - if (!success) - { - await SetVolume(0, cancellationToken).ConfigureAwait(false); - } - } - - public async Task Unmute(CancellationToken cancellationToken) - { - var success = await SetMute(false, cancellationToken).ConfigureAwait(true); - - if (!success) - { - var sendVolume = _muteVol <= 0 ? 20 : _muteVol; - - await SetVolume(sendVolume, cancellationToken).ConfigureAwait(false); - } - } - - private DeviceService? GetServiceRenderingControl() - { - var services = Properties.Services; - - return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:RenderingControl:1", StringComparison.OrdinalIgnoreCase)) ?? - services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); - } - - private DeviceService? GetAvTransportService() - { - var services = Properties.Services; - - return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:AVTransport:1", StringComparison.OrdinalIgnoreCase)) ?? - services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:AVTransport", StringComparison.OrdinalIgnoreCase)); - } - - private async Task<bool> SetMute(bool mute, CancellationToken cancellationToken) - { - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); - if (command is null) - { - return false; - } - - var service = GetServiceRenderingControl(); - - if (service is null) - { - return false; - } - - _logger.LogDebug("Setting mute"); - var value = mute ? 1 : 0; - - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - IsMuted = mute; - - return true; - } - - /// <summary> - /// Sets volume on a scale of 0-100. - /// </summary> - /// <param name="value">The volume on a scale of 0-100.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public async Task SetVolume(int value, CancellationToken cancellationToken) - { - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume"); - if (command is null) - { - return; - } - - var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service"); - - // Set it early and assume it will succeed - // Remote control will perform better - Volume = value; - - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task Seek(TimeSpan value, CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek"); - if (command is null) - { - return; - } - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - RestartTimer(true); - } - - public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - url = url.Replace("&", "&", StringComparison.Ordinal); - - _logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI"); - if (command is null) - { - return; - } - - var dictionary = new Dictionary<string, string> - { - { "CurrentURI", url }, - { "CurrentURIMetaData", CreateDidlMeta(metaData) } - }; - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - post, - header: header, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - - try - { - await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); - } - catch - { - // Some devices will throw an error if you tell it to play when it's already playing - // Others won't - } - - RestartTimer(true); - } - - /* - * SetNextAvTransport is used to specify to the DLNA device what is the next track to play. - * Without that information, the next track command on the device does not work. - */ - public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - url = url.Replace("&", "&", StringComparison.Ordinal); - - _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); - if (command is null) - { - return; - } - - var dictionary = new Dictionary<string, string> - { - { "NextURI", url }, - { "NextURIMetaData", CreateDidlMeta(metaData) } - }; - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken) - .ConfigureAwait(false); - } - - private static string CreateDidlMeta(string value) - { - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - return SecurityElement.Escape(value); - } - - private Task SetPlay(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Play"); - if (command is null) - { - return Task.CompletedTask; - } - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands.BuildPost(command, service.ServiceType, 1), - cancellationToken: cancellationToken); - } - - public async Task SetPlay(CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - if (avCommands is null) - { - return; - } - - await SetPlay(avCommands, cancellationToken).ConfigureAwait(false); - - RestartTimer(true); - } - - public async Task SetStop(CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop"); - if (command is null) - { - return; - } - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - RestartTimer(true); - } - - public async Task SetPause(CancellationToken cancellationToken) - { - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause"); - if (command is null) - { - return; - } - - var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service"); - await new DlnaHttpClient(_logger, _httpClientFactory) - .SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - TransportState = TransportState.PAUSED_PLAYBACK; - - RestartTimer(true); - } - - private async void TimerCallback(object? sender) - { - if (_disposed) - { - return; - } - - try - { - var cancellationToken = CancellationToken.None; - - var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); - - if (avCommands is null) - { - return; - } - - var transportState = await GetTransportInfo(avCommands, cancellationToken).ConfigureAwait(false); - - if (_disposed) - { - return; - } - - if (transportState.HasValue) - { - // If we're not playing anything no need to get additional data - if (transportState.Value == TransportState.STOPPED) - { - UpdateMediaInfo(null, transportState.Value); - } - else - { - var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false); - - var currentObject = tuple.Track; - - if (tuple.Success && currentObject is null) - { - currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false); - } - - if (currentObject is not null) - { - UpdateMediaInfo(currentObject, transportState.Value); - } - } - - _connectFailureCount = 0; - - if (_disposed) - { - return; - } - - // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive - if (transportState.Value == TransportState.STOPPED) - { - RestartTimerInactive(); - } - else - { - RestartTimer(); - } - } - else - { - RestartTimerInactive(); - } - } - catch (Exception ex) - { - if (_disposed) - { - return; - } - - _logger.LogError(ex, "Error updating device info for {DeviceName}", Properties.Name); - - _connectFailureCount++; - - if (_connectFailureCount >= 3) - { - var action = OnDeviceUnavailable; - if (action is not null) - { - _logger.LogDebug("Disposing device due to loss of connection"); - action(); - return; - } - } - - RestartTimerInactive(); - } - } - - private async Task GetVolume(CancellationToken cancellationToken) - { - if (_disposed) - { - return; - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume"); - if (command is null) - { - return; - } - - var service = GetServiceRenderingControl(); - - if (service is null) - { - return; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType), // null checked above - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return; - } - - var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i is not null); - var volumeValue = volume?.Value; - - if (string.IsNullOrWhiteSpace(volumeValue)) - { - return; - } - - Volume = int.Parse(volumeValue, CultureInfo.InvariantCulture); - - if (Volume > 0) - { - _muteVol = Volume; - } - } - - private async Task GetMute(CancellationToken cancellationToken) - { - if (_disposed) - { - return; - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute"); - if (command is null) - { - return; - } - - var service = GetServiceRenderingControl(); - - if (service is null) - { - return; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands!.BuildPost(command, service.ServiceType), // null checked above - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return; - } - - var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse") - .Select(i => i.Element("CurrentMute")) - .FirstOrDefault(i => i is not null); - - IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase); - } - - private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo"); - if (command is null) - { - return null; - } - - var service = GetAvTransportService(); - if (service is null) - { - return null; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - avCommands.BuildPost(command, service.ServiceType), - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return null; - } - - var transportState = - result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i is not null); - - var transportStateValue = transportState?.Value; - - if (transportStateValue is not null - && Enum.TryParse(transportStateValue, true, out TransportState state)) - { - return state; - } - - return null; - } - - private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); - if (command is null) - { - return null; - } - - var service = GetAvTransportService(); - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - if (rendererCommands is null) - { - return null; - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands.BuildPost(command, service.ServiceType), - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return null; - } - - var track = result.Document.Descendants("CurrentURIMetaData").FirstOrDefault(); - - if (track is null) - { - return null; - } - - var e = track.Element(UPnpNamespaces.Items) ?? track; - - var elementString = (string)e; - - if (!string.IsNullOrWhiteSpace(elementString)) - { - return UpnpContainer.Create(e); - } - - track = result.Document.Descendants("CurrentURI").FirstOrDefault(); - - if (track is null) - { - return null; - } - - e = track.Element(UPnpNamespaces.Items) ?? track; - - elementString = (string)e; - - if (!string.IsNullOrWhiteSpace(elementString)) - { - return new UBaseObject - { - Url = elementString - }; - } - - return null; - } - - private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) - { - var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); - if (command is null) - { - return (false, null); - } - - var service = GetAvTransportService(); - - if (service is null) - { - throw new InvalidOperationException("Unable to find service"); - } - - var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - - if (rendererCommands is null) - { - return (false, null); - } - - var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( - Properties.BaseUrl, - service, - command.Name, - rendererCommands.BuildPost(command, service.ServiceType), - cancellationToken: cancellationToken).ConfigureAwait(false); - - if (result is null || result.Document is null) - { - return (false, null); - } - - var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i is not null); - var trackUri = trackUriElem?.Value; - - var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i is not null); - var duration = durationElem?.Value; - - if (!string.IsNullOrWhiteSpace(duration) - && !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) - { - Duration = TimeSpan.Parse(duration, CultureInfo.InvariantCulture); - } - else - { - Duration = null; - } - - var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i is not null); - var position = positionElem?.Value; - - if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) - { - Position = TimeSpan.Parse(position, CultureInfo.InvariantCulture); - } - - var track = result.Document.Descendants("TrackMetaData").FirstOrDefault(); - - if (track is null) - { - // If track is null, some vendors do this, use GetMediaInfo instead. - return (true, null); - } - - var trackString = (string)track; - - if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase)) - { - return (true, null); - } - - XElement? uPnpResponse = null; - - try - { - uPnpResponse = ParseResponse(trackString); - } - catch (Exception ex) - { - _logger.LogError(ex, "Uncaught exception while parsing xml"); - } - - if (uPnpResponse is null) - { - _logger.LogError("Failed to parse xml: \n {Xml}", trackString); - return (true, null); - } - - var e = uPnpResponse.Element(UPnpNamespaces.Items); - - var uTrack = CreateUBaseObject(e, trackUri); - - return (true, uTrack); - } - - private XElement? ParseResponse(string xml) - { - // Handle different variations sent back by devices. - try - { - return XElement.Parse(xml); - } - catch (XmlException) - { - } - - // first try to add a root node with a dlna namespace. - try - { - return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>") - .Descendants() - .First(); - } - catch (XmlException) - { - } - - // some devices send back invalid xml - try - { - return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal)); - } - catch (XmlException) - { - } - - return null; - } - - private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri) - { - ArgumentNullException.ThrowIfNull(container); - - var url = container.GetValue(UPnpNamespaces.Res); - - if (string.IsNullOrWhiteSpace(url)) - { - url = trackUri; - } - - return new UBaseObject - { - Id = container.GetAttributeValue(UPnpNamespaces.Id), - ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), - Title = container.GetValue(UPnpNamespaces.Title), - IconUrl = container.GetValue(UPnpNamespaces.Artwork), - SecondText = string.Empty, - Url = url, - ProtocolInfo = GetProtocolInfo(container), - MetaData = container.ToString() - }; - } - - private static string[] GetProtocolInfo(XElement container) - { - ArgumentNullException.ThrowIfNull(container); - - var resElement = container.Element(UPnpNamespaces.Res); - - var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo); - - if (info is not null && !string.IsNullOrWhiteSpace(info.Value)) - { - return info.Value.Split(':'); - } - - return new string[4]; - } - - private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken) - { - if (AvCommands is not null) - { - return AvCommands; - } - - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - var avService = GetAvTransportService(); - if (avService is null) - { - return null; - } - - string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - - var httpClient = new DlnaHttpClient(_logger, _httpClientFactory); - - var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); - if (document is null) - { - return null; - } - - AvCommands = TransportCommands.Create(document); - return AvCommands; - } - - private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken) - { - if (RendererCommands is not null) - { - return RendererCommands; - } - - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - var avService = GetServiceRenderingControl(); - ArgumentNullException.ThrowIfNull(avService); - - string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); - - var httpClient = new DlnaHttpClient(_logger, _httpClientFactory); - _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync"); - var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false); - if (document is null) - { - return null; - } - - RendererCommands = TransportCommands.Create(document); - return RendererCommands; - } - - private string NormalizeUrl(string baseUrl, string url) - { - // If it's already a complete url, don't stick anything onto the front of it - if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return url; - } - - if (!url.Contains('/', StringComparison.Ordinal)) - { - url = "/dmr/" + url; - } - - if (!url.StartsWith('/')) - { - url = "/" + url; - } - - return baseUrl + url; - } - - public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) - { - var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory); - - var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false); - if (document is null) - { - return null; - } - - var friendlyNames = new List<string>(); - - var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault(); - if (name is not null && !string.IsNullOrWhiteSpace(name.Value)) - { - friendlyNames.Add(name.Value); - } - - var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault(); - if (room is not null && !string.IsNullOrWhiteSpace(room.Value)) - { - friendlyNames.Add(room.Value); - } - - var deviceProperties = new DeviceInfo() - { - Name = string.Join(' ', friendlyNames), - BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port) - }; - - var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault(); - if (model is not null) - { - deviceProperties.ModelName = model.Value; - } - - var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault(); - if (modelNumber is not null) - { - deviceProperties.ModelNumber = modelNumber.Value; - } - - var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault(); - if (uuid is not null) - { - deviceProperties.UUID = uuid.Value; - } - - var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault(); - if (manufacturer is not null) - { - deviceProperties.Manufacturer = manufacturer.Value; - } - - var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault(); - if (manufacturerUrl is not null) - { - deviceProperties.ManufacturerUrl = manufacturerUrl.Value; - } - - var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault(); - if (presentationUrl is not null) - { - deviceProperties.PresentationUrl = presentationUrl.Value; - } - - var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault(); - if (modelUrl is not null) - { - deviceProperties.ModelUrl = modelUrl.Value; - } - - var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault(); - if (serialNumber is not null) - { - deviceProperties.SerialNumber = serialNumber.Value; - } - - var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault(); - if (modelDescription is not null) - { - deviceProperties.ModelDescription = modelDescription.Value; - } - - var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault(); - if (icon is not null) - { - deviceProperties.Icon = CreateIcon(icon); - } - - foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList"))) - { - if (services is null) - { - continue; - } - - var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service")); - if (servicesList is null) - { - continue; - } - - foreach (var element in servicesList) - { - var service = Create(element); - - if (service is not null) - { - deviceProperties.Services.Add(service); - } - } - } - - return new Device(deviceProperties, httpClientFactory, logger); - } - - private static DeviceIcon CreateIcon(XElement element) - { - ArgumentNullException.ThrowIfNull(element); - - var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width")); - var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height")); - - _ = int.TryParse(width, NumberStyles.Integer, CultureInfo.InvariantCulture, out var widthValue); - _ = int.TryParse(height, NumberStyles.Integer, CultureInfo.InvariantCulture, out var heightValue); - - return new DeviceIcon - { - Depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth")) ?? string.Empty, - Height = heightValue, - MimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype")) ?? string.Empty, - Url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url")) ?? string.Empty, - Width = widthValue - }; - } - - private static DeviceService Create(XElement element) - => new DeviceService() - { - ControlUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL")) ?? string.Empty, - EventSubUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL")) ?? string.Empty, - ScpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL")) ?? string.Empty, - ServiceId = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId")) ?? string.Empty, - ServiceType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType")) ?? string.Empty - }; - - private void UpdateMediaInfo(UBaseObject? mediaInfo, TransportState state) - { - TransportState = state; - - var previousMediaInfo = CurrentMediaInfo; - CurrentMediaInfo = mediaInfo; - - if (mediaInfo is null) - { - if (previousMediaInfo is not null) - { - OnPlaybackStop(previousMediaInfo); - } - } - else if (previousMediaInfo is null) - { - if (state != TransportState.STOPPED) - { - OnPlaybackStart(mediaInfo); - } - } - else if (mediaInfo.Equals(previousMediaInfo)) - { - OnPlaybackProgress(mediaInfo); - } - else - { - OnMediaChanged(previousMediaInfo, mediaInfo); - } - } - - private void OnPlaybackStart(UBaseObject mediaInfo) - { - if (string.IsNullOrWhiteSpace(mediaInfo.Url)) - { - return; - } - - PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo)); - } - - private void OnPlaybackProgress(UBaseObject mediaInfo) - { - if (string.IsNullOrWhiteSpace(mediaInfo.Url)) - { - return; - } - - PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo)); - } - - private void OnPlaybackStop(UBaseObject mediaInfo) - { - PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo)); - } - - private void OnMediaChanged(UBaseObject old, UBaseObject newMedia) - { - MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia)); - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <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> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _timer?.Dispose(); - _timer = null; - Properties = null!; - } - - _disposed = true; - } - - /// <inheritdoc /> - public override string ToString() - { - return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl); - } - } -} diff --git a/Emby.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs deleted file mode 100644 index 2acfff4eb6..0000000000 --- a/Emby.Dlna/PlayTo/DeviceInfo.cs +++ /dev/null @@ -1,66 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using Emby.Dlna.Common; -using MediaBrowser.Model.Dlna; - -namespace Emby.Dlna.PlayTo -{ - public class DeviceInfo - { - private readonly List<DeviceService> _services = new List<DeviceService>(); - private string _baseUrl = string.Empty; - - public DeviceInfo() - { - Name = "Generic Device"; - } - - public string UUID { get; set; } - - public string Name { get; set; } - - public string ModelName { get; set; } - - public string ModelNumber { get; set; } - - public string ModelDescription { get; set; } - - public string ModelUrl { get; set; } - - public string Manufacturer { get; set; } - - public string SerialNumber { get; set; } - - public string ManufacturerUrl { get; set; } - - public string PresentationUrl { get; set; } - - public string BaseUrl - { - get => _baseUrl; - set => _baseUrl = value; - } - - public DeviceIcon Icon { get; set; } - - public List<DeviceService> Services => _services; - - public DeviceIdentification ToDeviceIdentification() - { - return new DeviceIdentification - { - Manufacturer = Manufacturer, - ModelName = ModelName, - ModelNumber = ModelNumber, - FriendlyName = Name, - ManufacturerUrl = ManufacturerUrl, - ModelUrl = ModelUrl, - ModelDescription = ModelDescription, - SerialNumber = SerialNumber - }; - } - } -} diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs deleted file mode 100644 index 255c51f19a..0000000000 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ /dev/null @@ -1,137 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net.Http; -using System.Net.Mime; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; -using Emby.Dlna.Common; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.PlayTo -{ - /// <summary> - /// Http client for Dlna PlayTo function. - /// </summary> - public partial class DlnaHttpClient - { - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - - public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - } - - [GeneratedRegex("(&(?![a-z]*;))")] - private static partial Regex EscapeAmpersandRegex(); - - private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) - { - // If it's already a complete url, don't stick anything onto the front of it - if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return serviceUrl; - } - - if (!serviceUrl.StartsWith('/')) - { - serviceUrl = "/" + serviceUrl; - } - - return baseUrl + serviceUrl; - } - - private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var client = _httpClientFactory.CreateClient(NamedClient.Dlna); - using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) - { - try - { - return await XDocument.LoadAsync( - stream, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - catch (XmlException) - { - // try correcting the Xml response with common errors - stream.Position = 0; - using StreamReader sr = new StreamReader(stream); - var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - - // find and replace unescaped ampersands (&) - xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); - - try - { - // retry reading Xml - using var xmlReader = new StringReader(xmlString); - return await XDocument.LoadAsync( - xmlReader, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - catch (XmlException ex) - { - _logger.LogError(ex, "Failed to parse response"); - _logger.LogDebug("Malformed response: {Content}\n", xmlString); - - return null; - } - } - } - } - - public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken) - { - using var request = new HttpRequestMessage(HttpMethod.Get, url); - - // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon - return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - } - - public async Task<XDocument?> SendCommandAsync( - string baseUrl, - DeviceService service, - string command, - string postData, - string? header = null, - CancellationToken cancellationToken = default) - { - using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl)) - { - Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml) - }; - - request.Headers.TryAddWithoutValidation( - "SOAPACTION", - string.Format( - CultureInfo.InvariantCulture, - "\"{0}#{1}\"", - service.ServiceType, - command)); - request.Headers.Pragma.ParseAdd("no-cache"); - - if (!string.IsNullOrEmpty(header)) - { - request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header); - } - - // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon - return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs b/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs deleted file mode 100644 index 0f7a524d62..0000000000 --- a/Emby.Dlna/PlayTo/MediaChangedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class MediaChangedEventArgs : EventArgs - { - public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo) - { - OldMediaInfo = oldMediaInfo; - NewMediaInfo = newMediaInfo; - } - - public UBaseObject OldMediaInfo { get; set; } - - public UBaseObject NewMediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs deleted file mode 100644 index f70ebf3ebc..0000000000 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ /dev/null @@ -1,980 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Emby.Dlna.Didl; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Events; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Session; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; -using Photo = MediaBrowser.Controller.Entities.Photo; - -namespace Emby.Dlna.PlayTo -{ - 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; - - private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); - private Device _device; - private int _currentPlaylistIndex; - - private bool _disposed; - - 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, - IMediaEncoder mediaEncoder, - Device device) - { - _session = session; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - _logger = logger; - _dlnaManager = dlnaManager; - _userManager = userManager; - _imageProcessor = imageProcessor; - _serverAddress = serverAddress; - _accessToken = accessToken; - _deviceDiscovery = deviceDiscovery; - _userDataManager = userDataManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _mediaEncoder = mediaEncoder; - - _device = device; - _device.OnDeviceUnavailable = OnDeviceUnavailable; - _device.PlaybackStart += OnDevicePlaybackStart; - _device.PlaybackProgress += OnDevicePlaybackProgress; - _device.PlaybackStopped += OnDevicePlaybackStopped; - _device.MediaChanged += OnDeviceMediaChanged; - - _device.Start(); - - _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; - } - - 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); - } - } - - private void OnDeviceUnavailable() - { - try - { - _sessionManager.ReportSessionEnded(_session.Id); - } - catch (Exception ex) - { - // Could throw if the session is already gone - _logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id); - } - } - - private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e) - { - var info = e.Argument; - - if (!_disposed - && info.Headers.TryGetValue("USN", out string? usn) - && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 - && (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 - || (info.Headers.TryGetValue("NT", out string? nt) - && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1))) - { - OnDeviceUnavailable(); - } - } - - private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e) - { - if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url)) - { - return; - } - - try - { - var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager); - if (streamInfo.Item is not null) - { - var positionTicks = GetProgressPositionTicks(streamInfo); - - await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false); - } - - streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager); - if (streamInfo.Item is null) - { - return; - } - - var newItemProgress = GetProgressInfo(streamInfo); - - 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; - } - - await SendNextTrackMessage(currentItemIndex, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e) - { - if (_disposed) - { - return; - } - - try - { - var streamInfo = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager); - - if (streamInfo.Item is null) - { - return; - } - - var positionTicks = GetProgressPositionTicks(streamInfo); - - await ReportPlaybackStopped(streamInfo, positionTicks).ConfigureAwait(false); - - var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false); - - var duration = mediaSource is null - ? _device.Duration?.Ticks - : mediaSource.RunTimeTicks; - - var playedToCompletion = positionTicks.HasValue && positionTicks.Value == 0; - - 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 - { - _playlist.Clear(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting playback stopped"); - } - } - - private async Task ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks) - { - try - { - await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo - { - ItemId = streamInfo.ItemId, - SessionId = _session.Id, - PositionTicks = positionTicks, - MediaSourceId = streamInfo.MediaSourceId - }).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e) - { - if (_disposed) - { - return; - } - - try - { - var info = StreamParams.ParseFromUrl(e.MediaInfo.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var progress = GetProgressInfo(info); - - await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e) - { - if (_disposed) - { - return; - } - - try - { - var mediaUrl = e.MediaInfo.Url; - - if (string.IsNullOrWhiteSpace(mediaUrl)) - { - return; - } - - var info = StreamParams.ParseFromUrl(mediaUrl, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var progress = GetProgressInfo(info); - - await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reporting progress"); - } - } - - private long? GetProgressPositionTicks(StreamParams info) - { - var ticks = _device.Position.Ticks; - - if (!EnableClientSideSeek(info)) - { - ticks += info.StartPositionTicks; - } - - return ticks; - } - - private PlaybackStartInfo GetProgressInfo(StreamParams info) - { - return new PlaybackStartInfo - { - ItemId = info.ItemId, - SessionId = _session.Id, - PositionTicks = GetProgressPositionTicks(info), - IsMuted = _device.IsMuted, - IsPaused = _device.IsPaused, - MediaSourceId = info.MediaSourceId, - AudioStreamIndex = info.AudioStreamIndex, - SubtitleStreamIndex = info.SubtitleStreamIndex, - VolumeLevel = _device.Volume, - - CanSeek = true, - - PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode - }; - } - - public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) - { - _logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand); - - var user = command.ControllingUserId.Equals(default) - ? null : - _userManager.GetUserById(command.ControllingUserId); - - var items = new List<BaseItem>(); - foreach (var id in command.ItemIds) - { - AddItemFromId(id, items); - } - - var startIndex = command.StartIndex ?? 0; - int len = items.Count - startIndex; - if (startIndex > 0) - { - items = items.GetRange(startIndex, len); - } - - var playlist = new PlaylistItem[len]; - - // Not nullable enabled - so this is required. - playlist[0] = CreatePlaylistItem( - items[0], - user, - command.StartPositionTicks ?? 0, - command.MediaSourceId ?? string.Empty, - command.AudioStreamIndex, - command.SubtitleStreamIndex); - - for (int i = 1; i < len; i++) - { - playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null); - } - - _logger.LogDebug("{0} - Playlist created", _session.DeviceName); - - if (command.PlayCommand == PlayCommand.PlayLast) - { - _playlist.AddRange(playlist); - } - - if (command.PlayCommand == PlayCommand.PlayNext) - { - _playlist.AddRange(playlist); - } - - if (!command.ControllingUserId.Equals(default)) - { - _sessionManager.LogSessionActivity( - _session.Client, - _session.ApplicationVersion, - _session.DeviceId, - _session.DeviceName, - _session.RemoteEndPoint, - user); - } - - return PlayItems(playlist, cancellationToken); - } - - private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) - { - switch (command.Command) - { - case PlaystateCommand.Stop: - _playlist.Clear(); - return _device.SetStop(CancellationToken.None); - - case PlaystateCommand.Pause: - return _device.SetPause(CancellationToken.None); - - case PlaystateCommand.Unpause: - return _device.SetPlay(CancellationToken.None); - - case PlaystateCommand.PlayPause: - return _device.IsPaused ? _device.SetPlay(CancellationToken.None) : _device.SetPause(CancellationToken.None); - - case PlaystateCommand.Seek: - return Seek(command.SeekPositionTicks ?? 0); - - case PlaystateCommand.NextTrack: - return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken); - - case PlaystateCommand.PreviousTrack: - return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken); - } - - return Task.CompletedTask; - } - - private async Task Seek(long newPosition) - { - var media = _device.CurrentMediaInfo; - - if (media is not null) - { - var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null && !EnableClientSideSeek(info)) - { - var user = _session.UserId.Equals(default) - ? null - : _userManager.GetUserById(_session.UserId); - var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex); - - 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); - await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false); - - return; - } - - await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); - } - } - - 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); - } - } - - private PlaylistItem CreatePlaylistItem( - BaseItem item, - User? user, - long startPostionTicks, - string? mediaSourceId, - int? audioStreamIndex, - int? subtitleStreamIndex) - { - var deviceInfo = _device.Properties; - - var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ?? - _dlnaManager.GetDefaultProfile(); - - var mediaSources = item is IHasMediaSources - ? _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray() - : Array.Empty<MediaSourceInfo>(); - - var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); - playlistItem.StreamInfo.StartPositionTicks = startPostionTicks; - - playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken)); - - 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); - - playlistItem.Didl = itemXml; - - return playlistItem; - } - - private string? GetDlnaHeaders(PlaylistItem item) - { - var profile = item.Profile; - var streamInfo = item.StreamInfo; - - if (streamInfo.MediaType == DlnaProfileType.Audio) - { - return ContentFeatureBuilder.BuildAudioHeader( - profile, - streamInfo.Container, - streamInfo.TargetAudioCodec.FirstOrDefault(), - streamInfo.TargetAudioBitrate, - streamInfo.TargetAudioSampleRate, - streamInfo.TargetAudioChannels, - streamInfo.TargetAudioBitDepth, - streamInfo.IsDirectStream, - streamInfo.RunTimeTicks ?? 0, - streamInfo.TranscodeSeekInfo); - } - - if (streamInfo.MediaType == DlnaProfileType.Video) - { - var list = ContentFeatureBuilder.BuildVideoHeader( - profile, - 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, - streamInfo.TargetVideoLevel, - streamInfo.TargetFramerate ?? 0, - streamInfo.TargetPacketLength, - streamInfo.TranscodeSeekInfo, - streamInfo.IsTargetAnamorphic, - streamInfo.IsTargetInterlaced, - streamInfo.TargetRefFrames, - streamInfo.TargetVideoStreamCount, - streamInfo.TargetAudioStreamCount, - streamInfo.TargetVideoCodecTag, - streamInfo.IsTargetAVC); - - return list.FirstOrDefault(); - } - - return null; - } - - private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex) - { - if (item.MediaType == MediaType.Video) - { - return new PlaylistItem - { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalVideoStream(new MediaOptions - { - ItemId = item.Id, - MediaSources = mediaSources, - Profile = profile, - DeviceId = deviceId, - MaxBitrate = profile.MaxStreamingBitrate, - MediaSourceId = mediaSourceId, - AudioStreamIndex = audioStreamIndex, - SubtitleStreamIndex = subtitleStreamIndex - }), - - Profile = profile - }; - } - - if (item.MediaType == MediaType.Audio) - { - return new PlaylistItem - { - StreamInfo = new StreamBuilder(_mediaEncoder, _logger).GetOptimalAudioStream(new MediaOptions - { - ItemId = item.Id, - MediaSources = mediaSources, - Profile = profile, - DeviceId = deviceId, - MaxBitrate = profile.MaxStreamingBitrate, - MediaSourceId = mediaSourceId - }), - - Profile = profile - }; - } - - if (item.MediaType == MediaType.Photo) - { - return PlaylistItemFactory.Create((Photo)item, profile); - } - - throw new ArgumentException("Unrecognized item type."); - } - - /// <summary> - /// Plays the items. - /// </summary> - /// <param name="items">The items.</param> - /// <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) - { - _playlist.Clear(); - _playlist.AddRange(items); - _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count); - - await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false); - return true; - } - - private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default) - { - if (index < 0 || index >= _playlist.Count) - { - _playlist.Clear(); - await _device.SetStop(cancellationToken).ConfigureAwait(false); - return; - } - - _currentPlaylistIndex = index; - var currentitem = _playlist[index]; - - await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); - - // Send a message to the DLNA device to notify what is the next track in the play list. - await SendNextTrackMessage(index, cancellationToken).ConfigureAwait(false); - - var streamInfo = currentitem.StreamInfo; - if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) - { - await SeekAfterTransportChange(streamInfo.StartPositionTicks, CancellationToken.None).ConfigureAwait(false); - } - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <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> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _device.PlaybackStart -= OnDevicePlaybackStart; - _device.PlaybackProgress -= OnDevicePlaybackProgress; - _device.PlaybackStopped -= OnDevicePlaybackStopped; - _device.MediaChanged -= OnDeviceMediaChanged; - _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; - _device.OnDeviceUnavailable = null; - _device.Dispose(); - } - - _disposed = true; - } - - private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) - { - 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: - if (command.Arguments.TryGetValue("Index", out string? index)) - { - if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return SetAudioStreamIndex(val); - } - - throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied."); - } - - throw new ArgumentException("SetAudioStreamIndex argument cannot be null"); - case GeneralCommandType.SetSubtitleStreamIndex: - if (command.Arguments.TryGetValue("Index", out index)) - { - if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - return SetSubtitleStreamIndex(val); - } - - throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied."); - } - - throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null"); - case GeneralCommandType.SetVolume: - if (command.Arguments.TryGetValue("Volume", out string? vol)) - { - if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume)) - { - return _device.SetVolume(volume, cancellationToken); - } - - throw new ArgumentException("Unsupported volume value supplied."); - } - - throw new ArgumentException("Volume argument cannot be null"); - default: - return Task.CompletedTask; - } - } - - private async Task SetAudioStreamIndex(int? newIndex) - { - var media = _device.CurrentMediaInfo; - - if (media is not null) - { - var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var newPosition = GetProgressPositionTicks(info) ?? 0; - - var user = _session.UserId.Equals(default) - ? null - : _userManager.GetUserById(_session.UserId); - var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex); - - 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); - await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false); - - if (EnableClientSideSeek(newItem.StreamInfo)) - { - await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); - } - } - } - } - - private async Task SetSubtitleStreamIndex(int? newIndex) - { - var media = _device.CurrentMediaInfo; - - if (media is not null) - { - var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager); - - if (info.Item is not null) - { - var newPosition = GetProgressPositionTicks(info) ?? 0; - - var user = _session.UserId.Equals(default) - ? null - : _userManager.GetUserById(_session.UserId); - var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex); - - 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); - await SendNextTrackMessage(newItemIndex, CancellationToken.None).ConfigureAwait(false); - - if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0) - { - await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false); - } - } - } - } - - private async Task SeekAfterTransportChange(long positionTicks, CancellationToken cancellationToken) - { - const int MaxWait = 15000000; - const int Interval = 500; - - var currentWait = 0; - while (_device.TransportState != TransportState.PLAYING && currentWait < MaxWait) - { - await Task.Delay(Interval, cancellationToken).ConfigureAwait(false); - currentWait += Interval; - } - - await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false); - } - - 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) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } - - return name switch - { - 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 - }; - } - - private class StreamParams - { - private MediaSourceInfo? _mediaSource; - private IMediaSourceManager? _mediaSourceManager; - - public Guid ItemId { get; set; } - - public bool IsDirectStream { get; set; } - - public long StartPositionTicks { get; set; } - - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public string? DeviceProfileId { get; set; } - - public string? DeviceId { get; set; } - - public string? MediaSourceId { get; set; } - - public string? LiveStreamId { get; set; } - - public BaseItem? Item { get; set; } - - public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken) - { - if (_mediaSource is not null) - { - return _mediaSource; - } - - if (Item is not IHasMediaSources) - { - return null; - } - - if (_mediaSourceManager is not null) - { - _mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false); - } - - return _mediaSource; - } - - private static Guid GetItemId(string url) - { - ArgumentException.ThrowIfNullOrEmpty(url); - - var parts = url.Split('/'); - - for (var i = 0; i < parts.Length - 1; i++) - { - var part = parts[i]; - - if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) - || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase)) - { - if (Guid.TryParse(parts[i + 1], out var result)) - { - return result; - } - } - } - - return default; - } - - public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager) - { - ArgumentException.ThrowIfNullOrEmpty(url); - - var request = new StreamParams - { - ItemId = GetItemId(url) - }; - - if (request.ItemId.Equals(default)) - { - return request; - } - - var index = url.IndexOf('?', StringComparison.Ordinal); - if (index == -1) - { - return request; - } - - var query = url.Substring(index + 1); - Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); - - request.DeviceProfileId = values.GetValueOrDefault("DeviceProfileId"); - request.DeviceId = values.GetValueOrDefault("DeviceId"); - request.MediaSourceId = values.GetValueOrDefault("MediaSourceId"); - request.LiveStreamId = values.GetValueOrDefault("LiveStreamId"); - request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase); - request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex"); - request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex"); - request.StartPositionTicks = GetLongValue(values, "StartPositionTicks"); - - request.Item = libraryManager.GetItemById(request.ItemId); - - request._mediaSourceManager = mediaSourceManager; - - return request; - } - } - } -} diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs deleted file mode 100644 index b05e0a0957..0000000000 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ /dev/null @@ -1,258 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Events; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; - -namespace Emby.Dlna.PlayTo -{ - public sealed class PlayToManager : IDisposable - { - private readonly ILogger _logger; - private readonly ISessionManager _sessionManager; - - private readonly ILibraryManager _libraryManager; - private readonly IUserManager _userManager; - private readonly IDlnaManager _dlnaManager; - private readonly IServerApplicationHost _appHost; - private readonly IImageProcessor _imageProcessor; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IUserDataManager _userDataManager; - private readonly ILocalizationManager _localization; - - private readonly IDeviceDiscovery _deviceDiscovery; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IMediaEncoder _mediaEncoder; - - private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); - private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - private bool _disposed; - - public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) - { - _logger = logger; - _sessionManager = sessionManager; - _libraryManager = libraryManager; - _userManager = userManager; - _dlnaManager = dlnaManager; - _appHost = appHost; - _imageProcessor = imageProcessor; - _deviceDiscovery = deviceDiscovery; - _httpClientFactory = httpClientFactory; - _userDataManager = userDataManager; - _localization = localization; - _mediaSourceManager = mediaSourceManager; - _mediaEncoder = mediaEncoder; - } - - public void Start() - { - _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; - } - - private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e) - { - if (_disposed) - { - return; - } - - var info = e.Argument; - - if (!info.Headers.TryGetValue("USN", out string? usn)) - { - usn = string.Empty; - } - - if (!info.Headers.TryGetValue("NT", out string? nt)) - { - nt = string.Empty; - } - - // It has to report that it's a media renderer - if (!usn.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase) - && !nt.Contains("MediaRenderer:", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - var cancellationToken = _disposeCancellationTokenSource.Token; - - await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - if (_disposed) - { - return; - } - - if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1)) - { - return; - } - - await AddDevice(info, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating PlayTo device."); - } - finally - { - _sessionLock.Release(); - } - } - - internal static string GetUuid(string usn) - { - const string UuidStr = "uuid:"; - const string UuidColonStr = "::"; - - var index = usn.IndexOf(UuidStr, StringComparison.OrdinalIgnoreCase); - if (index == -1) - { - return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - ReadOnlySpan<char> tmp = usn.AsSpan()[(index + UuidStr.Length)..]; - - index = tmp.IndexOf(UuidColonStr, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - tmp = tmp[..index]; - } - - index = tmp.IndexOf('{'); - if (index != -1) - { - int endIndex = tmp.IndexOf('}'); - if (endIndex != -1) - { - tmp = tmp[(index + 1)..endIndex]; - } - } - - return tmp.ToString(); - } - - private async Task AddDevice(UpnpDeviceInfo info, CancellationToken cancellationToken) - { - var uri = info.Location; - _logger.LogDebug("Attempting to create PlayToController from location {0}", uri); - - if (info.Headers.TryGetValue("USN", out string? uuid)) - { - uuid = GetUuid(uuid); - } - else - { - uuid = uri.ToString().GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - var sessionInfo = await _sessionManager - .LogSessionActivity("DLNA", _appHost.ApplicationVersionString, uuid, null, uri.OriginalString, null) - .ConfigureAwait(false); - - var controller = sessionInfo.SessionControllers.OfType<PlayToController>().FirstOrDefault(); - - if (controller is null) - { - var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false); - if (device is null) - { - _logger.LogError("Ignoring device as xml response is invalid."); - return; - } - - string deviceName = device.Properties.Name; - - _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - - string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress); - - controller = new PlayToController( - sessionInfo, - _sessionManager, - _libraryManager, - _logger, - _dlnaManager, - _userManager, - _imageProcessor, - serverAddress, - null, - _deviceDiscovery, - _userDataManager, - _localization, - _mediaSourceManager, - _mediaEncoder, - device); - - sessionInfo.AddController(controller); - - var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ?? - _dlnaManager.GetDefaultProfile(); - - _sessionManager.ReportCapabilities(sessionInfo.Id, new ClientCapabilities - { - PlayableMediaTypes = profile.GetSupportedMediaTypes(), - - SupportedCommands = new[] - { - GeneralCommandType.VolumeDown, - GeneralCommandType.VolumeUp, - GeneralCommandType.Mute, - GeneralCommandType.Unmute, - GeneralCommandType.ToggleMute, - GeneralCommandType.SetVolume, - GeneralCommandType.SetAudioStreamIndex, - GeneralCommandType.SetSubtitleStreamIndex, - GeneralCommandType.PlayMediaSource - }, - - SupportsMediaControl = true - }); - - _logger.LogInformation("DLNA Session created for {0} - {1}", device.Properties.Name, device.Properties.ModelName); - } - } - - /// <inheritdoc /> - public void Dispose() - { - _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; - - try - { - _disposeCancellationTokenSource.Cancel(); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error while disposing PlayToManager"); - } - - _sessionLock.Dispose(); - _disposeCancellationTokenSource.Dispose(); - - _disposed = true; - } - } -} diff --git a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs deleted file mode 100644 index c95d8b1e84..0000000000 --- a/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class PlaybackProgressEventArgs : EventArgs - { - public PlaybackProgressEventArgs(UBaseObject mediaInfo) - { - MediaInfo = mediaInfo; - } - - public UBaseObject MediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs deleted file mode 100644 index 619c861ed9..0000000000 --- a/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class PlaybackStartEventArgs : EventArgs - { - public PlaybackStartEventArgs(UBaseObject mediaInfo) - { - MediaInfo = mediaInfo; - } - - public UBaseObject MediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs deleted file mode 100644 index d0ec250591..0000000000 --- a/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Dlna.PlayTo -{ - public class PlaybackStoppedEventArgs : EventArgs - { - public PlaybackStoppedEventArgs(UBaseObject mediaInfo) - { - MediaInfo = mediaInfo; - } - - public UBaseObject MediaInfo { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs deleted file mode 100644 index 5056e69ae7..0000000000 --- a/Emby.Dlna/PlayTo/PlaylistItem.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using MediaBrowser.Model.Dlna; - -namespace Emby.Dlna.PlayTo -{ - public class PlaylistItem - { - public string StreamUrl { get; set; } - - public string Didl { get; set; } - - public StreamInfo StreamInfo { get; set; } - - public DeviceProfile Profile { get; set; } - } -} diff --git a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs deleted file mode 100644 index 53cd05cfda..0000000000 --- a/Emby.Dlna/PlayTo/PlaylistItemFactory.cs +++ /dev/null @@ -1,70 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.IO; -using System.Linq; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Session; - -namespace Emby.Dlna.PlayTo -{ - public static class PlaylistItemFactory - { - public static PlaylistItem Create(Photo item, DeviceProfile profile) - { - var playlistItem = new PlaylistItem - { - StreamInfo = new StreamInfo - { - ItemId = item.Id, - MediaType = DlnaProfileType.Photo, - DeviceProfile = profile - }, - - Profile = profile - }; - - var directPlay = profile.DirectPlayProfiles - .FirstOrDefault(i => i.Type == DlnaProfileType.Photo && IsSupported(i, item)); - - if (directPlay is not null) - { - playlistItem.StreamInfo.PlayMethod = PlayMethod.DirectStream; - playlistItem.StreamInfo.Container = Path.GetExtension(item.Path); - - return playlistItem; - } - - var transcodingProfile = profile.TranscodingProfiles - .FirstOrDefault(i => i.Type == DlnaProfileType.Photo); - - if (transcodingProfile is not null) - { - playlistItem.StreamInfo.PlayMethod = PlayMethod.Transcode; - playlistItem.StreamInfo.Container = "." + transcodingProfile.Container.TrimStart('.'); - } - - return playlistItem; - } - - private static bool IsSupported(DirectPlayProfile profile, Photo item) - { - var mediaPath = item.Path; - - if (profile.Container.Length > 0) - { - // Check container type - var mediaContainer = (Path.GetExtension(mediaPath) ?? string.Empty).TrimStart('.'); - - if (!profile.SupportsContainer(mediaContainer)) - { - return false; - } - } - - return true; - } - } -} diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs deleted file mode 100644 index 6b2096d9dc..0000000000 --- a/Emby.Dlna/PlayTo/TransportCommands.cs +++ /dev/null @@ -1,181 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml.Linq; -using Emby.Dlna.Common; -using Emby.Dlna.Ssdp; - -namespace Emby.Dlna.PlayTo -{ - public class TransportCommands - { - private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>"; - - public List<StateVariable> StateVariables { get; } = new List<StateVariable>(); - - public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>(); - - public static TransportCommands Create(XDocument document) - { - var command = new TransportCommands(); - - var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList"); - - foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action")) - { - command.ServiceActions.Add(ServiceActionFromXml(container)); - } - - var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault(); - - if (stateValues is not null) - { - foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable")) - { - command.StateVariables.Add(FromXml(container)); - } - } - - return command; - } - - private static ServiceAction ServiceActionFromXml(XElement container) - { - var serviceAction = new ServiceAction - { - Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, - }; - - var argumentList = serviceAction.ArgumentList; - - foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument")) - { - argumentList.Add(ArgumentFromXml(arg)); - } - - return serviceAction; - } - - private static Argument ArgumentFromXml(XElement container) - { - ArgumentNullException.ThrowIfNull(container); - - return new Argument - { - Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, - Direction = container.GetValue(UPnpNamespaces.Svc + "direction") ?? string.Empty, - RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable") ?? string.Empty - }; - } - - private static StateVariable FromXml(XElement container) - { - var allowedValues = Array.Empty<string>(); - var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList") - .FirstOrDefault(); - - if (element is not null) - { - var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue"); - - allowedValues = values.Select(child => child.Value).ToArray(); - } - - return new StateVariable - { - Name = container.GetValue(UPnpNamespaces.Svc + "name") ?? string.Empty, - DataType = container.GetValue(UPnpNamespaces.Svc + "dataType") ?? string.Empty, - AllowedValues = allowedValues - }; - } - - public string BuildPost(ServiceAction action, string xmlNamespace) - { - var stateString = string.Empty; - - foreach (var arg in action.ArgumentList) - { - if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) - { - continue; - } - - if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) - { - stateString += BuildArgumentXml(arg, "0"); - } - else - { - stateString += BuildArgumentXml(arg, null); - } - } - - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); - } - - public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "") - { - var stateString = string.Empty; - - foreach (var arg in action.ArgumentList) - { - if (string.Equals(arg.Direction, "out", StringComparison.Ordinal)) - { - continue; - } - - if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) - { - stateString += BuildArgumentXml(arg, "0"); - } - else - { - stateString += BuildArgumentXml(arg, value.ToString(), commandParameter); - } - } - - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); - } - - public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary) - { - var stateString = string.Empty; - - foreach (var arg in action.ArgumentList) - { - if (string.Equals(arg.Name, "InstanceID", StringComparison.Ordinal)) - { - stateString += BuildArgumentXml(arg, "0"); - } - else if (dictionary.TryGetValue(arg.Name, out var argValue)) - { - stateString += BuildArgumentXml(arg, argValue); - } - else - { - stateString += BuildArgumentXml(arg, value.ToString()); - } - } - - return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString); - } - - private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "") - { - var state = StateVariables.FirstOrDefault(a => string.Equals(a.Name, argument.RelatedStateVariable, StringComparison.OrdinalIgnoreCase)); - - if (state is not null) - { - var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ?? - (state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value); - - return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType, sendValue); - } - - return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value); - } - } -} diff --git a/Emby.Dlna/PlayTo/TransportState.cs b/Emby.Dlna/PlayTo/TransportState.cs deleted file mode 100644 index 0d6a78438c..0000000000 --- a/Emby.Dlna/PlayTo/TransportState.cs +++ /dev/null @@ -1,16 +0,0 @@ -#pragma warning disable CS1591 - -namespace Emby.Dlna.PlayTo -{ - /// <summary> - /// Core of the AVTransport service. It defines the conceptually top- - /// level state of the transport, for example, whether it is playing, recording, etc. - /// </summary> - public enum TransportState - { - STOPPED, - PLAYING, - TRANSITIONING, - PAUSED_PLAYBACK - } -} diff --git a/Emby.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs deleted file mode 100644 index 017d51e606..0000000000 --- a/Emby.Dlna/PlayTo/UpnpContainer.cs +++ /dev/null @@ -1,25 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Xml.Linq; -using Emby.Dlna.Ssdp; - -namespace Emby.Dlna.PlayTo -{ - public class UpnpContainer : UBaseObject - { - public static UBaseObject Create(XElement container) - { - ArgumentNullException.ThrowIfNull(container); - - return new UBaseObject - { - Id = container.GetAttributeValue(UPnpNamespaces.Id), - ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId), - Title = container.GetValue(UPnpNamespaces.Title), - IconUrl = container.GetValue(UPnpNamespaces.Artwork), - UpnpClass = container.GetValue(UPnpNamespaces.Class) - }; - } - } -} diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs deleted file mode 100644 index a8f451405c..0000000000 --- a/Emby.Dlna/PlayTo/uBaseObject.cs +++ /dev/null @@ -1,63 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using Jellyfin.Data.Enums; - -namespace Emby.Dlna.PlayTo -{ - public class UBaseObject - { - public string Id { get; set; } - - public string ParentId { get; set; } - - public string Title { get; set; } - - public string SecondText { get; set; } - - public string IconUrl { get; set; } - - public string MetaData { get; set; } - - public string Url { get; set; } - - public IReadOnlyList<string> ProtocolInfo { get; set; } - - public string UpnpClass { get; set; } - - public string MediaType - { - get - { - var classType = UpnpClass ?? string.Empty; - - if (classType.Contains("Audio", StringComparison.Ordinal)) - { - return "Audio"; - } - - if (classType.Contains("Video", StringComparison.Ordinal)) - { - return "Video"; - } - - if (classType.Contains("image", StringComparison.Ordinal)) - { - return "Photo"; - } - - return null; - } - } - - public bool Equals(UBaseObject obj) - { - ArgumentNullException.ThrowIfNull(obj); - - return string.Equals(Id, obj.Id, StringComparison.Ordinal); - } - } -} diff --git a/Emby.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs deleted file mode 100644 index 5042d44938..0000000000 --- a/Emby.Dlna/PlayTo/uPnpNamespaces.cs +++ /dev/null @@ -1,67 +0,0 @@ -#pragma warning disable CS1591 - -using System.Xml.Linq; - -namespace Emby.Dlna.PlayTo -{ - public static class UPnpNamespaces - { - public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/"; - - public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; - - public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0"; - - public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0"; - - public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/"; - - public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1"; - - public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1"; - - public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1"; - - public static XName Containers { get; } = Ns + "container"; - - public static XName Items { get; } = Ns + "item"; - - public static XName Title { get; } = Dc + "title"; - - public static XName Creator { get; } = Dc + "creator"; - - public static XName Artist { get; } = UPnp + "artist"; - - public static XName Id { get; } = "id"; - - public static XName ParentId { get; } = "parentID"; - - public static XName Class { get; } = UPnp + "class"; - - public static XName Artwork { get; } = UPnp + "albumArtURI"; - - public static XName Description { get; } = Dc + "description"; - - public static XName LongDescription { get; } = UPnp + "longDescription"; - - public static XName Album { get; } = UPnp + "album"; - - public static XName Author { get; } = UPnp + "author"; - - public static XName Director { get; } = UPnp + "director"; - - public static XName PlayCount { get; } = UPnp + "playbackCount"; - - public static XName Tracknumber { get; } = UPnp + "originalTrackNumber"; - - public static XName Res { get; } = Ns + "res"; - - public static XName Duration { get; } = "duration"; - - public static XName ProtocolInfo { get; } = "protocolInfo"; - - public static XName ServiceStateTable { get; } = Svc + "serviceStateTable"; - - public static XName StateVariable { get; } = Svc + "stateVariable"; - } -} |
