using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Serialization; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Library { public class MediaSourceManager : IMediaSourceManager, IDisposable { private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; private readonly IJsonSerializer _jsonSerializer; private IMediaSourceProvider[] _providers; private readonly ILogger _logger; public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer) { _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; _logger = logger; _jsonSerializer = jsonSerializer; } public void AddParts(IEnumerable providers) { _providers = providers.ToArray(); } public IEnumerable GetMediaStreams(MediaStreamQuery query) { var list = _itemRepo.GetMediaStreams(query) .ToList(); foreach (var stream in list) { stream.SupportsExternalStream = StreamSupportsExternalStream(stream); } return list; } private bool StreamSupportsExternalStream(MediaStream stream) { if (stream.IsExternal) { return true; } if (stream.IsTextSubtitleStream) { return InternalTextStreamSupportsExternalStream(stream); } return false; } private bool InternalTextStreamSupportsExternalStream(MediaStream stream) { // These usually have styles and fonts that won't convert to text very well if (string.Equals(stream.Codec, "ass", StringComparison.OrdinalIgnoreCase)) { return false; } if (string.Equals(stream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)) { return false; } return true; } public IEnumerable GetMediaStreams(string mediaSourceId) { var list = GetMediaStreams(new MediaStreamQuery { ItemId = new Guid(mediaSourceId) }); return GetMediaStreamsForItem(list); } public IEnumerable GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery { ItemId = itemId }); return GetMediaStreamsForItem(list); } private IEnumerable GetMediaStreamsForItem(IEnumerable streams) { var list = streams.ToList(); var subtitleStreams = list .Where(i => i.Type == MediaStreamType.Subtitle) .ToList(); if (subtitleStreams.Count > 0) { var videoStream = list.FirstOrDefault(i => i.Type == MediaStreamType.Video); // This is abitrary but at some point it becomes too slow to extract subtitles on the fly // We need to learn more about when this is the case vs. when it isn't const int maxAllowedBitrateForExternalSubtitleStream = 10000000; var videoBitrate = videoStream == null ? maxAllowedBitrateForExternalSubtitleStream : videoStream.BitRate ?? maxAllowedBitrateForExternalSubtitleStream; foreach (var subStream in subtitleStreams) { var supportsExternalStream = StreamSupportsExternalStream(subStream); if (supportsExternalStream && videoBitrate >= maxAllowedBitrateForExternalSubtitleStream) { supportsExternalStream = false; } subStream.SupportsExternalStream = supportsExternalStream; } } return list; } public async Task> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken) { var item = _libraryManager.GetItemById(id); var hasMediaSources = (IHasMediaSources)item; User user = null; if (!string.IsNullOrWhiteSpace(userId)) { user = _userManager.GetUserById(userId); } var mediaSources = GetStaticMediaSources(hasMediaSources, enablePathSubstitution, user); var dynamicMediaSources = await GetDynamicMediaSources(hasMediaSources, cancellationToken).ConfigureAwait(false); var list = new List(); list.AddRange(mediaSources); foreach (var source in dynamicMediaSources) { if (user != null) { SetUserProperties(source, user); } if (source.Protocol == MediaProtocol.File) { // TODO: Path substitution if (!File.Exists(source.Path)) { source.SupportsDirectStream = false; } } else if (source.Protocol == MediaProtocol.Http) { // TODO: Allow this when the source is plain http, e.g. not HLS or Mpeg Dash source.SupportsDirectStream = false; } else { source.SupportsDirectStream = false; } list.Add(source); } foreach (var source in list) { if (user != null) { if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { if (!user.Policy.EnableAudioPlaybackTranscoding) { source.SupportsTranscoding = false; } } else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { if (!user.Policy.EnableVideoPlaybackTranscoding) { source.SupportsTranscoding = false; } } } } return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder); } private async Task> GetDynamicMediaSources(IHasMediaSources item, CancellationToken cancellationToken) { var tasks = _providers.Select(i => GetDynamicMediaSources(item, i, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); return results.SelectMany(i => i.ToList()); } private async Task> GetDynamicMediaSources(IHasMediaSources item, IMediaSourceProvider provider, CancellationToken cancellationToken) { try { var sources = await provider.GetMediaSources(item, cancellationToken).ConfigureAwait(false); var list = sources.ToList(); foreach (var mediaSource in list) { SetKeyProperties(provider, mediaSource); } return list; } catch (Exception ex) { _logger.ErrorException("Error getting media sources", ex); return new List(); } } private void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource) { var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter; if (!string.IsNullOrWhiteSpace(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { mediaSource.OpenToken = prefix + mediaSource.OpenToken; } if (!string.IsNullOrWhiteSpace(mediaSource.LiveStreamId) && !mediaSource.LiveStreamId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { mediaSource.LiveStreamId = prefix + mediaSource.LiveStreamId; } } public async Task GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution) { var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video }, CancellationToken.None).ConfigureAwait(false); return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase)); } public IEnumerable GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null) { if (item == null) { throw new ArgumentNullException("item"); } if (!(item is Video)) { return item.GetMediaSources(enablePathSubstitution); } var sources = item.GetMediaSources(enablePathSubstitution).ToList(); if (user != null) { foreach (var source in sources) { SetUserProperties(source, user); } } return sources; } private void SetUserProperties(MediaSourceInfo source, User user) { var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference) ? new string[] { } : new[] { user.Configuration.AudioLanguagePreference }; var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference) ? new List { } : new List { user.Configuration.SubtitleLanguagePreference }; source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack); var defaultAudioIndex = source.DefaultAudioStreamIndex; var audioLangage = defaultAudioIndex == null ? null : source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault(); source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams, preferredSubs, user.Configuration.SubtitleMode, audioLangage); MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.Configuration.SubtitleMode, audioLangage); } private IEnumerable SortMediaSources(IEnumerable sources) { return sources.OrderBy(i => { if (i.VideoType.HasValue && i.VideoType.Value == VideoType.VideoFile) { return 0; } return 1; }).ThenBy(i => i.Video3DFormat.HasValue ? 1 : 0) .ThenByDescending(i => { var stream = i.VideoStream; return stream == null || stream.Width == null ? 0 : stream.Width.Value; }) .ToList(); } private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); public async Task OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { var tuple = GetProvider(request.OpenToken); var provider = tuple.Item1; var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId)) { throw new InvalidOperationException(string.Format("{0} returned null LiveStreamId", provider.GetType().Name)); } SetKeyProperties(provider, mediaSource); var info = new LiveStreamInfo { Date = DateTime.UtcNow, EnableCloseTimer = enableAutoClose, Id = mediaSource.LiveStreamId, MediaSource = mediaSource }; _openStreams.AddOrUpdate(mediaSource.LiveStreamId, info, (key, i) => info); if (enableAutoClose) { StartCloseTimer(); } var json = _jsonSerializer.SerializeToString(mediaSource); _logger.Debug("Live stream opened: " + json); var clone = _jsonSerializer.DeserializeFromString(json); if (!string.IsNullOrWhiteSpace(request.UserId)) { var user = _userManager.GetUserById(request.UserId); SetUserProperties(clone, user); } return new LiveStreamResponse { MediaSource = clone }; } finally { _liveStreamSemaphore.Release(); } } public async Task GetLiveStream(string id, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException("id"); } _logger.Debug("Getting live stream {0}", id); await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { LiveStreamInfo info; if (_openStreams.TryGetValue(id, out info)) { return info.MediaSource; } else { throw new ResourceNotFoundException(); } } finally { _liveStreamSemaphore.Release(); } } public async Task PingLiveStream(string id, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { LiveStreamInfo info; if (_openStreams.TryGetValue(id, out info)) { info.Date = DateTime.UtcNow; } else { _logger.Error("Failed to update MediaSource timestamp for {0}", id); } } finally { _liveStreamSemaphore.Release(); } } public async Task CloseLiveStream(string id, CancellationToken cancellationToken) { await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { LiveStreamInfo current; if (_openStreams.TryGetValue(id, out current)) { if (current.MediaSource.RequiresClosing) { var tuple = GetProvider(id); await tuple.Item1.CloseMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false); } } LiveStreamInfo removed; if (_openStreams.TryRemove(id, out removed)) { removed.Closed = true; } if (_openStreams.Count == 0) { StopCloseTimer(); } } finally { _liveStreamSemaphore.Release(); } } // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. private const char LiveStreamIdDelimeter = '_'; private Tuple GetProvider(string key) { var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), keys[0], StringComparison.OrdinalIgnoreCase)); return new Tuple(provider, keys[1]); } private Timer _closeTimer; private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(60); private void StartCloseTimer() { StopCloseTimer(); _closeTimer = new Timer(CloseTimerCallback, null, _openStreamMaxAge, _openStreamMaxAge); } private void StopCloseTimer() { var timer = _closeTimer; if (timer != null) { _closeTimer = null; timer.Dispose(); } } private async void CloseTimerCallback(object state) { var infos = _openStreams .Values .Where(i => i.EnableCloseTimer && (DateTime.UtcNow - i.Date) > _openStreamMaxAge) .ToList(); foreach (var info in infos) { if (!info.Closed) { try { await CloseLiveStream(info.Id, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { _logger.ErrorException("Error closing media source", ex); } } } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { StopCloseTimer(); Dispose(true); } private readonly object _disposeLock = new object(); /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { lock (_disposeLock) { foreach (var key in _openStreams.Keys.ToList()) { var task = CloseLiveStream(key, CancellationToken.None); Task.WaitAll(task); } _openStreams.Clear(); } } } private class LiveStreamInfo { public DateTime Date; public bool EnableCloseTimer; public string Id; public bool Closed; public MediaSourceInfo MediaSource; } } }