فهرست منبع

support multiple remote control outputs

Luke Pulverenti 11 سال پیش
والد
کامیت
f176307e59

+ 44 - 4
MediaBrowser.Api/ApiEntryPoint.cs

@@ -62,7 +62,7 @@ namespace MediaBrowser.Api
         {
             var jobCount = _activeTranscodingJobs.Count;
 
-            Parallel.ForEach(_activeTranscodingJobs, OnTranscodeKillTimerStopped);
+            Parallel.ForEach(_activeTranscodingJobs, KillTranscodingJob);
 
             // Try to allow for some time to kill the ffmpeg processes and delete the partial stream files
             if (jobCount > 0)
@@ -84,7 +84,8 @@ namespace MediaBrowser.Api
         /// <param name="process">The process.</param>
         /// <param name="isVideo">if set to <c>true</c> [is video].</param>
         /// <param name="startTimeTicks">The start time ticks.</param>
-        public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process, bool isVideo, long? startTimeTicks)
+        /// <param name="sourcePath">The source path.</param>
+        public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process, bool isVideo, long? startTimeTicks, string sourcePath)
         {
             lock (_activeTranscodingJobs)
             {
@@ -95,7 +96,8 @@ namespace MediaBrowser.Api
                     Process = process,
                     ActiveRequestCount = 1,
                     IsVideo = isVideo,
-                    StartTimeTicks = startTimeTicks
+                    StartTimeTicks = startTimeTicks,
+                    SourcePath = sourcePath
                 });
             }
         }
@@ -196,10 +198,47 @@ namespace MediaBrowser.Api
         /// Called when [transcode kill timer stopped].
         /// </summary>
         /// <param name="state">The state.</param>
