浏览代码

added full m3u8 generation

Luke Pulverenti 11 年之前
父节点
当前提交
e4f5a3f005

+ 3 - 8
MediaBrowser.Api/BaseApiService.cs

@@ -1,13 +1,13 @@
-using System.IO;
-using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Logging;
+using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Linq;
-using ServiceStack.Web;
 
 namespace MediaBrowser.Api
 {
@@ -52,11 +52,6 @@ namespace MediaBrowser.Api
             return ResultFactory.GetOptimizedResult(Request, result);
         }
 
-        protected object ToStreamResult(Stream stream, string contentType)
-        {
-            return ResultFactory.GetResult(stream, contentType);
-        }
-
         /// <summary>
         /// To the optimized result using cache.
         /// </summary>

+ 0 - 70
MediaBrowser.Api/Library/LibraryHelpers.cs

@@ -68,8 +68,6 @@ namespace MediaBrowser.Api.Library
             var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath;
             var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 
-            ValidateNewMediaPath(fileSystem, rootFolderPath, path);
-
             var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 
             var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
@@ -82,73 +80,5 @@ namespace MediaBrowser.Api.Library
 
             fileSystem.CreateShortcut(lnk, path);
         }
-
-        /// <summary>
-        /// Validates that a new media path can be added
-        /// </summary>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="currentViewRootFolderPath">The current view root folder path.</param>
-        /// <param name="mediaPath">The media path.</param>
-        /// <exception cref="System.ArgumentException">
-        /// </exception>
-        private static void ValidateNewMediaPath(IFileSystem fileSystem, string currentViewRootFolderPath, string mediaPath)
-        {
-            var pathsInCurrentVIew = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories)
-                    .Select(fileSystem.ResolveShortcut)
-                    .ToList();
-
-            // Don't allow duplicate sub-paths within the same user library, or it will result in duplicate items
-            // See comments in IsNewPathValid
-            var duplicate = pathsInCurrentVIew
-              .FirstOrDefault(p => !IsNewPathValid(fileSystem, mediaPath, p));
-
-            if (!string.IsNullOrEmpty(duplicate))
-            {
-                throw new ArgumentException(string.Format("The path cannot be added to the library because {0} already exists.", duplicate));
-            }
-            
-            // Make sure the current root folder doesn't already have a shortcut to the same path
-            duplicate = pathsInCurrentVIew
-                .FirstOrDefault(p => string.Equals(mediaPath, p, StringComparison.OrdinalIgnoreCase));
-
-            if (!string.IsNullOrEmpty(duplicate))
-            {
-                throw new ArgumentException(string.Format("The path {0} already exists in the library", mediaPath));
-            }
-        }
-
-        /// <summary>
-        /// Validates that a new path can be added based on an existing path
-        /// </summary>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="newPath">The new path.</param>
-        /// <param name="existingPath">The existing path.</param>
-        /// <returns><c>true</c> if [is new path valid] [the specified new path]; otherwise, <c>false</c>.</returns>
-        private static bool IsNewPathValid(IFileSystem fileSystem, string newPath, string existingPath)
-        {
-            // Example: D:\Movies is the existing path
-            // D:\ cannot be added
-            // Neither can D:\Movies\Kids
-            // A D:\Movies duplicate is ok here since that will be caught later
-
-            if (string.Equals(newPath, existingPath, StringComparison.OrdinalIgnoreCase))
-            {
-                return true;
-            }
-
-            // If enforceSubPathRestriction is true, validate the D:\Movies\Kids scenario
-            if (fileSystem.ContainsSubPath(existingPath, newPath))
-            {
-                return false;
-            }
-
-            // Validate the D:\ scenario
-            if (fileSystem.ContainsSubPath(newPath, existingPath))
-            {
-                return false;
-            }
-
-            return true;
-        }
     }
 }

+ 2 - 0
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1042,6 +1042,7 @@ namespace MediaBrowser.Api.Playback
                 }
 
                 itemId = recording.Id;
+                //state.RunTimeTicks = recording.RunTimeTicks;
                 state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
             }
             else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
@@ -1090,6 +1091,7 @@ namespace MediaBrowser.Api.Playback
                         : video.PlayableStreamFileNames.ToList();
                 }
 
+                state.RunTimeTicks = item.RunTimeTicks;
                 itemId = item.Id;
             }
 

+ 17 - 10
MediaBrowser.Api/Playback/Hls/BaseHlsService.cs

