diff options
Diffstat (limited to 'MediaBrowser.Api')
| -rw-r--r-- | MediaBrowser.Api/ApiEntryPoint.cs | 50 | ||||
| -rw-r--r-- | MediaBrowser.Api/AuthorizationRequestFilterAttribute.cs | 181 | ||||
| -rw-r--r-- | MediaBrowser.Api/BaseApiService.cs | 147 | ||||
| -rw-r--r-- | MediaBrowser.Api/MediaBrowser.Api.csproj | 41 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/BaseStreamingService.cs | 2 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/AudioHlsService.cs | 36 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/BaseHlsService.cs | 24 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/HlsSegmentResponseFilter.cs | 53 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/HlsSegmentService.cs | 147 | ||||
| -rw-r--r-- | MediaBrowser.Api/Playback/Hls/VideoHlsService.cs | 67 | ||||
| -rw-r--r-- | MediaBrowser.Api/SessionsService.cs | 258 | ||||
| -rw-r--r-- | MediaBrowser.Api/UserLibrary/UserLibraryService.cs | 60 |
12 files changed, 527 insertions, 539 deletions
diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 52707c3c6c..8754e57a1c 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 /// <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 }); } } @@ -178,7 +180,7 @@ namespace MediaBrowser.Api if (job.ActiveRequestCount == 0) { - var timerDuration = type == TranscodingJobType.Progressive ? 1000 : 60000; + var timerDuration = type == TranscodingJobType.Progressive ? 1000 : 180000; if (job.KillTimer == null) { @@ -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> 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 + /// <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. + /// <0 Executed before global request filters + /// >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; + } +} 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 /// <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. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// </summary> - /// <value>The priority.</value> - public int Priority - { - get { return 0; } - } - } } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index c7cca812fa..4f54b52498 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -13,6 +13,8 @@ <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> <RestorePackages>true</RestorePackages> + <ProductVersion>10.0.0</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -36,29 +38,25 @@ <RunPostBuildEvent>Always</RunPostBuildEvent> </PropertyGroup> <ItemGroup> - <Reference Include="MoreLinq, Version=1.0.16006.0, Culture=neutral, PublicKeyToken=384d532d7e88985d, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Drawing" /> + <Reference Include="System.Net.Http" /> + <Reference Include="MoreLinq"> <HintPath>..\packages\morelinq.1.0.16006\lib\net35\MoreLinq.dll</HintPath> </Reference> - <Reference Include="ServiceStack.Common, Version=3.9.60.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> + <Reference Include="ServiceStack.Common"> <HintPath>..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Common.dll</HintPath> </Reference> - <Reference Include="ServiceStack.Interfaces, Version=3.9.60.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> + <Reference Include="ServiceStack.Interfaces"> <HintPath>..\packages\ServiceStack.Common.3.9.62\lib\net35\ServiceStack.Interfaces.dll</HintPath> </Reference> - <Reference Include="ServiceStack.Text, Version=3.9.59.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> + <Reference Include="ServiceStack.Text"> <HintPath>..\packages\ServiceStack.Text.3.9.62\lib\net35\ServiceStack.Text.dll</HintPath> </Reference> - <Reference Include="System" /> - <Reference Include="System.Core" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> - <Reference Include="System.Drawing" /> - <Reference Include="System.Net.Http" /> - <Reference Include="System.XML" /> + <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> <Compile Include="..\SharedVersion.cs"> @@ -70,6 +68,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 +87,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" /> @@ -128,22 +129,24 @@ </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> - <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> + <Project>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</Project> <Name>MediaBrowser.Common</Name> </ProjectReference> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj"> - <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> + <Project>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</Project> <Name>MediaBrowser.Controller</Name> </ProjectReference> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> - <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> + <Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project> <Name>MediaBrowser.Model</Name> </ProjectReference> </ItemGroup> <ItemGroup> <None Include="packages.config" /> </ItemGroup> - <ItemGroup /> + <ItemGroup> + <Folder Include="Filters\" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> <PostBuildEvent> 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 { @@ -21,27 +20,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> public class AudioHlsService : BaseHlsService @@ -64,20 +42,6 @@ namespace MediaBrowser.Api.Playback.Hls /// </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> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> public object Get(GetHlsAudioStream request) { return ProcessRequest(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); - }); - } - } - /// <summary> /// Gets the command line arguments. /// </summary> 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<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); + } + } + } +} 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 +{ + /// <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); + }); + } + } +} 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 { @@ -32,44 +31,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> public class VideoHlsService : BaseHlsService @@ -82,6 +43,7 @@ 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) { @@ -92,33 +54,6 @@ namespace MediaBrowser.Api.Playback.Hls /// </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> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> public object Get(GetHlsVideoStream request) { return ProcessRequest(request); diff --git a/MediaBrowser.Api/SessionsService.cs b/MediaBrowser.Api/SessionsService.cs index cad3c43849..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 <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)); - } - - if (!session.SupportsRemoteControl) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } + Command = request.Command, + SeekPositionTicks = request.SeekPositionTicks + }; - var socket = session.WebSockets.OrderByDescending(i => i.LastActivityDate).FirstOrDefault(i => i.State == WebSocketState.Open); + var task = _sessionManager.SendPlaystateCommand(request.Id, command, CancellationToken.None); - 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> @@ -325,102 +249,27 @@ namespace MediaBrowser.Api /// <param name="request">The request.</param> public void Post(SendSystemCommand request) { - var task = SendSystemCommand(request); + var task = _sessionManager.SendSystemCommand(request.Id, request.Command, CancellationToken.None); Task.WaitAll(task); } - private async Task SendSystemCommand(SendSystemCommand 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) - { - throw new ArgumentException(string.Format("Session {0} does not support remote control.", session.Id)); - } - - 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 = request.Command.ToString() - - }, 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."); - } - } - /// <summary> /// Posts the specified request. /// </summary> /// <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> @@ -429,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); } } } diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index ab3e2af190..abd42910f5 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -186,7 +186,7 @@ namespace MediaBrowser.Api.UserLibrary [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any)", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public DateTime? DatePlayed { get; set; } - + /// <summary> /// Gets or sets the id. /// </summary> @@ -224,6 +224,13 @@ namespace MediaBrowser.Api.UserLibrary [Api(Description = "Reports that a user has begun playing an item")] public class OnPlaybackStart : IReturnVoid { + public OnPlaybackStart() + { + // Have to default these until all clients have a chance to incorporate them + CanSeek = true; + QueueableMediaTypes = "Audio,Video,Book,Game"; + } + /// <summary> /// Gets or sets the user id. /// </summary> @@ -237,6 +244,20 @@ namespace MediaBrowser.Api.UserLibrary /// <value>The id.</value> [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] public string Id { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UpdateUserItemRating" /> is likes. + /// </summary> + /// <value><c>true</c> if likes; otherwise, <c>false</c>.</value> + [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool CanSeek { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + [ApiMember(Name = "QueueableMediaTypes", Description = "A list of media types that can be queued from this item, comma delimited. Audio,Video,Book,Game", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] + public string QueueableMediaTypes { get; set; } } /// <summary> @@ -378,6 +399,8 @@ namespace MediaBrowser.Api.UserLibrary /// <param name="libraryManager">The library manager.</param> /// <param name="userDataRepository">The user data repository.</param> /// <param name="itemRepo">The item repo.</param> + /// <param name="sessionManager">The session manager.</param> + /// <param name="dtoService">The dto service.</param> /// <exception cref="System.ArgumentNullException">jsonSerializer</exception> public UserLibraryService(IUserManager userManager, ILibraryManager libraryManager, IUserDataRepository userDataRepository, IItemRepository itemRepo, ISessionManager sessionManager, IDtoService dtoService) { @@ -640,19 +663,11 @@ namespace MediaBrowser.Api.UserLibrary private SessionInfo GetSession() { - var auth = RequestFilterAttribute.GetAuthorization(RequestContext); - - string deviceId; - string client; - string version; - - auth.TryGetValue("DeviceId", out deviceId); - auth.TryGetValue("Client", out client); - auth.TryGetValue("Version", out version); + var auth = AuthorizationRequestFilterAttribute.GetAuthorization(RequestContext); - 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> @@ -665,7 +680,17 @@ namespace MediaBrowser.Api.UserLibrary var item = _dtoService.GetItemByDtoId(request.Id, user.Id); - _sessionManager.OnPlaybackStart(item, GetSession().Id); + var queueableMediaTypes = (request.QueueableMediaTypes ?? string.Empty); + + var info = new PlaybackInfo + { + CanSeek = request.CanSeek, + Item = item, + SessionId = GetSession().Id, + QueueableMediaTypes = queueableMediaTypes.Split(',').ToList() + }; + + _sessionManager.OnPlaybackStart(info); } /// <summary> @@ -693,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); } |