-        private async void OnTranscodeKillTimerStopped(object state)
+        private void OnTranscodeKillTimerStopped(object state)
         {
             var job = (TranscodingJob)state;
 
+            KillTranscodingJob(job);
+        }
+
+        /// <summary>
+        /// Kills the single transcoding job.
+        /// </summary>
+        /// <param name="sourcePath">The source path.</param>
+        internal void KillSingleTranscodingJob(string sourcePath)
+        {
+            if (string.IsNullOrEmpty(sourcePath))
+            {
+                throw new ArgumentNullException("sourcePath");
+            }
+
+            var jobs = new List<TranscodingJob>();
+
+            lock (_activeTranscodingJobs)
+            {
+                // This is really only needed for HLS. 
+                // Progressive streams can stop on their own reliably
+                jobs.AddRange(_activeTranscodingJobs.Where(i => string.Equals(sourcePath, i.SourcePath) && i.Type == TranscodingJobType.Hls));
+            }
+
+            // This method of killing is a bit of a shortcut, but it saves clients from having to send a request just for that
+            // But we can only kill if there's one active job. If there are more we won't know which one to stop
+            if (jobs.Count == 1)
+            {
+                KillTranscodingJob(jobs.First());
+            }
+        }
+
+        /// <summary>
+        /// Kills the transcoding job.
+        /// </summary>
+        /// <param name="job">The job.</param>
+        private async void KillTranscodingJob(TranscodingJob job)
+        {
             lock (_activeTranscodingJobs)
             {
                 _activeTranscodingJobs.Remove(job);
@@ -373,6 +412,7 @@ namespace MediaBrowser.Api
 
         public bool IsVideo { get; set; }
         public long? StartTimeTicks { get; set; }
+        public string SourcePath { get; set; }
     }
 
     /// <summary>

+ 181 - 0
MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs

@@ -0,0 +1,181 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Logging;
+using ServiceStack.Common.Web;
+using ServiceStack.ServiceHost;
+using System;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Api
+{
+    public class AuthorizationRequestFilterAttribute : Attribute, IHasRequestFilter
+    {
+        //This property will be resolved by the IoC container
+        /// <summary>
+        /// Gets or sets the user manager.
+        /// </summary>
+        /// <value>The user manager.</value>
+        public IUserManager UserManager { get; set; }
+
+        public ISessionManager SessionManager { get; set; }
+
+        /// <summary>
+        /// Gets or sets the logger.
+        /// </summary>
+        /// <value>The logger.</value>
+        public ILogger Logger { get; set; }
+
+        /// <summary>
+        /// The request filter is executed before the service.
+        /// </summary>
+        /// <param name="request">The http request wrapper</param>
+        /// <param name="response">The http response wrapper</param>
+        /// <param name="requestDto">The request DTO</param>
+        public void RequestFilter(IHttpRequest request, IHttpResponse response, object requestDto)
+        {
+            //This code is executed before the service
+
+            var auth = GetAuthorization(request);
+
+            if (auth != null)
+            {
+                User user = null;
+
+                if (auth.ContainsKey("UserId"))
+                {
+                    var userId = auth["UserId"];
+
+                    if (!string.IsNullOrEmpty(userId))
+                    {
+                        user = UserManager.GetUserById(new Guid(userId));
+                    }
+                }
+
+                string deviceId;
+                string device;
+                string client;
+                string version;
+
+                auth.TryGetValue("DeviceId", out deviceId);
+                auth.TryGetValue("Device", out device);
+                auth.TryGetValue("Client", out client);
+                auth.TryGetValue("Version", out version);
+
+                if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version))
+                {
+                    SessionManager.LogConnectionActivity(client, version, deviceId, device, user);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the auth.
+        /// </summary>
+        /// <param name="httpReq">The HTTP req.</param>
+        /// <returns>Dictionary{System.StringSystem.String}.</returns>
+        public static Dictionary<string, string> GetAuthorization(IHttpRequest httpReq)
+        {
+            var auth = httpReq.Headers[HttpHeaders.Authorization];
+
+            return GetAuthorization(auth);
+        }
+
+        /// <summary>
+        /// Gets the authorization.
+        /// </summary>
+        /// <param name="httpReq">The HTTP req.</param>
+        /// <returns>Dictionary{System.StringSystem.String}.</returns>
+        public static AuthorizationInfo GetAuthorization(IRequestContext httpReq)
+        {
+            var header = httpReq.GetHeader("Authorization");
+
+            var auth = GetAuthorization(header);
+
+            string userId;
+            string deviceId;
+            string device;
+            string client;
+            string version;
+
+            auth.TryGetValue("UserId", out userId);
+            auth.TryGetValue("DeviceId", out deviceId);
+            auth.TryGetValue("Device", out device);
+            auth.TryGetValue("Client", out client);
+            auth.TryGetValue("Version", out version);
+
+            return new AuthorizationInfo
+            {
+                Client = client,
+                Device = device,
+                DeviceId = deviceId,
+                UserId = userId,
+                Version = version
+            };
+        }
+
+        /// <summary>
+        /// Gets the authorization.
+        /// </summary>
+        /// <param name="authorizationHeader">The authorization header.</param>
+        /// <returns>Dictionary{System.StringSystem.String}.</returns>
+        private static Dictionary<string, string> GetAuthorization(string authorizationHeader)
+        {
+            if (authorizationHeader == null) return null;
+
+            var parts = authorizationHeader.Split(' ');
+
+            // There should be at least to parts
+            if (parts.Length < 2) return null;
+
+            // It has to be a digest request
+            if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase))
+            {
+                return null;
+            }
+
+            // Remove uptil the first space
+            authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' '));
+            parts = authorizationHeader.Split(',');
+
+            var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+            foreach (var item in parts)
+            {
+                var param = item.Trim().Split(new[] { '=' }, 2);
+                result.Add(param[0], param[1].Trim(new[] { '"' }));
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// A new shallow copy of this filter is used on every request.
+        /// </summary>
+        /// <returns>IHasRequestFilter.</returns>
+        public IHasRequestFilter Copy()
+        {
+            return this;
+        }
+
+        /// <summary>
+        /// Order in which Request Filters are executed.
+        /// &lt;0 Executed before global request filters
+        /// &gt;0 Executed after global request filters
+        /// </summary>
+        /// <value>The priority.</value>
+        public int Priority
+        {
+            get { return 0; }
+        }
+    }
+
+    public class AuthorizationInfo
+    {
+        public string UserId;
+        public string DeviceId;
+        public string Device;
+        public string Client;
+        public string Version;
+    }
+}

+ 1 - 146
MediaBrowser.Api/BaseApiService.cs

@@ -2,9 +2,7 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Logging;
-using ServiceStack.Common.Web;
 using ServiceStack.ServiceHost;
 using System;
 using System.Collections.Generic;
@@ -15,7 +13,7 @@ namespace MediaBrowser.Api
     /// <summary>
     /// Class BaseApiService
     /// </summary>
-    [RequestFilter]
+    [AuthorizationRequestFilter]
     public class BaseApiService : IHasResultFactory, IRestfulService
     {
         /// <summary>
@@ -308,147 +306,4 @@ namespace MediaBrowser.Api
             return item;
         }
     }