@@ -75,18 +75,23 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <returns>System.Object.</returns>
         protected object ProcessRequest(StreamRequest request)
         {
-            var state = GetState(request, CancellationToken.None).Result;
-
-            return ProcessRequestAsync(state).Result;
+            return ProcessRequestAsync(request).Result;
         }
 
         /// <summary>
         /// Processes the request async.
         /// </summary>
-        /// <param name="state">The state.</param>
+        /// <param name="request">The request.</param>
         /// <returns>Task{System.Object}.</returns>
-        public async Task<object> ProcessRequestAsync(StreamState state)
+        /// <exception cref="ArgumentException">
+        /// A video bitrate is required
+        /// or
+        /// An audio bitrate is required
+        /// </exception>
+        private async Task<object> ProcessRequestAsync(StreamRequest request)
         {
+            var state = GetState(request, CancellationToken.None).Result;
+
             if (!state.VideoRequest.VideoBitRate.HasValue && (!state.VideoRequest.VideoCodec.HasValue || state.VideoRequest.VideoCodec.Value != VideoCodecs.Copy))
             {
                 throw new ArgumentException("A video bitrate is required");
@@ -155,7 +160,7 @@ namespace MediaBrowser.Api.Playback.Hls
         /// <param name="state">The state.</param>
         /// <param name="audioBitrate">The audio bitrate.</param>
         /// <param name="videoBitrate">The video bitrate.</param>
-        private void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate)
+        protected void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate)
         {
             var audioBitrateParam = GetAudioBitrateParam(state);
             var videoBitrateParam = GetVideoBitrateParam(state);
@@ -269,7 +274,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var threads = GetNumberOfThreads(false);
 
-            var args = string.Format("{0}{1} {2} {3} -i {4}{5} -map_metadata -1 -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{10}\"",
+            var args = string.Format("{0}{1} {2} {3} -i {4}{5} -map_metadata -1 -threads {6} {7} {8} -sc_threshold 0 {9} -hls_time {10} -start_number 0 -hls_list_size 1440 \"{11}\"",
                 itsOffset,
                 probeSize,
                 GetUserAgentParam(state.MediaPath),
@@ -280,6 +285,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 GetMapArgs(state),
                 GetVideoArguments(state, performSubtitleConversions),
                 GetAudioArguments(state),
+                state.SegmentLength.ToString(UsCulture),
                 outputPath
                 ).Trim();
 
@@ -291,10 +297,11 @@ namespace MediaBrowser.Api.Playback.Hls
 
                     var bitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? 64000;
 
-                    var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {2} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{1}\"",
+                    var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {1} -hls_time {2} -start_number 0 -hls_list_size 1440 \"{3}\"",
                         threads,
-                        lowBitratePath,
-                        bitrate / 2);
+                        bitrate / 2,
+                        state.SegmentLength.ToString(UsCulture),
+                        lowBitratePath);
 
                     args += " " + lowBitrateParams;
                 }

+ 150 - 0
MediaBrowser.Api/Playback/Hls/VideoHlsService.cs

@@ -5,9 +5,14 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaInfo;
 using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using ServiceStack;
 using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
 
 namespace MediaBrowser.Api.Playback.Hls
 {
@@ -28,6 +33,29 @@ namespace MediaBrowser.Api.Playback.Hls
         public int TimeStampOffsetMs { get; set; }
     }
 
