using MediaBrowser.Common.Events; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using MediaBrowser.UI.Configuration; using MediaBrowser.UI.Controller; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.UI.Playback { /// /// Class BaseMediaPlayer /// public abstract class BaseMediaPlayer : IDisposable { /// /// Gets the logger. /// /// The logger. protected ILogger Logger { get; private set; } #region VolumeChanged /// /// Occurs when [volume changed]. /// public event EventHandler VolumeChanged; protected void OnVolumeChanged() { EventHelper.FireEventIfNotNull(VolumeChanged, this, EventArgs.Empty, Logger); } #endregion #region PlayStateChanged /// /// Occurs when [play state changed]. /// public event EventHandler PlayStateChanged; protected void OnPlayStateChanged() { EventHelper.FireEventIfNotNull(PlayStateChanged, this, EventArgs.Empty, Logger); } #endregion /// /// The null task result /// protected Task NullTaskResult = Task.FromResult(false); /// /// Gets a value indicating whether [supports multi file playback]. /// /// true if [supports multi file playback]; otherwise, false. public abstract bool SupportsMultiFilePlayback { get; } /// /// The currently playing items /// public List Playlist = new List(); /// /// The _play state /// private PlayState _playState; /// /// Gets or sets the state of the play. /// /// The state of the play. public PlayState PlayState { get { return _playState; } set { _playState = value; OnPlayStateChanged(); } } /// /// Gets or sets a value indicating whether this is mute. /// /// true if mute; otherwise, false. public bool Mute { get { return IsMuted; } set { SetMute(value); OnVolumeChanged(); } } /// /// Gets or sets the volume. /// /// The volume. public int Volume { get { return GetVolume(); } set { SetVolume(value); OnVolumeChanged(); } } /// /// Gets the current player configuration. /// /// The current player configuration. public PlayerConfiguration CurrentPlayerConfiguration { get; private set; } /// /// Gets the current play options. /// /// The current play options. public PlayOptions CurrentPlayOptions { get; private set; } /// /// Gets the name. /// /// The name. public abstract string Name { get; } /// /// Determines whether this instance can play the specified item. /// /// The item. /// true if this instance can play the specified item; otherwise, false. public abstract bool CanPlay(BaseItemDto item); /// /// Gets a value indicating whether this instance can change volume. /// /// true if this instance can change volume; otherwise, false. public abstract bool CanControlVolume { get; } /// /// Gets a value indicating whether this instance can mute. /// /// true if this instance can mute; otherwise, false. public abstract bool CanMute { get; } /// /// Gets a value indicating whether this instance can queue. /// /// true if this instance can queue; otherwise, false. public abstract bool CanQueue { get; } /// /// Gets a value indicating whether this instance can pause. /// /// true if this instance can pause; otherwise, false. public abstract bool CanPause { get; } /// /// Gets a value indicating whether this instance can seek. /// /// true if this instance can seek; otherwise, false. public abstract bool CanSeek { get; } /// /// Gets the index of the current playlist. /// /// The index of the current playlist. public virtual int CurrentPlaylistIndex { get { return 0; } } /// /// Gets the current media. /// /// The current media. public BaseItemDto CurrentMedia { get { return CurrentPlaylistIndex == -1 ? null : Playlist[CurrentPlaylistIndex]; } } /// /// Gets the current position ticks. /// /// The current position ticks. public virtual long? CurrentPositionTicks { get { return null; } } /// /// Gets a value indicating whether this instance is muted. /// /// true if this instance is muted; otherwise, false. protected virtual bool IsMuted { get { return false; } } /// /// Initializes a new instance of the class. /// protected BaseMediaPlayer(ILogger logger) { Logger = logger; } /// /// Sets the mute. /// /// if set to true [mute]. protected virtual void SetMute(bool mute) { } /// /// Sets the volume, on a scale from 0-100 /// /// The value. protected virtual void SetVolume(int value) { } /// /// Gets the volume. /// /// System.Int32. protected virtual int GetVolume() { return 0; } /// /// Plays the internal. /// /// The items. /// The options. /// The player configuration. protected abstract void PlayInternal(List items, PlayOptions options, PlayerConfiguration playerConfiguration); /// /// Queues the internal. /// /// The items. protected virtual void QueueInternal(List items) { } /// /// Stops the internal. /// /// Task. protected abstract Task StopInternal(); /// /// The play semaphore /// private readonly SemaphoreSlim PlaySemaphore = new SemaphoreSlim(1, 1); /// /// Gets or sets the progress update timer. /// /// The progress update timer. private Timer ProgressUpdateTimer { get; set; } /// /// Gets a value indicating whether this instance can monitor progress. /// /// true if this instance can monitor progress; otherwise, false. protected virtual bool CanMonitorProgress { get { return false; } } /// /// Stops this instance. /// /// Task. /// public Task Stop() { var playstate = PlayState; if (playstate == PlayState.Playing || playstate == PlayState.Paused) { Logger.Info("Stopping"); return StopInternal(); } throw new InvalidOperationException(string.Format("{0} is already {1}", Name, playstate)); } /// /// Plays the specified item. /// /// The options. /// The player configuration. /// Task. /// items internal async Task Play(PlayOptions options, PlayerConfiguration playerConfiguration) { if (options == null) { throw new ArgumentNullException("options"); } await PlaySemaphore.WaitAsync(); PlayState = PlayState.Playing; lock (Playlist) { Playlist.Clear(); Playlist.AddRange(options.Items); } CurrentPlayerConfiguration = playerConfiguration; CurrentPlayOptions = options; if (options.Items.Count > 1) { Logger.Info("Playing {0} items", options.Items.Count); } else { Logger.Info("Playing {0}", options.Items[0].Name); } try { PlayInternal(options.Items, options, playerConfiguration); } catch (Exception ex) { Logger.Info("Error beginning playback", ex); CurrentPlayerConfiguration = null; CurrentPlayOptions = null; Playlist.Clear(); PlayState = PlayState.Idle; PlaySemaphore.Release(); throw; } SendPlaybackStartCheckIn(options.Items[0]); ReloadProgressUpdateTimer(); } /// /// Restarts the progress update timer. /// private void ReloadProgressUpdateTimer() { ProgressUpdateTimer = new Timer(OnProgressTimerStopped, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)); } /// /// Called when [progress timer stopped]. /// /// The state. private void OnProgressTimerStopped(object state) { var index = CurrentPlaylistIndex; if (index != -1) { SendPlaybackProgressCheckIn(Playlist[index], CurrentPositionTicks); } } /// /// Queues the specified items. /// /// The items. /// items /// internal void Queue(List items) { if (items == null) { throw new ArgumentNullException("items"); } var playstate = PlayState; if (playstate != PlayState.Playing && playstate != PlayState.Paused) { throw new InvalidOperationException(string.Format("{0} cannot queue from playstate: {1}", Name, playstate)); } lock (Playlist) { Playlist.AddRange(items); } QueueInternal(items); } /// /// Called when [player stopped]. /// /// Last index of the playlist. /// The position ticks. protected void OnPlayerStopped(int? lastPlaylistIndex, long? positionTicks) { Logger.Info("Stopped"); if (positionTicks.HasValue && positionTicks.Value == 0) { positionTicks = null; } var items = Playlist.ToList(); DisposeProgressUpdateTimer(); var index = lastPlaylistIndex ?? CurrentPlaylistIndex; var lastItem = items[index]; SendPlaybackStopCheckIn(items[index], positionTicks); if (!CanMonitorProgress) { if (items.Count > 1) { MarkWatched(items.Except(new[] { lastItem })); } } OnPlayerStoppedInternal(); UIKernel.Instance.PlaybackManager.OnPlaybackCompleted(this, Playlist.ToList()); CurrentPlayerConfiguration = null; CurrentPlayOptions = null; Logger.Info("Clearing Playlist"); Playlist.Clear(); PlayState = PlayState.Idle; PlaySemaphore.Release(); } /// /// Called when [player stopped internal]. /// protected virtual void OnPlayerStoppedInternal() { } /// /// Seeks the specified position ticks. /// /// The position ticks. /// Task. /// public async Task Seek(long positionTicks) { var playState = PlayState; if (playState == PlayState.Playing || playState == PlayState.Paused) { await SeekInternal(positionTicks); } else { throw new InvalidOperationException(string.Format("Cannot seek {0} with playstate {1}", Name, PlayState)); } } /// /// Seeks the internal. /// /// The position ticks. /// Task. protected virtual Task SeekInternal(long positionTicks) { return NullTaskResult; } /// /// The ten seconds /// private static readonly long TenSeconds = TimeSpan.FromSeconds(10).Ticks; /// /// Goes to next chapter. /// /// Task. public virtual Task GoToNextChapter() { var current = CurrentPositionTicks; var chapter = CurrentMedia.Chapters.FirstOrDefault(c => c.StartPositionTicks > current); return chapter != null ? Seek(chapter.StartPositionTicks) : NullTaskResult; } /// /// Goes to previous chapter. /// /// Task. public virtual Task GoToPreviousChapter() { var current = CurrentPositionTicks; var chapter = CurrentMedia.Chapters.LastOrDefault(c => c.StartPositionTicks < current - TenSeconds); return chapter != null ? Seek(chapter.StartPositionTicks) : NullTaskResult; } /// /// Pauses this instance. /// /// Task. /// public async Task Pause() { if (PlayState == PlayState.Playing) { await PauseInternal(); PlayState = PlayState.Paused; } else { throw new InvalidOperationException(string.Format("Cannot pause {0} with playstate {1}", Name, PlayState)); } } /// /// Pauses the internal. /// /// Task. protected virtual Task PauseInternal() { return NullTaskResult; } /// /// Uns the pause. /// /// Task. /// public async Task UnPause() { if (PlayState == PlayState.Paused) { await UnPauseInternal(); PlayState = PlayState.Playing; } else { throw new InvalidOperationException(string.Format("Cannot unpause {0} with playstate {1}", Name, PlayState)); } } /// /// Uns the pause internal. /// /// Task. protected virtual Task UnPauseInternal() { return NullTaskResult; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); } /// /// 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) { Logger.Info("Disposing"); DisposeProgressUpdateTimer(); if (PlayState == PlayState.Playing || PlayState == PlayState.Paused) { var index = CurrentPlaylistIndex; if (index != -1) { SendPlaybackStopCheckIn(Playlist[index], CurrentPositionTicks); } Task.Run(() => Stop()); Thread.Sleep(1000); } PlaySemaphore.Dispose(); } /// /// Disposes the progress update timer. /// private void DisposeProgressUpdateTimer() { if (ProgressUpdateTimer != null) { ProgressUpdateTimer.Dispose(); } } /// /// Sends the playback start check in. /// /// The item. protected async void SendPlaybackStartCheckIn(BaseItemDto item) { if (string.IsNullOrEmpty(item.Id)) { return; } Logger.Info("Sending playback start checkin for {0}", item.Name); try { await UIKernel.Instance.ApiClient.ReportPlaybackStartAsync(item.Id, App.Instance.CurrentUser.Id); } catch (HttpException ex) { Logger.ErrorException("Error sending playback start checking for {0}", ex, item.Name); } } /// /// Sends the playback progress check in. /// /// The item. /// The position ticks. protected async void SendPlaybackProgressCheckIn(BaseItemDto item, long? positionTicks) { if (string.IsNullOrEmpty(item.Id)) { return; } var position = positionTicks.HasValue ? TimeSpan.FromTicks(positionTicks.Value).ToString() : "unknown"; Logger.Info("Sending playback progress checkin for {0} at position {1}", item.Name, position); try { await UIKernel.Instance.ApiClient.ReportPlaybackProgressAsync(item.Id, App.Instance.CurrentUser.Id, positionTicks); } catch (HttpException ex) { Logger.ErrorException("Error sending playback progress checking for {0}", ex, item.Name); } } /// /// Sends the playback stop check in. /// /// The item. /// The position ticks. protected async void SendPlaybackStopCheckIn(BaseItemDto item, long? positionTicks) { if (string.IsNullOrEmpty(item.Id)) { return; } var position = positionTicks.HasValue ? TimeSpan.FromTicks(positionTicks.Value).ToString() : "unknown"; Logger.Info("Sending playback stop checkin for {0} at position {1}", item.Name, position); try { await UIKernel.Instance.ApiClient.ReportPlaybackStoppedAsync(item.Id, App.Instance.CurrentUser.Id, positionTicks); } catch (HttpException ex) { Logger.ErrorException("Error sending playback stop checking for {0}", ex, item.Name); } } /// /// Marks the watched. /// /// The items. protected async void MarkWatched(IEnumerable items) { var idList = items.Where(i => !string.IsNullOrEmpty(i.Id)).Select(i => i.Id); try { await UIKernel.Instance.ApiClient.UpdatePlayedStatusAsync(idList.First(), App.Instance.CurrentUser.Id, true); } catch (HttpException ex) { Logger.ErrorException("Error marking items watched", ex); } } /// /// Called when [media changed]. /// /// Old index of the playlist. /// The ending position ticks. /// New index of the playlist. protected void OnMediaChanged(int oldPlaylistIndex, long? endingPositionTicks, int newPlaylistIndex) { DisposeProgressUpdateTimer(); Task.Run(() => { if (oldPlaylistIndex != -1) { SendPlaybackStopCheckIn(Playlist[oldPlaylistIndex], endingPositionTicks); } if (newPlaylistIndex != -1) { SendPlaybackStartCheckIn(Playlist[newPlaylistIndex]); } }); ReloadProgressUpdateTimer(); } } }