-
-    /// <summary>
-    /// Class RequestFilterAttribute
-    /// </summary>
-    public class RequestFilterAttribute : Attribute, IHasRequestFilter
-    {
-        //This property will be resolved by the IoC container
-        /// <summary>
-        /// Gets or sets the user manager.
-        /// </summary>
-        /// <value>The user manager.</value>
-        public IUserManager UserManager { get; set; }
-
-        public ISessionManager SessionManager { get; set; }
-
-        /// <summary>
-        /// Gets or sets the logger.
-        /// </summary>
-        /// <value>The logger.</value>
-        public ILogger Logger { get; set; }
-
-        /// <summary>
-        /// The request filter is executed before the service.
-        /// </summary>
-        /// <param name="request">The http request wrapper</param>
-        /// <param name="response">The http response wrapper</param>
-        /// <param name="requestDto">The request DTO</param>
-        public void RequestFilter(IHttpRequest request, IHttpResponse response, object requestDto)
-        {
-            //This code is executed before the service
-
-            var auth = GetAuthorization(request);
-
-            if (auth != null)
-            {
-                User user = null;
-
-                if (auth.ContainsKey("UserId"))
-                {
-                    var userId = auth["UserId"];
-
-                    if (!string.IsNullOrEmpty(userId))
-                    {
-                        user = UserManager.GetUserById(new Guid(userId));
-                    }
-                }
-
-                string deviceId;
-                string device;
-                string client;
-                string version;
-
-                auth.TryGetValue("DeviceId", out deviceId);
-                auth.TryGetValue("Device", out device);
-                auth.TryGetValue("Client", out client);
-                auth.TryGetValue("Version", out version);
-
-                if (!string.IsNullOrEmpty(client) && !string.IsNullOrEmpty(deviceId) && !string.IsNullOrEmpty(device) && !string.IsNullOrEmpty(version))
-                {
-                    SessionManager.LogConnectionActivity(client, version, deviceId, device, user);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Gets the auth.
-        /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        public static Dictionary<string, string> GetAuthorization(IHttpRequest httpReq)
-        {
-            var auth = httpReq.Headers[HttpHeaders.Authorization];
-
-            return GetAuthorization(auth);
-        }
-
-        /// <summary>
-        /// Gets the authorization.
-        /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        public static Dictionary<string, string> GetAuthorization(IRequestContext httpReq)
-        {
-            var auth = httpReq.GetHeader("Authorization");
-
-            return GetAuthorization(auth);
-        }
-
-        /// <summary>
-        /// Gets the authorization.
-        /// </summary>
-        /// <param name="authorizationHeader">The authorization header.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private static Dictionary<string, string> GetAuthorization(string authorizationHeader)
-        {
-            if (authorizationHeader == null) return null;
-
-            var parts = authorizationHeader.Split(' ');
-
-            // There should be at least to parts
-            if (parts.Length < 2) return null;
-
-            // It has to be a digest request
-            if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase))
-            {
-                return null;
-            }
-
-            // Remove uptil the first space
-            authorizationHeader = authorizationHeader.Substring(authorizationHeader.IndexOf(' '));
-            parts = authorizationHeader.Split(',');
-
-            var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
-            foreach (var item in parts)
-            {
-                var param = item.Trim().Split(new[] { '=' }, 2);
-                result.Add(param[0], param[1].Trim(new[] { '"' }));
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// A new shallow copy of this filter is used on every request.
-        /// </summary>
-        /// <returns>IHasRequestFilter.</returns>
-        public IHasRequestFilter Copy()
-        {
-            return this;
-        }
-
-        /// <summary>
-        /// Order in which Request Filters are executed.
-        /// &lt;0 Executed before global request filters
-        /// &gt;0 Executed after global request filters
-        /// </summary>
-        /// <value>The priority.</value>
-        public int Priority
-        {
-            get { return 0; }
-        }
-    }
 }

+ 6 - 1
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -70,6 +70,7 @@
     <Compile Include="DefaultTheme\Models.cs" />
     <Compile Include="DisplayPreferencesService.cs" />
     <Compile Include="EnvironmentService.cs" />
+    <Compile Include="AuthorizationRequestFilterAttribute.cs" />
     <Compile Include="GamesService.cs" />
     <Compile Include="Images\ImageByNameService.cs" />
     <Compile Include="Images\ImageRequest.cs" />
@@ -88,6 +89,8 @@
     <Compile Include="PackageService.cs" />
     <Compile Include="Playback\Hls\AudioHlsService.cs" />
     <Compile Include="Playback\Hls\BaseHlsService.cs" />
+    <Compile Include="Playback\Hls\HlsSegmentResponseFilter.cs" />
+    <Compile Include="Playback\Hls\HlsSegmentService.cs" />
     <Compile Include="Playback\Hls\VideoHlsService.cs" />
     <Compile Include="Playback\Progressive\AudioService.cs" />
     <Compile Include="Playback\Progressive\BaseProgressiveStreamingService.cs" />
@@ -143,7 +146,9 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
-  <ItemGroup />
+  <ItemGroup>
+    <Folder Include="Filters\" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <PropertyGroup>
     <PostBuildEvent>

+ 1 - 1
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -613,7 +613,7 @@ namespace MediaBrowser.Api.Playback
                 EnableRaisingEvents = true
             };
 
-            ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, video != null, state.Request.StartTimeTicks);
+            ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, video != null, state.Request.StartTimeTicks, state.Item.Path);
 
             Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
 

