2014-04-30 15:07:02 +00:00
|
|
|
|
using MediaBrowser.Controller.Dlna;
|
2014-04-23 16:29:21 +00:00
|
|
|
|
using MediaBrowser.Controller.Drawing;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
using MediaBrowser.Controller.Entities;
|
2014-03-22 18:25:03 +00:00
|
|
|
|
using MediaBrowser.Controller.Entities.Audio;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
using MediaBrowser.Controller.Library;
|
|
|
|
|
using MediaBrowser.Controller.Persistence;
|
|
|
|
|
using MediaBrowser.Controller.Session;
|
2014-04-23 03:18:01 +00:00
|
|
|
|
using MediaBrowser.Dlna.Didl;
|
2014-04-27 03:42:05 +00:00
|
|
|
|
using MediaBrowser.Dlna.Ssdp;
|
2014-04-01 22:23:07 +00:00
|
|
|
|
using MediaBrowser.Model.Dlna;
|
2014-04-18 05:03:01 +00:00
|
|
|
|
using MediaBrowser.Model.Dto;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
using MediaBrowser.Model.Entities;
|
|
|
|
|
using MediaBrowser.Model.Logging;
|
|
|
|
|
using MediaBrowser.Model.Session;
|
2014-06-14 18:24:20 +00:00
|
|
|
|
using MediaBrowser.Model.System;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2014-04-16 02:17:48 +00:00
|
|
|
|
using System.Globalization;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace MediaBrowser.Dlna.PlayTo
|
|
|
|
|
{
|
|
|
|
|
public class PlayToController : ISessionController, IDisposable
|
|
|
|
|
{
|
|
|
|
|
private Device _device;
|
|
|
|
|
private readonly SessionInfo _session;
|
|
|
|
|
private readonly ISessionManager _sessionManager;
|
|
|
|
|
private readonly IItemRepository _itemRepository;
|
|
|
|
|
private readonly ILibraryManager _libraryManager;
|
|
|
|
|
private readonly ILogger _logger;
|
2014-03-13 19:08:02 +00:00
|
|
|
|
private readonly IDlnaManager _dlnaManager;
|
2014-03-16 04:23:58 +00:00
|
|
|
|
private readonly IUserManager _userManager;
|
2014-04-23 16:29:21 +00:00
|
|
|
|
private readonly IImageProcessor _imageProcessor;
|
2014-03-17 04:25:11 +00:00
|
|
|
|
|
2014-04-27 03:42:05 +00:00
|
|
|
|
private readonly SsdpHandler _ssdpHandler;
|
2014-04-30 15:07:02 +00:00
|
|
|
|
private readonly string _serverAddress;
|
2014-04-27 03:42:05 +00:00
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
public bool IsSessionActive
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2014-04-26 02:55:07 +00:00
|
|
|
|
return _device != null;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-17 18:37:40 +00:00
|
|
|
|
public bool SupportsMediaControl
|
|
|
|
|
{
|
|
|
|
|
get { return IsSessionActive; }
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-07 02:28:19 +00:00
|
|
|
|
private Timer _updateTimer;
|
|
|
|
|
|
2014-06-14 18:24:20 +00:00
|
|
|
|
public PlayToController(SessionInfo session, ISessionManager sessionManager, IItemRepository itemRepository, ILibraryManager libraryManager, ILogger logger, IDlnaManager dlnaManager, IUserManager userManager, IImageProcessor imageProcessor, SsdpHandler ssdpHandler, string serverAddress)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
|
|
|
|
_session = session;
|
|
|
|
|
_itemRepository = itemRepository;
|
|
|
|
|
_sessionManager = sessionManager;
|
|
|
|
|
_libraryManager = libraryManager;
|
2014-03-13 21:00:17 +00:00
|
|
|
|
_dlnaManager = dlnaManager;
|
2014-03-16 04:23:58 +00:00
|
|
|
|
_userManager = userManager;
|
2014-04-23 16:29:21 +00:00
|
|
|
|
_imageProcessor = imageProcessor;
|
2014-04-27 03:42:05 +00:00
|
|
|
|
_ssdpHandler = ssdpHandler;
|
2014-04-30 15:07:02 +00:00
|
|
|
|
_serverAddress = serverAddress;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
_logger = logger;
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-13 19:08:02 +00:00
|
|
|
|
public void Init(Device device)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
|
|
|
|
_device = device;
|
2014-04-22 17:25:54 +00:00
|
|
|
|
_device.PlaybackStart += _device_PlaybackStart;
|
|
|
|
|
_device.PlaybackProgress += _device_PlaybackProgress;
|
|
|
|
|
_device.PlaybackStopped += _device_PlaybackStopped;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
_device.Start();
|
|
|
|
|
|
2014-04-27 03:42:05 +00:00
|
|
|
|
_ssdpHandler.MessageReceived += _SsdpHandler_MessageReceived;
|
2014-05-07 02:28:19 +00:00
|
|
|
|
|
|
|
|
|
_updateTimer = new Timer(updateTimer_Elapsed, null, 60000, 60000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async void updateTimer_Elapsed(object state)
|
|
|
|
|
{
|
|
|
|
|
if (DateTime.UtcNow >= _device.DateLastActivity.AddSeconds(60))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Session is inactive, mark it for Disposal and don't start the elapsed timer.
|
2014-05-28 15:51:42 +00:00
|
|
|
|
_sessionManager.ReportSessionEnded(_session.Id);
|
2014-05-07 02:28:19 +00:00
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.ErrorException("Error in ReportSessionEnded", ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-04-27 03:42:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-30 15:07:02 +00:00
|
|
|
|
private string GetServerAddress()
|
|
|
|
|
{
|
|
|
|
|
return _serverAddress;
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-27 03:42:05 +00:00
|
|
|
|
async void _SsdpHandler_MessageReceived(object sender, SsdpMessageEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
string nts;
|
|
|
|
|
e.Headers.TryGetValue("NTS", out nts);
|
|
|
|
|
|
|
|
|
|
string usn;
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (!e.Headers.TryGetValue("USN", out usn)) usn = String.Empty;
|
2014-04-27 03:42:05 +00:00
|
|
|
|
|
|
|
|
|
string nt;
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (!e.Headers.TryGetValue("NT", out nt)) nt = String.Empty;
|
2014-05-28 15:51:42 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (String.Equals(e.Method, "NOTIFY", StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
String.Equals(nts, "ssdp:byebye", StringComparison.OrdinalIgnoreCase) &&
|
2014-04-30 15:07:02 +00:00
|
|
|
|
usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
|
|
|
|
|
!_disposed)
|
2014-04-27 03:42:05 +00:00
|
|
|
|
{
|
2014-04-30 15:07:02 +00:00
|
|
|
|
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
|
|
|
|
|
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
|
2014-04-27 03:42:05 +00:00
|
|
|
|
{
|
2014-04-30 15:07:02 +00:00
|
|
|
|
try
|
|
|
|
|
{
|
2014-05-28 15:51:42 +00:00
|
|
|
|
_sessionManager.ReportSessionEnded(_session.Id);
|
2014-04-30 15:07:02 +00:00
|
|
|
|
}
|
|
|
|
|
catch
|
2014-04-27 03:42:05 +00:00
|
|
|
|
{
|
2014-04-30 15:07:02 +00:00
|
|
|
|
// Could throw if the session is already gone
|
2014-04-27 03:42:05 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
async void _device_PlaybackStopped(object sender, PlaybackStoppedEventArgs e)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-04-22 17:25:54 +00:00
|
|
|
|
try
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-04-16 02:17:48 +00:00
|
|
|
|
await _sessionManager.OnPlaybackStopped(new PlaybackStopInfo
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-04-22 17:25:54 +00:00
|
|
|
|
ItemId = e.MediaInfo.Id,
|
2014-03-10 10:10:16 +00:00
|
|
|
|
SessionId = _session.Id,
|
|
|
|
|
PositionTicks = _device.Position.Ticks
|
|
|
|
|
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
2014-04-22 17:25:54 +00:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.ErrorException("Error reporting progress", ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await SetNext().ConfigureAwait(false);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
async void _device_PlaybackStart(object sender, PlaybackStartEventArgs e)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-06-20 04:06:24 +00:00
|
|
|
|
var info = GetProgressInfo(e.MediaInfo);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
try
|
2014-04-22 17:25:54 +00:00
|
|
|
|
{
|
2014-06-20 04:06:24 +00:00
|
|
|
|
await _sessionManager.OnPlaybackStart(info).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.ErrorException("Error reporting progress", ex);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
async void _device_PlaybackProgress(object sender, PlaybackProgressEventArgs e)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-06-20 04:06:24 +00:00
|
|
|
|
var info = GetProgressInfo(e.MediaInfo);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
try
|
2014-03-17 04:25:11 +00:00
|
|
|
|
{
|
2014-06-20 04:06:24 +00:00
|
|
|
|
await _sessionManager.OnPlaybackProgress(info).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.ErrorException("Error reporting progress", ex);
|
2014-03-17 04:25:11 +00:00
|
|
|
|
}
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
private PlaybackStartInfo GetProgressInfo(uBaseObject mediaInfo)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-04-22 17:25:54 +00:00
|
|
|
|
var ticks = _device.Position.Ticks;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
var info = StreamParams.ParseFromUrl(mediaInfo.Url, _libraryManager);
|
|
|
|
|
|
|
|
|
|
if (!info.IsDirectStream)
|
2014-04-22 17:25:54 +00:00
|
|
|
|
{
|
2014-06-20 04:06:24 +00:00
|
|
|
|
ticks += info.StartPositionTicks;
|
2014-04-22 17:25:54 +00:00
|
|
|
|
}
|
2014-04-16 02:17:48 +00:00
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
return new PlaybackStartInfo
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-04-22 17:25:54 +00:00
|
|
|
|
ItemId = mediaInfo.Id,
|
|
|
|
|
SessionId = _session.Id,
|
|
|
|
|
PositionTicks = ticks,
|
|
|
|
|
IsMuted = _device.IsMuted,
|
|
|
|
|
IsPaused = _device.IsPaused,
|
2014-06-20 04:06:24 +00:00
|
|
|
|
MediaSourceId = info.MediaSourceId,
|
|
|
|
|
AudioStreamIndex = info.AudioStreamIndex,
|
|
|
|
|
SubtitleStreamIndex = info.SubtitleStreamIndex,
|
2014-04-22 17:25:54 +00:00
|
|
|
|
VolumeLevel = _device.Volume,
|
2014-06-20 04:06:24 +00:00
|
|
|
|
|
|
|
|
|
CanSeek = info.MediaSource == null ? _device.Duration.HasValue : info.MediaSource.RunTimeTicks.HasValue,
|
|
|
|
|
|
|
|
|
|
PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode,
|
2014-04-22 17:25:54 +00:00
|
|
|
|
QueueableMediaTypes = new List<string> { mediaInfo.MediaType }
|
|
|
|
|
};
|
|
|
|
|
}
|
2014-04-18 05:03:01 +00:00
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
#region SendCommands
|
|
|
|
|
|
2014-03-16 04:23:58 +00:00
|
|
|
|
public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
|
|
|
|
_logger.Debug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
|
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
var user = String.IsNullOrEmpty(command.ControllingUserId) ? null : _userManager.GetUserById(new Guid(command.ControllingUserId));
|
2014-06-14 18:24:20 +00:00
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
var items = new List<BaseItem>();
|
|
|
|
|
foreach (string id in command.ItemIds)
|
|
|
|
|
{
|
|
|
|
|
AddItemFromId(Guid.Parse(id), items);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var playlist = new List<PlaylistItem>();
|
|
|
|
|
var isFirst = true;
|
|
|
|
|
|
|
|
|
|
var serverAddress = GetServerAddress();
|
|
|
|
|
|
|
|
|
|
foreach (var item in items)
|
|
|
|
|
{
|
|
|
|
|
if (isFirst && command.StartPositionTicks.HasValue)
|
|
|
|
|
{
|
2014-06-14 18:24:20 +00:00
|
|
|
|
playlist.Add(CreatePlaylistItem(item, user, command.StartPositionTicks.Value, serverAddress));
|
2014-03-10 10:10:16 +00:00
|
|
|
|
isFirst = false;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2014-06-14 18:24:20 +00:00
|
|
|
|
playlist.Add(CreatePlaylistItem(item, user, 0, serverAddress));
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_logger.Debug("{0} - Playlist created", _session.DeviceName);
|
|
|
|
|
|
|
|
|
|
if (command.PlayCommand == PlayCommand.PlayLast)
|
|
|
|
|
{
|
2014-04-22 17:25:54 +00:00
|
|
|
|
Playlist.AddRange(playlist);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
if (command.PlayCommand == PlayCommand.PlayNext)
|
|
|
|
|
{
|
2014-04-22 17:25:54 +00:00
|
|
|
|
Playlist.AddRange(playlist);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_logger.Debug("{0} - Playing {1} items", _session.DeviceName, playlist.Count);
|
2014-03-16 04:23:58 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (!String.IsNullOrWhiteSpace(command.ControllingUserId))
|
2014-03-16 04:23:58 +00:00
|
|
|
|
{
|
|
|
|
|
await _sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
|
|
|
|
|
_session.DeviceName, _session.RemoteEndPoint, user).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await PlayItems(playlist).ConfigureAwait(false);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
switch (command.Command)
|
|
|
|
|
{
|
|
|
|
|
case PlaystateCommand.Stop:
|
|
|
|
|
Playlist.Clear();
|
|
|
|
|
return _device.SetStop();
|
|
|
|
|
|
|
|
|
|
case PlaystateCommand.Pause:
|
|
|
|
|
return _device.SetPause();
|
|
|
|
|
|
|
|
|
|
case PlaystateCommand.Unpause:
|
|
|
|
|
return _device.SetPlay();
|
|
|
|
|
|
|
|
|
|
case PlaystateCommand.Seek:
|
2014-06-20 04:06:24 +00:00
|
|
|
|
{
|
|
|
|
|
var media = _device.CurrentMediaInfo;
|
2014-04-06 17:53:23 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (media != null)
|
|
|
|
|
{
|
|
|
|
|
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (info.Item != null && !info.IsDirectStream)
|
|
|
|
|
{
|
|
|
|
|
var user = _session.UserId.HasValue ? _userManager.GetUserById(_session.UserId.Value) : null;
|
|
|
|
|
var newItem = CreatePlaylistItem(info.Item, user, command.SeekPositionTicks ?? 0, GetServerAddress(), info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
|
|
|
|
|
|
|
|
|
return _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return _device.Seek(TimeSpan.FromTicks(command.SeekPositionTicks ?? 0));
|
|
|
|
|
}
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
|
|
|
|
case PlaystateCommand.NextTrack:
|
|
|
|
|
return SetNext();
|
|
|
|
|
|
|
|
|
|
case PlaystateCommand.PreviousTrack:
|
|
|
|
|
return SetPrevious();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-18 19:58:42 +00:00
|
|
|
|
public Task SendRestartRequiredNotification(SystemInfo info, CancellationToken cancellationToken)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task SendServerRestartNotification(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-06 17:53:23 +00:00
|
|
|
|
public Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-12 17:27:53 +00:00
|
|
|
|
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task SendLibraryUpdateInfo(LibraryUpdateInfo info, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Playlist
|
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
2014-03-10 10:10:16 +00:00
|
|
|
|
private List<PlaylistItem> Playlist
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return _playlist;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void AddItemFromId(Guid id, List<BaseItem> list)
|
|
|
|
|
{
|
|
|
|
|
var item = _libraryManager.GetItemById(id);
|
|
|
|
|
if (item.IsFolder)
|
|
|
|
|
{
|
|
|
|
|
foreach (var childId in _itemRepository.GetChildren(item.Id))
|
|
|
|
|
{
|
|
|
|
|
AddItemFromId(childId, list);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
|
|
|
|
|
{
|
|
|
|
|
list.Add(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-06-14 18:24:20 +00:00
|
|
|
|
private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string serverAddress)
|
2014-06-20 04:06:24 +00:00
|
|
|
|
{
|
|
|
|
|
return CreatePlaylistItem(item, user, startPostionTicks, serverAddress, null, null, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string serverAddress, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
2014-03-13 19:08:02 +00:00
|
|
|
|
var deviceInfo = _device.Properties;
|
|
|
|
|
|
2014-03-26 15:17:36 +00:00
|
|
|
|
var profile = _dlnaManager.GetProfile(deviceInfo.ToDeviceIdentification()) ??
|
|
|
|
|
_dlnaManager.GetDefaultProfile();
|
2014-03-23 16:42:02 +00:00
|
|
|
|
|
2014-06-14 18:24:20 +00:00
|
|
|
|
var hasMediaSources = item as IHasMediaSources;
|
|
|
|
|
var mediaSources = hasMediaSources != null
|
|
|
|
|
? (user == null ? hasMediaSources.GetMediaSources(true) : hasMediaSources.GetMediaSources(true, user)).ToList()
|
2014-04-18 05:03:01 +00:00
|
|
|
|
: new List<MediaSourceInfo>();
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
2014-04-18 05:03:01 +00:00
|
|
|
|
playlistItem.StreamInfo.StartPositionTicks = startPostionTicks;
|
|
|
|
|
|
|
|
|
|
playlistItem.StreamUrl = playlistItem.StreamInfo.ToUrl(serverAddress);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
2014-06-14 18:24:20 +00:00
|
|
|
|
var itemXml = new DidlBuilder(profile, user, _imageProcessor, serverAddress).GetItemDidl(item, _session.DeviceId, new Filter());
|
2014-04-18 05:03:01 +00:00
|
|
|
|
|
2014-04-23 16:29:21 +00:00
|
|
|
|
playlistItem.Didl = itemXml;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
|
|
|
|
|
return playlistItem;
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-23 05:10:33 +00:00
|
|
|
|
private string GetDlnaHeaders(PlaylistItem item)
|
|
|
|
|
{
|
2014-04-23 02:47:46 +00:00
|
|
|
|
var profile = item.Profile;
|
2014-04-18 05:03:01 +00:00
|
|
|
|
var streamInfo = item.StreamInfo;
|
|
|
|
|
|
2014-04-23 02:47:46 +00:00
|
|
|
|
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
2014-03-23 05:10:33 +00:00
|
|
|
|
{
|
2014-04-23 02:47:46 +00:00
|
|
|
|
return new ContentFeatureBuilder(profile)
|
|
|
|
|
.BuildAudioHeader(streamInfo.Container,
|
|
|
|
|
streamInfo.AudioCodec,
|
|
|
|
|
streamInfo.TargetAudioBitrate,
|
|
|
|
|
streamInfo.TargetAudioSampleRate,
|
|
|
|
|
streamInfo.TargetAudioChannels,
|
|
|
|
|
streamInfo.IsDirectStream,
|
|
|
|
|
streamInfo.RunTimeTicks,
|
|
|
|
|
streamInfo.TranscodeSeekInfo);
|
2014-03-23 05:10:33 +00:00
|
|
|
|
}
|
2014-04-23 02:47:46 +00:00
|
|
|
|
|
|
|
|
|
if (streamInfo.MediaType == DlnaProfileType.Video)
|
2014-03-23 05:10:33 +00:00
|
|
|
|
{
|
2014-04-23 02:47:46 +00:00
|
|
|
|
return new ContentFeatureBuilder(profile)
|
|
|
|
|
.BuildVideoHeader(streamInfo.Container,
|
|
|
|
|
streamInfo.VideoCodec,
|
|
|
|
|
streamInfo.AudioCodec,
|
|
|
|
|
streamInfo.TargetWidth,
|
|
|
|
|
streamInfo.TargetHeight,
|
2014-04-24 05:08:10 +00:00
|
|
|
|
streamInfo.TargetVideoBitDepth,
|
|
|
|
|
streamInfo.TargetVideoBitrate,
|
|
|
|
|
streamInfo.TargetAudioChannels,
|
|
|
|
|
streamInfo.TargetAudioBitrate,
|
2014-04-23 02:47:46 +00:00
|
|
|
|
streamInfo.TargetTimestamp,
|
|
|
|
|
streamInfo.IsDirectStream,
|
|
|
|
|
streamInfo.RunTimeTicks,
|
2014-04-24 05:08:10 +00:00
|
|
|
|
streamInfo.TargetVideoProfile,
|
|
|
|
|
streamInfo.TargetVideoLevel,
|
|
|
|
|
streamInfo.TargetFramerate,
|
|
|
|
|
streamInfo.TargetPacketLength,
|
2014-04-23 02:47:46 +00:00
|
|
|
|
streamInfo.TranscodeSeekInfo);
|
2014-03-23 05:10:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-23 02:47:46 +00:00
|
|
|
|
return null;
|
2014-03-23 05:10:33 +00:00
|
|
|
|
}
|
2014-04-06 18:34:47 +00:00
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
private PlaylistItem GetPlaylistItem(BaseItem item, List<MediaSourceInfo> mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
|
2014-03-22 18:25:03 +00:00
|
|
|
|
{
|
|
|
|
|
var video = item as Video;
|
|
|
|
|
|
|
|
|
|
if (video != null)
|
|
|
|
|
{
|
2014-04-18 05:03:01 +00:00
|
|
|
|
return new PlaylistItem
|
|
|
|
|
{
|
|
|
|
|
StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
|
|
|
|
|
{
|
|
|
|
|
ItemId = item.Id.ToString("N"),
|
|
|
|
|
MediaSources = mediaSources,
|
|
|
|
|
Profile = profile,
|
2014-06-14 23:13:09 +00:00
|
|
|
|
DeviceId = deviceId,
|
2014-06-20 04:06:24 +00:00
|
|
|
|
MaxBitrate = profile.MaxBitrate,
|
|
|
|
|
MediaSourceId = mediaSourceId,
|
|
|
|
|
AudioStreamIndex = audioStreamIndex,
|
|
|
|
|
SubtitleStreamIndex = subtitleStreamIndex
|
2014-04-23 02:47:46 +00:00
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
Profile = profile
|
2014-04-18 05:03:01 +00:00
|
|
|
|
};
|
2014-03-22 18:25:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var audio = item as Audio;
|
|
|
|
|
|
|
|
|
|
if (audio != null)
|
|
|
|
|
{
|
2014-04-18 05:03:01 +00:00
|
|
|
|
return new PlaylistItem
|
|
|
|
|
{
|
|
|
|
|
StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
|
|
|
|
|
{
|
|
|
|
|
ItemId = item.Id.ToString("N"),
|
|
|
|
|
MediaSources = mediaSources,
|
|
|
|
|
Profile = profile,
|
2014-06-14 23:13:09 +00:00
|
|
|
|
DeviceId = deviceId,
|
2014-06-20 04:06:24 +00:00
|
|
|
|
MaxBitrate = profile.MaxBitrate,
|
|
|
|
|
MediaSourceId = mediaSourceId
|
2014-04-23 02:47:46 +00:00
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
Profile = profile
|
2014-04-18 05:03:01 +00:00
|
|
|
|
};
|
2014-03-22 18:25:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var photo = item as Photo;
|
|
|
|
|
|
|
|
|
|
if (photo != null)
|
|
|
|
|
{
|
2014-03-23 05:10:33 +00:00
|
|
|
|
return new PlaylistItemFactory().Create(photo, profile);
|
2014-03-22 18:25:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new ArgumentException("Unrecognized item type.");
|
|
|
|
|
}
|
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Plays the items.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="items">The items.</param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items)
|
|
|
|
|
{
|
|
|
|
|
Playlist.Clear();
|
|
|
|
|
Playlist.AddRange(items);
|
|
|
|
|
await SetNext();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-26 02:55:07 +00:00
|
|
|
|
private async Task SetNext()
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
|
|
|
|
if (!Playlist.Any() || Playlist.All(i => i.PlayState != 0))
|
|
|
|
|
{
|
2014-04-26 02:55:07 +00:00
|
|
|
|
return;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
2014-04-26 02:55:07 +00:00
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1);
|
|
|
|
|
|
|
|
|
|
if (currentitem != null)
|
|
|
|
|
{
|
|
|
|
|
currentitem.PlayState = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nextTrack = Playlist.FirstOrDefault(i => i.PlayState == 0);
|
|
|
|
|
if (nextTrack == null)
|
|
|
|
|
{
|
|
|
|
|
await _device.SetStop();
|
2014-04-26 02:55:07 +00:00
|
|
|
|
return;
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
2014-03-23 05:10:33 +00:00
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
nextTrack.PlayState = 1;
|
2014-03-23 05:10:33 +00:00
|
|
|
|
|
|
|
|
|
var dlnaheaders = GetDlnaHeaders(nextTrack);
|
|
|
|
|
|
|
|
|
|
_logger.Debug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", _device.Properties.Name, nextTrack.StreamUrl, dlnaheaders);
|
|
|
|
|
|
|
|
|
|
await _device.SetAvTransport(nextTrack.StreamUrl, dlnaheaders, nextTrack.Didl);
|
2014-04-06 18:34:47 +00:00
|
|
|
|
|
2014-04-18 05:03:01 +00:00
|
|
|
|
var streamInfo = nextTrack.StreamInfo;
|
|
|
|
|
if (streamInfo.StartPositionTicks > 0 && streamInfo.IsDirectStream)
|
|
|
|
|
await _device.Seek(TimeSpan.FromTicks(streamInfo.StartPositionTicks));
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
public Task SetPrevious()
|
2014-03-10 10:10:16 +00:00
|
|
|
|
{
|
|
|
|
|
if (!Playlist.Any() || Playlist.All(i => i.PlayState != 2))
|
|
|
|
|
return Task.FromResult(false);
|
|
|
|
|
|
|
|
|
|
var currentitem = Playlist.FirstOrDefault(i => i.PlayState == 1);
|
|
|
|
|
|
|
|
|
|
var prevTrack = Playlist.LastOrDefault(i => i.PlayState == 2);
|
|
|
|
|
|
|
|
|
|
if (currentitem != null)
|
|
|
|
|
{
|
|
|
|
|
currentitem.PlayState = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (prevTrack == null)
|
|
|
|
|
return Task.FromResult(false);
|
|
|
|
|
|
|
|
|
|
prevTrack.PlayState = 1;
|
2014-04-18 05:03:01 +00:00
|
|
|
|
return _device.SetAvTransport(prevTrack.StreamInfo.ToDlnaUrl(GetServerAddress()), GetDlnaHeaders(prevTrack), prevTrack.Didl);
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (!_disposed)
|
|
|
|
|
{
|
|
|
|
|
_disposed = true;
|
2014-04-26 02:55:07 +00:00
|
|
|
|
|
2014-04-22 17:25:54 +00:00
|
|
|
|
_device.PlaybackStart -= _device_PlaybackStart;
|
|
|
|
|
_device.PlaybackProgress -= _device_PlaybackProgress;
|
|
|
|
|
_device.PlaybackStopped -= _device_PlaybackStopped;
|
2014-04-27 03:42:05 +00:00
|
|
|
|
_ssdpHandler.MessageReceived -= _SsdpHandler_MessageReceived;
|
2014-04-26 02:55:07 +00:00
|
|
|
|
|
2014-05-07 02:28:19 +00:00
|
|
|
|
DisposeUpdateTimer();
|
|
|
|
|
|
2014-03-10 10:10:16 +00:00
|
|
|
|
_device.Dispose();
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-03-28 19:58:18 +00:00
|
|
|
|
|
2014-05-07 02:28:19 +00:00
|
|
|
|
private void DisposeUpdateTimer()
|
|
|
|
|
{
|
|
|
|
|
if (_updateTimer != null)
|
|
|
|
|
{
|
|
|
|
|
_updateTimer.Dispose();
|
|
|
|
|
_updateTimer = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-04-15 03:54:52 +00:00
|
|
|
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
2014-04-16 02:17:48 +00:00
|
|
|
|
|
2014-03-31 21:04:22 +00:00
|
|
|
|
public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
2014-03-28 19:58:18 +00:00
|
|
|
|
{
|
2014-03-31 21:04:22 +00:00
|
|
|
|
GeneralCommandType commandType;
|
|
|
|
|
|
2014-04-14 06:42:51 +00:00
|
|
|
|
if (Enum.TryParse(command.Name, true, out commandType))
|
2014-03-31 21:04:22 +00:00
|
|
|
|
{
|
|
|
|
|
switch (commandType)
|
|
|
|
|
{
|
|
|
|
|
case GeneralCommandType.VolumeDown:
|
|
|
|
|
return _device.VolumeDown();
|
|
|
|
|
case GeneralCommandType.VolumeUp:
|
|
|
|
|
return _device.VolumeUp();
|
|
|
|
|
case GeneralCommandType.Mute:
|
2014-04-22 17:25:54 +00:00
|
|
|
|
return _device.Mute();
|
2014-03-31 21:04:22 +00:00
|
|
|
|
case GeneralCommandType.Unmute:
|
2014-04-22 17:25:54 +00:00
|
|
|
|
return _device.Unmute();
|
2014-03-31 21:04:22 +00:00
|
|
|
|
case GeneralCommandType.ToggleMute:
|
|
|
|
|
return _device.ToggleMute();
|
2014-04-15 03:54:52 +00:00
|
|
|
|
case GeneralCommandType.SetVolume:
|
|
|
|
|
{
|
2014-04-16 02:17:48 +00:00
|
|
|
|
string volumeArg;
|
2014-04-15 03:54:52 +00:00
|
|
|
|
|
2014-04-16 02:17:48 +00:00
|
|
|
|
if (command.Arguments.TryGetValue("Volume", out volumeArg))
|
2014-04-15 03:54:52 +00:00
|
|
|
|
{
|
2014-04-16 02:17:48 +00:00
|
|
|
|
int volume;
|
|
|
|
|
|
2014-06-20 04:06:24 +00:00
|
|
|
|
if (Int32.TryParse(volumeArg, NumberStyles.Any, _usCulture, out volume))
|
2014-04-16 02:17:48 +00:00
|
|
|
|
{
|
|
|
|
|
return _device.SetVolume(volume);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new ArgumentException("Unsupported volume value supplied.");
|
2014-04-15 03:54:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
2014-04-16 02:17:48 +00:00
|
|
|
|
throw new ArgumentException("Volume argument cannot be null");
|
2014-04-15 03:54:52 +00:00
|
|
|
|
}
|
2014-03-31 21:04:22 +00:00
|
|
|
|
default:
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Task.FromResult(true);
|
2014-03-28 19:58:18 +00:00
|
|
|
|
}
|
2014-06-20 04:06:24 +00:00
|
|
|
|
|
|
|
|
|
private class StreamParams
|
|
|
|
|
{
|
|
|
|
|
public string 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 BaseItem Item { get; set; }
|
|
|
|
|
public MediaSourceInfo MediaSource { get; set; }
|
|
|
|
|
|
|
|
|
|
private static string GetItemId(string url)
|
|
|
|
|
{
|
|
|
|
|
var parts = url.Split('/');
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < parts.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var part = parts[i];
|
|
|
|
|
|
|
|
|
|
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
if (parts.Length > i + 1)
|
|
|
|
|
{
|
|
|
|
|
return parts[i + 1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static StreamParams ParseFromUrl(string url, ILibraryManager libraryManager)
|
|
|
|
|
{
|
|
|
|
|
var request = new StreamParams
|
|
|
|
|
{
|
|
|
|
|
ItemId = GetItemId(url)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(request.ItemId))
|
|
|
|
|
{
|
|
|
|
|
return request;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const string srch = "params=";
|
|
|
|
|
var index = url.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
if (index == -1) return request;
|
|
|
|
|
|
|
|
|
|
var vals = url.Substring(index + srch.Length).Split(';');
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < vals.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var val = vals[i];
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(val))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i == 0)
|
|
|
|
|
{
|
|
|
|
|
request.DeviceProfileId = val;
|
|
|
|
|
}
|
|
|
|
|
else if (i == 1)
|
|
|
|
|
{
|
|
|
|
|
request.DeviceId = val;
|
|
|
|
|
}
|
|
|
|
|
else if (i == 2)
|
|
|
|
|
{
|
|
|
|
|
request.MediaSourceId = val;
|
|
|
|
|
}
|
|
|
|
|
else if (i == 3)
|
|
|
|
|
{
|
|
|
|
|
request.IsDirectStream = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
else if (i == 6)
|
|
|
|
|
{
|
|
|
|
|
request.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
|
|
}
|
|
|
|
|
else if (i == 7)
|
|
|
|
|
{
|
|
|
|
|
request.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
|
|
|
|
}
|
|
|
|
|
else if (i == 14)
|
|
|
|
|
{
|
|
|
|
|
request.StartPositionTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request.Item = string.IsNullOrWhiteSpace(request.ItemId)
|
|
|
|
|
? null
|
|
|
|
|
: libraryManager.GetItemById(new Guid(request.ItemId));
|
|
|
|
|
|
|
|
|
|
var hasMediaSources = request.Item as IHasMediaSources;
|
|
|
|
|
|
|
|
|
|
request.MediaSource = hasMediaSources == null ?
|
|
|
|
|
null :
|
|
|
|
|
hasMediaSources.GetMediaSources(false).FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return request;
|
|
|
|
|
}
|
|
|
|
|
}
|
2014-03-10 10:10:16 +00:00
|
|
|
|
}
|
|
|
|
|
}
|