From f176307e591dc8cd4fd1dabe1ebc5e22fba26d51 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 24 Sep 2013 15:54:42 -0400 Subject: [PATCH] support multiple remote control outputs --- MediaBrowser.Api/ApiEntryPoint.cs | 48 +++- .../AuthorizationRequestFilterAttribute.cs | 181 +++++++++++++++ MediaBrowser.Api/BaseApiService.cs | 147 +----------- MediaBrowser.Api/MediaBrowser.Api.csproj | 7 +- .../Playback/BaseStreamingService.cs | 2 +- .../Playback/Hls/AudioHlsService.cs | 36 --- .../Playback/Hls/BaseHlsService.cs | 24 -- .../Playback/Hls/HlsSegmentResponseFilter.cs | 53 +++++ .../Playback/Hls/HlsSegmentService.cs | 147 ++++++++++++ .../Playback/Hls/VideoHlsService.cs | 67 +----- MediaBrowser.Api/SessionsService.cs | 218 +++--------------- .../UserLibrary/UserLibraryService.cs | 23 +- .../Session/ISessionManager.cs | 36 +++ .../Session/ISessionRemoteController.cs | 36 +++ .../Session/SessionManager.cs | 64 +++++ .../Session/WebSocketController.cs | 94 +++++--- 16 files changed, 677 insertions(+), 506 deletions(-) create mode 100644 MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs create mode 100644 MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs create mode 100644 MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 52707c3c6c..273d9a7a90 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/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 /// The process. /// if set to true [is video]. /// The start time ticks. - public void OnTranscodeBeginning(string path, TranscodingJobType type, Process process, bool isVideo, long? startTimeTicks) + /// The source path. + 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]. /// /// The state. - private async void OnTranscodeKillTimerStopped(object state) + private void OnTranscodeKillTimerStopped(object state) { var job = (TranscodingJob)state; + KillTranscodingJob(job); + } + + /// + /// Kills the single transcoding job. + /// + /// The source path. + internal void KillSingleTranscodingJob(string sourcePath) + { + if (string.IsNullOrEmpty(sourcePath)) + { + throw new ArgumentNullException("sourcePath"); + } + + var jobs = new List(); + + 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()); + } + } + + /// + /// Kills the transcoding job. + /// + /// The job. + 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; } } /// diff --git a/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs b/MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs new file mode 100644 index 0000000000..d225bdd994 --- /dev/null +++ b/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 + /// + /// Gets or sets the user manager. + /// + /// The user manager. + public IUserManager UserManager { get; set; } + + public ISessionManager SessionManager { get; set; } + + /// + /// Gets or sets the logger. + /// + /// The logger. + public ILogger Logger { get; set; } + + /// + /// The request filter is executed before the service. + /// + /// The http request wrapper + /// The http response wrapper + /// The request DTO + 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); + } + } + } + + /// + /// Gets the auth. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + public static Dictionary GetAuthorization(IHttpRequest httpReq) + { + var auth = httpReq.Headers[HttpHeaders.Authorization]; + + return GetAuthorization(auth); + } + + /// + /// Gets the authorization. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + 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 + }; + } + + /// + /// Gets the authorization. + /// + /// The authorization header. + /// Dictionary{System.StringSystem.String}. + private static Dictionary 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(StringComparer.OrdinalIgnoreCase); + + foreach (var item in parts) + { + var param = item.Trim().Split(new[] { '=' }, 2); + result.Add(param[0], param[1].Trim(new[] { '"' })); + } + + return result; + } + + /// + /// A new shallow copy of this filter is used on every request. + /// + /// IHasRequestFilter. + public IHasRequestFilter Copy() + { + return this; + } + + /// + /// Order in which Request Filters are executed. + /// <0 Executed before global request filters + /// >0 Executed after global request filters + /// + /// The priority. + public int Priority + { + get { return 0; } + } + } + + public class AuthorizationInfo + { + public string UserId; + public string DeviceId; + public string Device; + public string Client; + public string Version; + } +} diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index b3f5027e0a..069bc0fe1b 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/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 /// /// Class BaseApiService /// - [RequestFilter] + [AuthorizationRequestFilter] public class BaseApiService : IHasResultFactory, IRestfulService { /// @@ -308,147 +306,4 @@ namespace MediaBrowser.Api return item; } } - - /// - /// Class RequestFilterAttribute - /// - public class RequestFilterAttribute : Attribute, IHasRequestFilter - { - //This property will be resolved by the IoC container - /// - /// Gets or sets the user manager. - /// - /// The user manager. - public IUserManager UserManager { get; set; } - - public ISessionManager SessionManager { get; set; } - - /// - /// Gets or sets the logger. - /// - /// The logger. - public ILogger Logger { get; set; } - - /// - /// The request filter is executed before the service. - /// - /// The http request wrapper - /// The http response wrapper - /// The request DTO - 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); - } - } - } - - /// - /// Gets the auth. - /// - /// The HTTP req. - /// Dictionary{System.StringSystem.String}. - public static Dictionary GetAuthorization(IHttpRequest httpReq) - { - var auth = httpReq.Headers[HttpHeaders.Authorization]; - - return GetAuthorization(auth); - } - - /// - /// Gets the authorization. - /// - /// The HTTP req. - /// Dictionary{System.StringSystem.String}. - public static Dictionary GetAuthorization(IRequestContext httpReq) - { - var auth = httpReq.GetHeader("Authorization"); - - return GetAuthorization(auth); - } - - /// - /// Gets the authorization. - /// - /// The authorization header. - /// Dictionary{System.StringSystem.String}. - private static Dictionary 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(StringComparer.OrdinalIgnoreCase); - - foreach (var item in parts) - { - var param = item.Trim().Split(new[] { '=' }, 2); - result.Add(param[0], param[1].Trim(new[] { '"' })); - } - - return result; - } - - /// - /// A new shallow copy of this filter is used on every request. - /// - /// IHasRequestFilter. - public IHasRequestFilter Copy() - { - return this; - } - - /// - /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// - /// The priority. - public int Priority - { - get { return 0; } - } - } } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index c7cca812fa..995b5cdf11 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -70,6 +70,7 @@ + @@ -88,6 +89,8 @@ + + @@ -143,7 +146,9 @@ - + + + diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index e31a112d5a..c782c243d0 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/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); diff --git a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs b/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs index d7ee73a9e4..6e36ba0ad8 100644 --- a/MediaBrowser.Api/Playback/Hls/AudioHlsService.cs +++ b/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 } - /// - /// Class GetHlsAudioSegment - /// - [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 - { - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - /// - /// Gets or sets the segment id. - /// - /// The segment id. - public string SegmentId { get; set; } - } - /// /// Class AudioHlsService /// @@ -59,20 +37,6 @@ namespace MediaBrowser.Api.Playback.Hls { } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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); - } - /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index e680546b03..05441bba74 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/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); - }); - } - } - /// /// Gets the command line arguments. /// diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs new file mode 100644 index 0000000000..44996c99f5 --- /dev/null +++ b/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(1); + var playlistId = pathInfo.GetArgumentValue(3); + + OnEndRequest(itemId, playlistId); + } + + public IHasResponseFilter Copy() + { + return this; + } + + public int Priority + { + get { return -1; } + } + + /// + /// Called when [end request]. + /// + /// The item id. + /// The playlist id. + 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); + } + } + } +} diff --git a/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs b/MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs new file mode 100644 index 0000000000..f1fa86f780 --- /dev/null +++ b/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 +{ + /// + /// Class GetHlsAudioSegment + /// + [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 + { + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + + /// + /// Gets or sets the segment id. + /// + /// The segment id. + public string SegmentId { get; set; } + } + + /// + /// Class GetHlsVideoSegment + /// + [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsVideoSegment + { + /// + /// Gets or sets the id. + /// + /// The id. + public string Id { get; set; } + + public string PlaylistId { get; set; } + + /// + /// Gets or sets the segment id. + /// + /// The segment id. + public string SegmentId { get; set; } + } + + /// + /// Class GetHlsVideoSegment + /// + [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] + [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] + public class GetHlsPlaylist + { + /// + /// Gets or sets the id. + /// + /// The id. + 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); + } + + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. + 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); + } + + /// + /// Gets the specified request. + /// + /// The request. + /// System.Object. + 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); + } + + /// + /// Called when [begin request]. + /// + /// The playlist id. + 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); + }); + } + } +} diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 901b276887..4694b68a1d 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/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 } } - /// - /// Class GetHlsVideoSegment - /// - [Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")] - [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsVideoSegment - { - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - public string PlaylistId { get; set; } - - /// - /// Gets or sets the segment id. - /// - /// The segment id. - public string SegmentId { get; set; } - } - - /// - /// Class GetHlsVideoSegment - /// - [Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")] - [Api(Description = "Gets an Http live streaming segment file. Internal use only.")] - public class GetHlsPlaylist - { - /// - /// Gets or sets the id. - /// - /// The id. - public string Id { get; set; } - - public string PlaylistId { get; set; } - } - /// /// Class VideoHlsService /// @@ -82,38 +43,12 @@ namespace MediaBrowser.Api.Playback.Hls /// The library manager. /// The iso manager. /// The media encoder. + /// The dto service. public VideoHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService) { } - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - 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); - } - /// /// Gets the specified request. /// diff --git a/MediaBrowser.Api/SessionsService.cs b/MediaBrowser.Api/SessionsService.cs index b93b5326e6..5888d9fba3 100644 --- a/MediaBrowser.Api/SessionsService.cs +++ b/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 class. /// /// The session manager. + /// The dto service. 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 - { - 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); } /// @@ -268,55 +230,17 @@ namespace MediaBrowser.Api /// The request. public void Post(BrowseTo request) { - var task = BrowseTo(request); - - Task.WaitAll(task); - } - - /// - /// Browses to. - /// - /// The request. - /// Task. - /// - /// - /// The requested session does not have an open web socket. - 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 - { - 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); } /// @@ -336,53 +260,16 @@ namespace MediaBrowser.Api /// The request. 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 - { - 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); } /// @@ -391,62 +278,17 @@ namespace MediaBrowser.Api /// The request. public void Post(Play request) { - var task = Play(request); - - Task.WaitAll(task); - } - - /// - /// Plays the specified request. - /// - /// The request. - /// Task. - /// - /// - /// The requested session does not have an open web socket. - 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 - { - 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); } } } diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 9085a3ecfd..abd42910f5 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/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)); } /// @@ -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); } diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index f8f7ded2b7..0932ee52d5 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -89,5 +89,41 @@ namespace MediaBrowser.Controller.Session /// The cancellation token. /// Task. Task SendSystemCommand(Guid sessionId, SystemCommand command, CancellationToken cancellationToken); + + /// + /// Sends the message command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendMessageCommand(Guid sessionId, MessageCommand command, CancellationToken cancellationToken); + + /// + /// Sends the play command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlayCommand(Guid sessionId, PlayRequest command, CancellationToken cancellationToken); + + /// + /// Sends the browse command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendBrowseCommand(Guid sessionId, BrowseRequest command, CancellationToken cancellationToken); + + /// + /// Sends the playstate command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlaystateCommand(Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/MediaBrowser.Controller/Session/ISessionRemoteController.cs b/MediaBrowser.Controller/Session/ISessionRemoteController.cs index 1f6faeb9c2..9ba5c983d1 100644 --- a/MediaBrowser.Controller/Session/ISessionRemoteController.cs +++ b/MediaBrowser.Controller/Session/ISessionRemoteController.cs @@ -21,5 +21,41 @@ namespace MediaBrowser.Controller.Session /// The cancellation token. /// Task. Task SendSystemCommand(SessionInfo session, SystemCommand command, CancellationToken cancellationToken); + + /// + /// Sends the message command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendMessageCommand(SessionInfo session, MessageCommand command, CancellationToken cancellationToken); + + /// + /// Sends the play command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlayCommand(SessionInfo session, PlayRequest command, CancellationToken cancellationToken); + + /// + /// Sends the browse command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendBrowseCommand(SessionInfo session, BrowseRequest command, CancellationToken cancellationToken); + + /// + /// Sends the playstate command. + /// + /// The session. + /// The command. + /// The cancellation token. + /// Task. + Task SendPlaystateCommand(SessionInfo session, PlaystateRequest command, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 5b0d957ae1..79dfbc8a52 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -465,5 +465,69 @@ namespace MediaBrowser.Server.Implementations.Session return Task.WhenAll(tasks); } + + /// + /// Sends the message command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + 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); + } + + /// + /// Sends the play command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + 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); + } + + /// + /// Sends the browse command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + 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); + } + + /// + /// Sends the playstate command. + /// + /// The session id. + /// The command. + /// The cancellation token. + /// Task. + 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); + } } } diff --git a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs b/MediaBrowser.Server.Implementations/Session/WebSocketController.cs index daa4c7d819..6915cfc64f 100644 --- a/MediaBrowser.Server.Implementations/Session/WebSocketController.cs +++ b/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 - { - 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 + { + MessageType = "SystemCommand", + Data = command.ToString() + + }, cancellationToken); + } + + public Task SendMessageCommand(SessionInfo session, MessageCommand command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "MessageCommand", + Data = command + + }, cancellationToken); + } + + public Task SendPlayCommand(SessionInfo session, PlayRequest command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "Play", + Data = command + + }, cancellationToken); + } + + public Task SendBrowseCommand(SessionInfo session, BrowseRequest command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "Browse", + Data = command + + }, cancellationToken); + } + + public Task SendPlaystateCommand(SessionInfo session, PlaystateRequest command, CancellationToken cancellationToken) + { + var socket = GetSocket(session); + + return socket.SendAsync(new WebSocketMessage + { + MessageType = "Playstate", + Data = command + + }, cancellationToken); } } }