+ 0 - 36
MediaBrowser.Api/Playback/Hls/AudioHlsService.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using ServiceStack.ServiceHost;
 using System;
-using System.IO;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -20,27 +19,6 @@ namespace MediaBrowser.Api.Playback.Hls
 
     }
 
-    /// <summary>
-    /// Class GetHlsAudioSegment
-    /// </summary>
-    [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
-    [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
-    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
-    public class GetHlsAudioSegment
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
     /// <summary>
     /// Class AudioHlsService
     /// </summary>
@@ -59,20 +37,6 @@ namespace MediaBrowser.Api.Playback.Hls
         {
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetHlsAudioSegment request)
-        {
-            var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo);
-
-            file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file);
-
-            return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 0 - 24
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -10,7 +10,6 @@ using MediaBrowser.Model.IO;
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 
@@ -213,29 +212,6 @@ namespace MediaBrowser.Api.Playback.Hls
             return count;
         }
 
-        protected void ExtendHlsTimer(string itemId, string playlistId)
-        {
-            var normalizedPlaylistId = playlistId.Replace("-low", string.Empty);
-
-            foreach (var playlist in Directory.EnumerateFiles(ApplicationPaths.EncodedMediaCachePath, "*.m3u8")
-                .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
-                .ToList())
-            {
-                ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
-
-                // Avoid implicitly captured closure
-                var playlist1 = playlist;
-
-                Task.Run(async () =>
-                {
-                    // This is an arbitrary time period corresponding to when the request completes.
-                    await Task.Delay(30000).ConfigureAwait(false);
-
-                    ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist1, TranscodingJobType.Hls);
-                });
-            }
-        }
-
         /// <summary>
         /// Gets the command line arguments.
         /// </summary>

+ 53 - 0
MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs

@@ -0,0 +1,53 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Logging;
+using ServiceStack.ServiceHost;
+using ServiceStack.Text.Controller;
+using System;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Api.Playback.Hls
+{
+    public class HlsSegmentResponseFilter : Attribute, IHasResponseFilter
+    {
+        public ILogger Logger { get; set; }
+        public IServerApplicationPaths ApplicationPaths { get; set; }
+
+        public void ResponseFilter(IHttpRequest req, IHttpResponse res, object response)
+        {
+            var pathInfo = PathInfo.Parse(req.PathInfo);
+            var itemId = pathInfo.GetArgumentValue<string>(1);
+            var playlistId = pathInfo.GetArgumentValue<string>(3);
+
+            OnEndRequest(itemId, playlistId);
+        }
+
+        public IHasResponseFilter Copy()
+        {
+            return this;
+        }
+
+        public int Priority
+        {
+            get { return -1; }
+        }
+
+        /// <summary>
+        /// Called when [end request].
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="playlistId">The playlist id.</param>
+        protected void OnEndRequest(string itemId, string playlistId)
+        {
+            Logger.Info("OnEndRequest " + playlistId);
+            var normalizedPlaylistId = playlistId.Replace("-low", string.Empty);
+
+            foreach (var playlist in Directory.EnumerateFiles(ApplicationPaths.EncodedMediaCachePath, "*.m3u8")
+                .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
+                .ToList())
+            {
+                ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
+            }
+        }
+    }
+}

+ 147 - 0
MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs

