using MediaBrowser.Common.IO; using MediaBrowser.Common.Logging; using MediaBrowser.Model.Dto; using MediaBrowser.UI.Configuration; using MediaBrowser.UI.Playback; using MediaBrowser.UI.Playback.ExternalPlayer; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel.Composition; using System.Diagnostics; using System.Globalization; using System.IO; using System.Threading.Tasks; namespace MediaBrowser.Plugins.Tmt5 { /// /// Class GenericExternalPlayer /// [Export(typeof(BaseMediaPlayer))] public class Tmt5MediaPlayer : BaseExternalPlayer { /// /// Gets the name. /// /// The name. public override string Name { get { return "TMT5"; } } /// /// Gets a value indicating whether this instance can pause. /// /// true if this instance can pause; otherwise, false. public override bool CanPause { get { return true; } } /// /// Gets a value indicating whether this instance can close automatically. /// /// true if this instance can close automatically; otherwise, false. protected override bool CanCloseAutomatically { get { return true; } } /// /// Gets the play state directory. /// /// The play state directory. private string PlayStateDirectory { get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ArcSoft"); } } /// /// The _current position ticks /// private long? _currentPositionTicks; /// /// Gets the current position ticks. /// /// The current position ticks. public override long? CurrentPositionTicks { get { return _currentPositionTicks; } } /// /// The _current playlist index /// private int _currentPlaylistIndex; /// /// Gets the index of the current playlist. /// /// The index of the current playlist. public override int CurrentPlaylistIndex { get { return _currentPlaylistIndex; } } /// /// Gets or sets the status file watcher. /// /// The status file watcher. private FileSystemWatcher StatusFileWatcher { get; set; } /// /// Gets or sets a value indicating whether this instance has started playing. /// /// true if this instance has started playing; otherwise, false. private bool HasStartedPlaying { get; set; } /// /// Gets or sets a value indicating whether this instance has stopped playing. /// /// true if this instance has stopped playing; otherwise, false. private bool HasStoppedPlaying { get; set; } /// /// Determines whether this instance can play the specified item. /// /// The item. /// true if this instance can play the specified item; otherwise, false. public override bool CanPlay(BaseItemDto item) { return item.IsVideo || item.IsAudio; } /// /// Called when [player stopped internal]. /// protected override void OnPlayerStoppedInternal() { DisposeFileSystemWatcher(); HasStartedPlaying = false; HasStoppedPlaying = false; _currentPlaylistIndex = 0; _currentPositionTicks = 0; base.OnPlayerStoppedInternal(); } /// /// Gets the command arguments. /// /// The items. /// The options. /// The player configuration. /// System.String. protected override string GetCommandArguments(List items, PlayOptions options, PlayerConfiguration playerConfiguration) { return "\"" + items[0].Path + "\""; } /// /// Called when [external player launched]. /// protected override void OnExternalPlayerLaunched() { base.OnExternalPlayerLaunched(); // If the playstate directory exists, start watching it if (Directory.Exists(PlayStateDirectory)) { ReloadFileSystemWatcher(); } } /// /// Pauses the internal. /// /// Task. protected override Task PauseInternal() { return SendCommandToMMC("-pause"); } /// /// Uns the pause internal. /// /// Task. protected override Task UnPauseInternal() { return SendCommandToMMC("-play"); } /// /// Stops the internal. /// /// Task. protected override Task StopInternal() { return SendCommandToMMC("-stop"); } /// /// Closes the player. /// /// Task. protected Task ClosePlayer() { return SendCommandToMMC("-close"); } /// /// Seeks the internal. /// /// The position ticks. /// Task. /// No media to seek to protected override Task SeekInternal(long positionTicks) { if (CurrentMedia == null) { throw new InvalidOperationException("No media to seek to"); } if (CurrentMedia.Chapters == null) { throw new InvalidOperationException("TMT5 cannot seek without chapter information"); } var chapterIndex = 0; for (var i = 0; i < CurrentMedia.Chapters.Count; i++) { if (CurrentMedia.Chapters[i].StartPositionTicks < positionTicks) { chapterIndex = i; } } return JumpToChapter(chapterIndex); } /// /// Jumps to chapter. /// /// The chapter. /// Task. protected Task JumpToChapter(int chapter) { return SendCommandToMMC(" -chapter " + chapter); } /// /// Sends an arbitrary command to the TMT MMC console /// /// The command. /// Task. protected Task SendCommandToMMC(string command) { return Task.Run(() => { var directory = Path.GetDirectoryName(CurrentPlayerConfiguration.Command); var processInfo = new ProcessStartInfo { FileName = Path.Combine(directory, "MMCEDT5.exe"), Arguments = command, CreateNoWindow = true }; Logger.Debug("{0} {1}", processInfo.FileName, processInfo.Arguments); using (var process = Process.Start(processInfo)) { process.WaitForExit(2000); } }); } /// /// Reloads the file system watcher. /// private void ReloadFileSystemWatcher() { DisposeFileSystemWatcher(); Logger.Info("Watching TMT folder: " + PlayStateDirectory); StatusFileWatcher = new FileSystemWatcher(PlayStateDirectory, "*.set") { IncludeSubdirectories = true }; // Need to include subdirectories since there are subfolders undearneath this with the TMT version #. StatusFileWatcher.Changed += StatusFileWatcher_Changed; StatusFileWatcher.EnableRaisingEvents = true; } private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Handles the Changed event of the StatusFileWatcher control. /// /// The source of the event. /// The instance containing the event data. async void StatusFileWatcher_Changed(object sender, FileSystemEventArgs e) { Logger.Debug("TMT File Watcher reports change type {1} at {0}", e.FullPath, e.ChangeType); NameValueCollection values; try { values = FileSystem.ParseIniFile(e.FullPath); } catch (IOException) { // This can happen if the file is being written to at the exact moment we're trying to access it // Unfortunately we kind of have to just eat it return; } var tmtPlayState = values["State"]; if (tmtPlayState.Equals("play", StringComparison.OrdinalIgnoreCase)) { PlayState = PlayState.Playing; // Playback just started HasStartedPlaying = true; if (CurrentPlayOptions.StartPositionTicks > 0) { SeekInternal(CurrentPlayOptions.StartPositionTicks); } } else if (tmtPlayState.Equals("pause", StringComparison.OrdinalIgnoreCase)) { PlayState = PlayState.Paused; } // If playback has previously started... // First notify the Progress event handler // Then check if playback has stopped if (HasStartedPlaying) { TimeSpan currentPosition; //TimeSpan.TryParse(values["TotalTime"], out currentDuration); if (TimeSpan.TryParse(values["CurTime"], UsCulture, out currentPosition)) { _currentPositionTicks = currentPosition.Ticks; } _currentPlaylistIndex = 0; // Playback has stopped if (tmtPlayState.Equals("stop", StringComparison.OrdinalIgnoreCase)) { Logger.Info("Playstate changed to stopped"); if (!HasStoppedPlaying) { HasStoppedPlaying = true; DisposeFileSystemWatcher(); await ClosePlayer().ConfigureAwait(false); } } } } /// /// Disposes the file system watcher. /// private void DisposeFileSystemWatcher() { if (StatusFileWatcher != null) { StatusFileWatcher.EnableRaisingEvents = false; StatusFileWatcher.Changed -= StatusFileWatcher_Changed; StatusFileWatcher.Dispose(); StatusFileWatcher = null; } } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool dispose) { if (dispose) { DisposeFileSystemWatcher(); } base.Dispose(dispose); } } }