added live channel playback
This commit is contained in:
parent
a5be2523c5
commit
ede84702d1
|
@ -349,26 +349,42 @@ namespace MediaBrowser.Api
|
||||||
// Also don't cache video
|
// Also don't cache video
|
||||||
if (!hasExitedSuccessfully || job.StartTimeTicks.HasValue || job.IsVideo)
|
if (!hasExitedSuccessfully || job.StartTimeTicks.HasValue || job.IsVideo)
|
||||||
{
|
{
|
||||||
Logger.Info("Deleting partial stream file(s) {0}", job.Path);
|
DeletePartialStreamFiles(job.Path, job.Type, 0, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(1500).ConfigureAwait(false);
|
private async void DeletePartialStreamFiles(string path, TranscodingJobType jobType, int retryCount, int delayMs)
|
||||||
|
{
|
||||||
|
if (retryCount >= 5)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
Logger.Info("Deleting partial stream file(s) {0}", path);
|
||||||
|
|
||||||
|
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (jobType == TranscodingJobType.Progressive)
|
||||||
{
|
{
|
||||||
if (job.Type == TranscodingJobType.Progressive)
|
DeleteProgressivePartialStreamFiles(path);
|
||||||
{
|
|
||||||
DeleteProgressivePartialStreamFiles(job.Path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DeleteHlsPartialStreamFiles(job.Path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (IOException ex)
|
else
|
||||||
{
|
{
|
||||||
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, job.Path);
|
DeleteHlsPartialStreamFiles(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
|
||||||
|
|
||||||
|
DeletePartialStreamFiles(path, jobType, retryCount + 1, 500);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
<Compile Include="NotificationsService.cs" />
|
<Compile Include="NotificationsService.cs" />
|
||||||
<Compile Include="PackageReviewService.cs" />
|
<Compile Include="PackageReviewService.cs" />
|
||||||
<Compile Include="PackageService.cs" />
|
<Compile Include="PackageService.cs" />
|
||||||
|
<Compile Include="Playback\EndlessStreamCopy.cs" />
|
||||||
<Compile Include="Playback\Hls\AudioHlsService.cs" />
|
<Compile Include="Playback\Hls\AudioHlsService.cs" />
|
||||||
<Compile Include="Playback\Hls\BaseHlsService.cs" />
|
<Compile Include="Playback\Hls\BaseHlsService.cs" />
|
||||||
<Compile Include="Playback\Hls\HlsSegmentResponseFilter.cs" />
|
<Compile Include="Playback\Hls\HlsSegmentResponseFilter.cs" />
|
||||||
|
|
|
@ -13,6 +13,7 @@ using MediaBrowser.Model.Drawing;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.LiveTv;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
@ -539,8 +540,8 @@ namespace MediaBrowser.Api.Playback
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
protected string GetProbeSizeArgument(string mediaPath, bool isVideo, VideoType? videoType, IsoType? isoType)
|
protected string GetProbeSizeArgument(string mediaPath, bool isVideo, VideoType? videoType, IsoType? isoType)
|
||||||
{
|
{
|
||||||
var type = !isVideo ? MediaEncoderHelpers.GetInputType(mediaPath, null, null) :
|
var type = !isVideo ? MediaEncoderHelpers.GetInputType(null, null) :
|
||||||
MediaEncoderHelpers.GetInputType(mediaPath, videoType, isoType);
|
MediaEncoderHelpers.GetInputType(videoType, isoType);
|
||||||
|
|
||||||
return MediaEncoder.GetProbeSizeArgument(type);
|
return MediaEncoder.GetProbeSizeArgument(type);
|
||||||
}
|
}
|
||||||
|
@ -654,6 +655,11 @@ namespace MediaBrowser.Api.Playback
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
protected string GetInputArgument(StreamState state)
|
protected string GetInputArgument(StreamState state)
|
||||||
{
|
{
|
||||||
|
if (state.SendInputOverStandardInput)
|
||||||
|
{
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
var type = InputType.AudioFile;
|
var type = InputType.AudioFile;
|
||||||
|
|
||||||
var inputPath = new[] { state.MediaPath };
|
var inputPath = new[] { state.MediaPath };
|
||||||
|
@ -705,7 +711,9 @@ namespace MediaBrowser.Api.Playback
|
||||||
Arguments = GetCommandLineArguments(outputPath, state, true),
|
Arguments = GetCommandLineArguments(outputPath, state, true),
|
||||||
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
ErrorDialog = false
|
ErrorDialog = false,
|
||||||
|
|
||||||
|
RedirectStandardInput = state.SendInputOverStandardInput
|
||||||
},
|
},
|
||||||
|
|
||||||
EnableRaisingEvents = true
|
EnableRaisingEvents = true
|
||||||
|
@ -738,6 +746,11 @@ namespace MediaBrowser.Api.Playback
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.SendInputOverStandardInput)
|
||||||
|
{
|
||||||
|
StreamToStandardInput(process, state);
|
||||||
|
}
|
||||||
|
|
||||||
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
|
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
|
||||||
process.BeginOutputReadLine();
|
process.BeginOutputReadLine();
|
||||||
|
|
||||||
|
@ -763,6 +776,34 @@ namespace MediaBrowser.Api.Playback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void StreamToStandardInput(Process process, StreamState state)
|
||||||
|
{
|
||||||
|
state.StandardInputCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await StreamToStandardInputInternal(process, state).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
Logger.Debug("Stream to standard input closed normally.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.ErrorException("Error writing to standard input", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StreamToStandardInputInternal(Process process, StreamState state)
|
||||||
|
{
|
||||||
|
state.StandardInputCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
using (var fileStream = FileSystem.GetFileStream(state.MediaPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||||
|
{
|
||||||
|
await new EndlessStreamCopy().CopyStream(fileStream, process.StandardInput.BaseStream, state.StandardInputCancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected int? GetVideoBitrateParam(StreamState state)
|
protected int? GetVideoBitrateParam(StreamState state)
|
||||||
{
|
{
|
||||||
return state.VideoRequest.VideoBitRate;
|
return state.VideoRequest.VideoBitRate;
|
||||||
|
@ -831,6 +872,11 @@ namespace MediaBrowser.Api.Playback
|
||||||
state.IsoMount = null;
|
state.IsoMount = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.StandardInputCancellationTokenSource != null)
|
||||||
|
{
|
||||||
|
state.StandardInputCancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
var outputFilePath = GetOutputFilePath(state);
|
var outputFilePath = GetOutputFilePath(state);
|
||||||
|
|
||||||
state.LogFileStream.Dispose();
|
state.LogFileStream.Dispose();
|
||||||
|
@ -903,10 +949,11 @@ namespace MediaBrowser.Api.Playback
|
||||||
}
|
}
|
||||||
|
|
||||||
itemId = recording.Id;
|
itemId = recording.Id;
|
||||||
|
state.SendInputOverStandardInput = recording.RecordingInfo.Status == RecordingStatus.InProgress;
|
||||||
}
|
}
|
||||||
else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(request.Type, "Channel", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var channel = LiveTvManager.GetInternalChannel(request.Id);
|
var channel = LiveTvManager.GetInternalChannel(request.Id);
|
||||||
|
|
||||||
state.VideoType = VideoType.VideoFile;
|
state.VideoType = VideoType.VideoFile;
|
||||||
state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
||||||
|
@ -926,6 +973,7 @@ namespace MediaBrowser.Api.Playback
|
||||||
}
|
}
|
||||||
|
|
||||||
itemId = channel.Id;
|
itemId = channel.Id;
|
||||||
|
state.SendInputOverStandardInput = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
32
MediaBrowser.Api/Playback/EndlessStreamCopy.cs
Normal file
32
MediaBrowser.Api/Playback/EndlessStreamCopy.cs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Api.Playback
|
||||||
|
{
|
||||||
|
public class EndlessStreamCopy
|
||||||
|
{
|
||||||
|
public async Task CopyStream(Stream source, Stream target, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
long position = 0;
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await source.CopyToAsync(target, 81920, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var fsPosition = source.Position;
|
||||||
|
|
||||||
|
var bytesRead = fsPosition - position;
|
||||||
|
|
||||||
|
//Logger.Debug("Streamed {0} bytes from file {1}", bytesRead, path);
|
||||||
|
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
position = fsPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using MediaBrowser.Model.Entities;
|
using System.Threading;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -47,5 +48,9 @@ namespace MediaBrowser.Api.Playback
|
||||||
public List<string> PlayableStreamFileNames { get; set; }
|
public List<string> PlayableStreamFileNames { get; set; }
|
||||||
|
|
||||||
public bool HasMediaStreams { get; set; }
|
public bool HasMediaStreams { get; set; }
|
||||||
|
|
||||||
|
public bool SendInputOverStandardInput { get; set; }
|
||||||
|
|
||||||
|
public CancellationTokenSource StandardInputCancellationTokenSource { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,11 +81,10 @@ namespace MediaBrowser.Controller.MediaInfo
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type of the input.
|
/// Gets the type of the input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <param name="videoType">Type of the video.</param>
|
/// <param name="videoType">Type of the video.</param>
|
||||||
/// <param name="isoType">Type of the iso.</param>
|
/// <param name="isoType">Type of the iso.</param>
|
||||||
/// <returns>InputType.</returns>
|
/// <returns>InputType.</returns>
|
||||||
public static InputType GetInputType(string path, VideoType? videoType, IsoType? isoType)
|
public static InputType GetInputType(VideoType? videoType, IsoType? isoType)
|
||||||
{
|
{
|
||||||
var type = InputType.AudioFile;
|
var type = InputType.AudioFile;
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
|
// Avoid implicitly captured closure
|
||||||
|
var currentUser = user;
|
||||||
|
|
||||||
channels = channels
|
channels = channels
|
||||||
.Where(i => i.IsParentalAllowed(user))
|
.Where(i => i.IsParentalAllowed(currentUser))
|
||||||
.OrderBy(i =>
|
.OrderBy(i =>
|
||||||
{
|
{
|
||||||
double number = 0;
|
double number = 0;
|
||||||
|
@ -419,7 +422,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||||
var allChannelsList = allChannels.ToList();
|
var allChannelsList = allChannels.ToList();
|
||||||
|
|
||||||
var list = new List<LiveTvChannel>();
|
var list = new List<LiveTvChannel>();
|
||||||
var programs = new List<LiveTvProgram>();
|
|
||||||
|
|
||||||
var numComplete = 0;
|
var numComplete = 0;
|
||||||
|
|
||||||
|
@ -429,13 +431,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||||
{
|
{
|
||||||
var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, cancellationToken).ConfigureAwait(false);
|
var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var channelPrograms = await service.GetProgramsAsync(channelInfo.Item2.Id, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var programTasks = channelPrograms.Select(program => GetProgram(program, item.ChannelInfo.ChannelType, service.Name, cancellationToken));
|
|
||||||
var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false);
|
|
||||||
|
|
||||||
programs.AddRange(programEntities);
|
|
||||||
|
|
||||||
list.Add(item);
|
list.Add(item);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
@ -451,11 +446,45 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||||
double percent = numComplete;
|
double percent = numComplete;
|
||||||
percent /= allChannelsList.Count;
|
percent /= allChannelsList.Count;
|
||||||
|
|
||||||
|
progress.Report(5 * percent + 10);
|
||||||
|
}
|
||||||
|
_channels = list.ToDictionary(i => i.Id);
|
||||||
|
progress.Report(15);
|
||||||
|
|
||||||
|
numComplete = 0;
|
||||||
|
var programs = new List<LiveTvProgram>();
|
||||||
|
|
||||||
|
foreach (var item in list)
|
||||||
|
{
|
||||||
|
// Avoid implicitly captured closure
|
||||||
|
var currentChannel = item;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var channelPrograms = await service.GetProgramsAsync(currentChannel.ChannelInfo.Id, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var programTasks = channelPrograms.Select(program => GetProgram(program, currentChannel.ChannelInfo.ChannelType, service.Name, cancellationToken));
|
||||||
|
var programEntities = await Task.WhenAll(programTasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
programs.AddRange(programEntities);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.ErrorException("Error getting programs for channel {0}", ex, currentChannel.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
numComplete++;
|
||||||
|
double percent = numComplete;
|
||||||
|
percent /= allChannelsList.Count;
|
||||||
|
|
||||||
progress.Report(90 * percent + 10);
|
progress.Report(90 * percent + 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
_programs = programs.ToDictionary(i => i.Id);
|
_programs = programs.ToDictionary(i => i.Id);
|
||||||
_channels = list.ToDictionary(i => i.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken)
|
private async Task<IEnumerable<Tuple<string, ChannelInfo>>> GetChannels(ILiveTvService service, CancellationToken cancellationToken)
|
||||||
|
|
|
@ -127,6 +127,9 @@
|
||||||
<Content Include="dashboard-ui\css\images\icons\subtitles.png">
|
<Content Include="dashboard-ui\css\images\icons\subtitles.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="dashboard-ui\css\images\icons\tv.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="dashboard-ui\css\images\icons\volumedown.png">
|
<Content Include="dashboard-ui\css\images\icons\volumedown.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -136,6 +139,9 @@
|
||||||
<Content Include="dashboard-ui\css\images\items\detail\tv.png">
|
<Content Include="dashboard-ui\css\images\items\detail\tv.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="dashboard-ui\css\images\media\tvflyout.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
<Content Include="dashboard-ui\css\livetv.css">
|
<Content Include="dashboard-ui\css\livetv.css">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user