@@ -0,0 +1,147 @@
+using MediaBrowser.Controller;
+using ServiceStack.ServiceHost;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Playback.Hls
+{
+    /// <summary>
+    /// Class GetHlsAudioSegment
+    /// </summary>
+    [Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
+    [Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
+    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
+    public class GetHlsAudioSegment
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+
+        /// <summary>
+        /// Gets or sets the segment id.
+        /// </summary>
+        /// <value>The segment id.</value>
+        public string SegmentId { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetHlsVideoSegment
+    /// </summary>
+    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
+    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
+    public class GetHlsVideoSegment
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+
+        public string PlaylistId { get; set; }
+
+        /// <summary>
+        /// Gets or sets the segment id.
+        /// </summary>
+        /// <value>The segment id.</value>
+        public string SegmentId { get; set; }
+    }
+
+    /// <summary>
+    /// Class GetHlsVideoSegment
+    /// </summary>
+    [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
+    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
+    public class GetHlsPlaylist
+    {
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public string Id { get; set; }
+
+        public string PlaylistId { get; set; }
+    }
+
+    public class HlsSegmentService : BaseApiService
+    {
+        private readonly IServerApplicationPaths _appPaths;
+
+        public HlsSegmentService(IServerApplicationPaths appPaths)
+        {
+            _appPaths = appPaths;
+        }
+
+        public object Get(GetHlsPlaylist request)
+        {
+            OnBeginRequest(request.PlaylistId);
+
+            var file = request.PlaylistId + Path.GetExtension(RequestContext.PathInfo);
+
+            file = Path.Combine(_appPaths.EncodedMediaCachePath, file);
+
+            return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetHlsVideoSegment request)
+        {
+            var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo);
+
+            file = Path.Combine(_appPaths.EncodedMediaCachePath, file);
+
+            OnBeginRequest(request.PlaylistId);
+
+            return ResultFactory.GetStaticFileResult(RequestContext, file);
+        }
+
+        /// <summary>
+        /// Gets the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <returns>System.Object.</returns>
+        public object Get(GetHlsAudioSegment request)
+        {
+            var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo);
+
+            file = Path.Combine(_appPaths.EncodedMediaCachePath, file);
+
+            return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite);
+        }
+
+        /// <summary>
+        /// Called when [begin request].
+        /// </summary>
+        /// <param name="playlistId">The playlist id.</param>
+        protected void OnBeginRequest(string playlistId)
+        {
+            var normalizedPlaylistId = playlistId.Replace("-low", string.Empty);
+
+            foreach (var playlist in Directory.EnumerateFiles(_appPaths.EncodedMediaCachePath, "*.m3u8")
+                .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
+                .ToList())
+            {
+                ExtendPlaylistTimer(playlist);
+            }
+        }
+
+        private void ExtendPlaylistTimer(string playlist)
+        {
+            ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
+
+            Task.Run(async () =>
+            {
+                await Task.Delay(20000).ConfigureAwait(false);
+
+                ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
+            });
+        }
+    }
+}

+ 1 - 66
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -5,7 +5,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.IO;
 using ServiceStack.ServiceHost;
 using System;
-using System.IO;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -31,44 +30,6 @@ namespace MediaBrowser.Api.Playback.Hls
         }
     }
 
-    /// <summary>
-    /// Class GetHlsVideoSegment
-    /// </summary>
-    [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
-    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
-    public class GetHlsVideoSegment
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public string Id { get; set; }
-
-        public string PlaylistId { get; set; }
-        
-        /// <summary>
-        /// Gets or sets the segment id.
-        /// </summary>
-        /// <value>The segment id.</value>
-        public string SegmentId { get; set; }
-    }
-
-    /// <summary>
-    /// Class GetHlsVideoSegment
-    /// </summary>
-    [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
-    [Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
-    public class GetHlsPlaylist
-    {
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public string Id { get; set; }
-
-        public string PlaylistId { get; set; }
-    }
-    
     /// <summary>
     /// Class VideoHlsService
     /// </summary>
@@ -82,38 +43,12 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="isoManager">The iso manager.</param>
         /// <param name="mediaEncoder">The media encoder.</param>
+        /// <param name="dtoService">The dto service.</param>
         public VideoHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService)
             : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService)
         {
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        public object Get(GetHlsVideoSegment request)
-        {
-            ExtendHlsTimer(request.Id, request.PlaylistId);
-            
-            var file = request.SegmentId + Path.GetExtension(RequestContext.PathInfo);
-
-            file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file);
-
-            return ResultFactory.GetStaticFileResult(RequestContext, file);
-        }
-
-        public object Get(GetHlsPlaylist request)
-        {
-            ExtendHlsTimer(request.Id, request.PlaylistId);
-
-            var file = request.PlaylistId + Path.GetExtension(RequestContext.PathInfo);
-
-            file = Path.Combine(ApplicationPaths.EncodedMediaCachePath, file);
-
-            return ResultFactory.GetStaticFileResult(RequestContext, file, FileShare.ReadWrite);
-        }
-
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 30 - 188
MediaBrowser.Api/SessionsService.cs