+    [Route("/Videos/{Id}/master.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetMasterHlsVideoStream : VideoStreamRequest
+    {
+        [ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
+        public int? BaselineStreamAudioBitRate { get; set; }
+
+        [ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
+        public bool AppendBaselineStream { get; set; }
+    }
+
+    [Route("/Videos/{Id}/main.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetMainHlsVideoStream : VideoStreamRequest
+    {
+    }
+
+    [Route("/Videos/{Id}/baseline.m3u8", "GET")]
+    [Api(Description = "Gets a video stream using HTTP live streaming.")]
+    public class GetBaselineHlsVideoStream : VideoStreamRequest
+    {
+    }
+
     /// <summary>
     /// Class VideoHlsService
     /// </summary>
@@ -38,6 +66,128 @@ namespace MediaBrowser.Api.Playback.Hls
         {
         }
 
+        public object Get(GetMasterHlsVideoStream request)
+        {
+            var result = GetAsync(request).Result;
+
+            return result;
+        }
+
+        public object Get(GetMainHlsVideoStream request)
+        {
+            var result = GetPlaylistAsync(request, "main").Result;
+
+            return result;
+        }
+
+        public object Get(GetBaselineHlsVideoStream request)
+        {
+            var result = GetPlaylistAsync(request, "baseline").Result;
+
+            return result;
+        }
+
+        private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
+        {
+            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
+
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+            builder.AppendLine("#EXT-X-VERSION:3");
+            builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
+            builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
+
+            var queryStringIndex = Request.RawUrl.IndexOf('?');
+            var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
+            
+            var seconds = TimeSpan.FromTicks(state.RunTimeTicks ?? 0).TotalSeconds;
+
+            var index = 0;
+
+            while (seconds > 0)
+            {
+                var length = seconds >= state.SegmentLength ? state.SegmentLength : seconds;
+
+                builder.AppendLine("#EXTINF:" + length.ToString(UsCulture));
+
+                builder.AppendLine(string.Format("hls/{0}/{1}.ts{2}" ,
+
+                    name,
+                    index.ToString(UsCulture),
+                    queryString));
+
+                seconds -= state.SegmentLength;
+                index++;
+            }
+
+            builder.AppendLine("#EXT-X-ENDLIST");
+
+            var playlistText = builder.ToString();
+
+            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+        }
+
+        private async Task<object> GetAsync(GetMasterHlsVideoStream request)
+        {
+            var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
+
+            if (!state.VideoRequest.VideoBitRate.HasValue && (!state.VideoRequest.VideoCodec.HasValue || state.VideoRequest.VideoCodec.Value != VideoCodecs.Copy))
+            {
+                throw new ArgumentException("A video bitrate is required");
+            }
+            if (!state.Request.AudioBitRate.HasValue && (!state.Request.AudioCodec.HasValue || state.Request.AudioCodec.Value != AudioCodecs.Copy))
+            {
+                throw new ArgumentException("An audio bitrate is required");
+            }
+
+            int audioBitrate;
+            int videoBitrate;
+            GetPlaylistBitrates(state, out audioBitrate, out videoBitrate);
+
+            var appendBaselineStream = false;
+            var baselineStreamBitrate = 64000;
+
+            var hlsVideoRequest = state.VideoRequest as GetMasterHlsVideoStream;
+            if (hlsVideoRequest != null)
+            {
+                appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
+                baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate;
+            }
+
+            var playlistText = GetMasterPlaylistFileText(videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
+
+            return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
+        }
+
+        private string GetMasterPlaylistFileText(int bitrate, bool includeBaselineStream, int baselineStreamBitrate)
+        {
+            var builder = new StringBuilder();
+
+            builder.AppendLine("#EXTM3U");
+
+            // Pad a little to satisfy the apple hls validator
+            var paddedBitrate = Convert.ToInt32(bitrate * 1.05);
+
+            var queryStringIndex = Request.RawUrl.IndexOf('?');
+            var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
+
+            // Main stream
+            builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
+            var playlistUrl = "main.m3u8" + queryString;
+            builder.AppendLine(playlistUrl);
+
+            // Low bitrate stream
+            if (includeBaselineStream)
+            {
+                builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture));
+                playlistUrl = "baseline.m3u8" + queryString;
+                builder.AppendLine(playlistUrl);
+            }
+
+            return builder.ToString();
+        }
+
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 6 - 2
MediaBrowser.Api/Playback/StreamState.cs

@@ -1,8 +1,8 @@
-using System.Threading;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using System.Collections.Generic;
 using System.IO;
+using System.Threading;
 
 namespace MediaBrowser.Api.Playback
 {
@@ -54,5 +54,9 @@ namespace MediaBrowser.Api.Playback
         public CancellationTokenSource StandardInputCancellationTokenSource { get; set; }
 
         public string LiveTvStreamId { get; set; }
+
+        public int SegmentLength = 10;
+
+        public long? RunTimeTicks;
     }
 }

+ 8 - 0
MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs

@@ -2,6 +2,7 @@
 using ServiceStack.Web;
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Threading.Tasks;
 
@@ -13,6 +14,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer
     public class StreamWriter : IStreamWriter, IHasOptions
     {
         private ILogger Logger { get; set; }
+
+        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
         
         /// <summary>
         /// Gets or sets the source stream.
@@ -50,6 +53,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer
             Logger = logger;
 
             Options["Content-Type"] = contentType;
+
+            if (source.CanSeek)
+            {
+                Options["Content-Length"] = source.Length.ToString(UsCulture);
+            }
         }
 
         /// <summary>

+ 0 - 9
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -157,15 +157,6 @@
     <Content Include="dashboard-ui\css\images\media\tvflyout.png">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
-    <Content Include="dashboard-ui\css\images\userdata\starhalf.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\userdata\staroff.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
-    <Content Include="dashboard-ui\css\images\userdata\staron.png">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </Content>
     <Content Include="dashboard-ui\css\livetv.css">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>