@@ -1,7 +1,5 @@
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Session;
 using ServiceStack.ServiceHost;
 using System;
@@ -189,6 +187,7 @@ namespace MediaBrowser.Api
         /// Initializes a new instance of the <see cref="SessionsService" /> class.
         /// </summary>
         /// <param name="sessionManager">The session manager.</param>
+        /// <param name="dtoService">The dto service.</param>
         public SessionsService(ISessionManager sessionManager, IDtoService dtoService)
         {
             _sessionManager = sessionManager;
@@ -214,52 +213,15 @@ namespace MediaBrowser.Api
 
         public void Post(SendPlaystateCommand request)
         {
-            var task = SendPlaystateCommand(request);
-
-            Task.WaitAll(task);
-        }
-
-        private async Task SendPlaystateCommand(SendPlaystateCommand request)
-        {
-            var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id);
-
-            if (session == null)
+            var command = new PlaystateRequest
             {
-                throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id));
-            }
+                Command = request.Command,
+                SeekPositionTicks = request.SeekPositionTicks
+            };
 
-            if (!session.SupportsRemoteControl)
-            {
-                throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id));
-            }
+            var task = _sessionManager.SendPlaystateCommand(request.Id, command, CancellationToken.None);
 
-            var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open);
-
-            if (socket != null)
-            {
-                try
-                {
-                    await socket.SendAsync(new WebSocketMessage<PlaystateRequest>
-                    {
-                        MessageType = "Playstate",
-
-                        Data = new PlaystateRequest
-                        {
-                            Command = request.Command,
-                            SeekPositionTicks = request.SeekPositionTicks
-                        }
-
-                    }, CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.ErrorException("Error sending web socket message", ex);
-                }
-            }
-            else
-            {
-                throw new InvalidOperationException("The requested session does not have an open web socket.");
-            }
+            Task.WaitAll(task);
         }
 
         /// <summary>
@@ -268,55 +230,17 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public void Post(BrowseTo request)
         {
-            var task = BrowseTo(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Browses to.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ResourceNotFoundException"></exception>
-        /// <exception cref="System.ArgumentException"></exception>
-        /// <exception cref="System.InvalidOperationException">The requested session does not have an open web socket.</exception>
-        private async Task BrowseTo(BrowseTo request)
-        {
-            var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id);
-
-            if (session == null)
-            {
-                throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id));
-            }
-
-            if (!session.SupportsRemoteControl)
+            var command = new BrowseRequest
             {
-                throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id));
-            }
+                Context = request.Context,
+                ItemId = request.ItemId,
+                ItemName = request.ItemName,
+                ItemType = request.ItemType
+            };
 
-            var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open);
+            var task = _sessionManager.SendBrowseCommand(request.Id, command, CancellationToken.None);
 
-            if (socket != null)
-            {
-                try
-                {
-                    await socket.SendAsync(new WebSocketMessage<BrowseTo>
-                    {
-                        MessageType = "Browse",
-                        Data = request
-
-                    }, CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.ErrorException("Error sending web socket message", ex);
-                }
-            }
-            else
-            {
-                throw new InvalidOperationException("The requested session does not have an open web socket.");
-            }
+            Task.WaitAll(task);
         }
 
         /// <summary>
@@ -336,53 +260,16 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public void Post(SendMessageCommand request)
         {
-            var task = SendMessageCommand(request);
-
-            Task.WaitAll(task);
-        }
-
-        private async Task SendMessageCommand(SendMessageCommand request)
-        {
-            var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id);
-
-            if (session == null)
-            {
-                throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id));
-            }
-
-            if (!session.SupportsRemoteControl)
+            var command = new MessageCommand
             {
-                throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id));
-            }
+                Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header,
+                TimeoutMs = request.TimeoutMs,
+                Text = request.Text
+            };
 
-            var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open);
+            var task = _sessionManager.SendMessageCommand(request.Id, command, CancellationToken.None);
 
-            if (socket != null)
-            {
-                try
-                {
-                    await socket.SendAsync(new WebSocketMessage<MessageCommand>
-                    {
-                        MessageType = "MessageCommand",
-
-                        Data = new MessageCommand
-                        {
-                            Header = string.IsNullOrEmpty(request.Header) ? "Message from Server" : request.Header,
-                            TimeoutMs = request.TimeoutMs,
-                            Text = request.Text
-                        }
-
-                    }, CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.ErrorException("Error sending web socket message", ex);
-                }
-            }
-            else
-            {
-                throw new InvalidOperationException("The requested session does not have an open web socket.");
-            }
+            Task.WaitAll(task);
         }
 
         /// <summary>
@@ -391,62 +278,17 @@ namespace MediaBrowser.Api
         /// <param name="request">The request.</param>
         public void Post(Play request)
         {
-            var task = Play(request);
-
-            Task.WaitAll(task);
-        }
-
-        /// <summary>
-        /// Plays the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ResourceNotFoundException"></exception>
-        /// <exception cref="System.ArgumentException"></exception>
-        /// <exception cref="System.InvalidOperationException">The requested session does not have an open web socket.</exception>
-        private async Task Play(Play request)
-        {
-            var session = _sessionManager.Sessions.FirstOrDefault(i => i.Id == request.Id);
-
-            if (session == null)
+            var command = new PlayRequest
             {
-                throw new ResourceNotFoundException(string.Format("Session {0} not found.", request.Id));
-            }
+                ItemIds = request.ItemIds.Split(',').ToArray(),
 
-            if (!session.SupportsRemoteControl)
-            {
-                throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id));
-            }
+                PlayCommand = request.PlayCommand,
+                StartPositionTicks = request.StartPositionTicks
+            };
 
-            var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open);
+            var task = _sessionManager.SendPlayCommand(request.Id, command, CancellationToken.None);
 
-            if (socket != null)
-            {
-                try
-                {
-                    await socket.SendAsync(new WebSocketMessage<PlayRequest>
-                    {
-                        MessageType = "Play",
-
-                        Data = new PlayRequest
-                        {
-                            ItemIds = request.ItemIds.Split(',').ToArray(),
-
-                            PlayCommand = request.PlayCommand,
-                            StartPositionTicks = request.StartPositionTicks
-                        }
-
-                    }, CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.ErrorException("Error sending web socket message", ex);
-                }
-            }
-            else
-            {
-                throw new InvalidOperationException("The requested session does not have an open web socket.");
-            }
+            Task.WaitAll(task);
         }
     }
 }

+ 10 - 13
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -663,19 +663,11 @@ namespace MediaBrowser.Api.UserLibrary
 
         private SessionInfo GetSession()
         {
-            var auth = RequestFilterAttribute.GetAuthorization(RequestContext);
+            var auth = AuthorizationRequestFilterAttribute.GetAuthorization(RequestContext);
 
-            string deviceId;
-            string client;
-            string version;
-
-            auth.TryGetValue("DeviceId", out deviceId);
-            auth.TryGetValue("Client", out client);
-            auth.TryGetValue("Version", out version);
-
-            return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, deviceId) &&
-                string.Equals(i.Client, client) &&
-                string.Equals(i.ApplicationVersion, version));
+            return _sessionManager.Sessions.First(i => string.Equals(i.DeviceId, auth.DeviceId) &&
+                string.Equals(i.Client, auth.Client) &&
+                string.Equals(i.ApplicationVersion, auth.Version));
         }
 
         /// <summary>
@@ -726,7 +718,12 @@ namespace MediaBrowser.Api.UserLibrary
 
             var item = _dtoService.GetItemByDtoId(request.Id, user.Id);
 
-            var task = _sessionManager.OnPlaybackStopped(item, request.PositionTicks, GetSession().Id);
+            // Kill the encoding
+            ApiEntryPoint.Instance.KillSingleTranscodingJob(item.Path);
+
+            var session = GetSession();
+
+            var task = _sessionManager.OnPlaybackStopped(item, request.PositionTicks, session.Id);
 
             Task.WaitAll(task);
         }

+ 36 - 0
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -89,5 +89,41 @@ namespace MediaBrowser.Controller.Session
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task SendSystemCommand(Guid sessionId, SystemCommand command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the message command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendMessageCommand(Guid sessionId, MessageCommand command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the play command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendPlayCommand(Guid sessionId, PlayRequest command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the browse command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendBrowseCommand(Guid sessionId, BrowseRequest command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the playstate command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendPlaystateCommand(Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken);
     }
 }

+ 36 - 0
MediaBrowser.Controller/Session/ISessionRemoteController.cs

@@ -21,5 +21,41 @@ namespace MediaBrowser.Controller.Session
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
         Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the message command.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendMessageCommand(SessionInfo session, MessageCommand command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the play command.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendPlayCommand(SessionInfo session, PlayRequest command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the browse command.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendBrowseCommand(SessionInfo session, BrowseRequest command, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Sends the playstate command.
+        /// </summary>
+        /// <param name="session">The session.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        Task SendPlaystateCommand(SessionInfo session, PlaystateRequest command, CancellationToken cancellationToken);
     }
 }

+ 64 - 0
MediaBrowser.Server.Implementations/Session/SessionManager.cs

@@ -465,5 +465,69 @@ namespace MediaBrowser.Server.Implementations.Session
 
             return Task.WhenAll(tasks);
         }
+
+        /// <summary>
+        /// Sends the message command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendMessageCommand(Guid sessionId, MessageCommand command, CancellationToken cancellationToken)
+        {
+            var session = GetSessionForRemoteControl(sessionId);
+
+            var tasks = GetControllers(session).Select(i => i.SendMessageCommand(session, command, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
+        /// <summary>
+        /// Sends the play command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendPlayCommand(Guid sessionId, PlayRequest command, CancellationToken cancellationToken)
+        {
+            var session = GetSessionForRemoteControl(sessionId);
+
+            var tasks = GetControllers(session).Select(i => i.SendPlayCommand(session, command, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
+        /// <summary>
+        /// Sends the browse command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendBrowseCommand(Guid sessionId, BrowseRequest command, CancellationToken cancellationToken)
+        {
+            var session = GetSessionForRemoteControl(sessionId);
+
+            var tasks = GetControllers(session).Select(i => i.SendBrowseCommand(session, command, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
+
+        /// <summary>
+        /// Sends the playstate command.
+        /// </summary>
+        /// <param name="sessionId">The session id.</param>
+        /// <param name="command">The command.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendPlaystateCommand(Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken)
+        {
+            var session = GetSessionForRemoteControl(sessionId);
+
+            var tasks = GetControllers(session).Select(i => i.SendPlaystateCommand(session, command, cancellationToken));
+
+            return Task.WhenAll(tasks);
+        }
     }
 }

+ 67 - 27
MediaBrowser.Server.Implementations/Session/WebSocketController.cs

@@ -1,5 +1,5 @@
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Logging;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Session;
 using System;
@@ -11,42 +11,82 @@ namespace MediaBrowser.Server.Implementations.Session
 {
     public class WebSocketController : ISessionRemoteController
     {
-        private readonly ILogger _logger;
-
-        public WebSocketController(ILogger logger)
-        {
-            _logger = logger;
-        }
-
         public bool Supports(SessionInfo session)
         {
             return session.WebSockets.Any(i => i.State == WebSocketState.Open);
         }
 
-        public async Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken)
+        private IWebSocketConnection GetSocket(SessionInfo session)
         {
             var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open);
 
-            if (socket != null)
-            {
-                try
-                {
-                    await socket.SendAsync(new WebSocketMessage<string>
-                    {
-                        MessageType = "SystemCommand",
-                        Data = command.ToString()
-
-                    }, cancellationToken).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    _logger.ErrorException("Error sending web socket message", ex);
-                }
-            }
-            else
+
+            if (socket == null)
             {
                 throw new InvalidOperationException("The requested session does not have an open web socket.");
             }
+
+            return socket;
+        }
+
+        public Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken)
+        {
+            var socket = GetSocket(session);
+
+            return socket.SendAsync(new WebSocketMessage<string>
+            {
+                MessageType = "SystemCommand",
+                Data = command.ToString()
+
+            }, cancellationToken);
+        }
+
+        public Task SendMessageCommand(SessionInfo session, MessageCommand command, CancellationToken cancellationToken)
+        {
+            var socket = GetSocket(session);
+
+            return socket.SendAsync(new WebSocketMessage<MessageCommand>
+            {
+                MessageType = "MessageCommand",
+                Data = command
+
+            }, cancellationToken);
+        }
+
+        public Task SendPlayCommand(SessionInfo session, PlayRequest command, CancellationToken cancellationToken)
+        {
+            var socket = GetSocket(session);
+
+            return socket.SendAsync(new WebSocketMessage<PlayRequest>
+            {
+                MessageType = "Play",
+                Data = command
+
+            }, cancellationToken);
+        }
+
+        public Task SendBrowseCommand(SessionInfo session, BrowseRequest command, CancellationToken cancellationToken)
+        {
+            var socket = GetSocket(session);
+
+            return socket.SendAsync(new WebSocketMessage<BrowseRequest>
+            {
+                MessageType = "Browse",
+                Data = command
+
+            }, cancellationToken);
+        }
+
+        public Task SendPlaystateCommand(SessionInfo session, PlaystateRequest command, CancellationToken cancellationToken)
+        {
+            var socket = GetSocket(session);
+
+            return socket.SendAsync(new WebSocketMessage<PlaystateRequest>
+            {
+                MessageType = "Playstate",
+                Data = command
+
+            }, cancellationToken);
         }
     }
 }