rework live stream handling

This commit is contained in:
Luke Pulverenti 2016-09-25 14:39:13 -04:00
parent 48d7f686eb
commit d596053ec7
24 changed files with 523 additions and 310 deletions

View File

@ -8,6 +8,7 @@ using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
@ -44,7 +45,13 @@ namespace MediaBrowser.Api
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
public readonly SemaphoreSlim TranscodingStartLock = new SemaphoreSlim(1, 1); /// <summary>
/// The active transcoding jobs
/// </summary>
private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks =
new Dictionary<string, SemaphoreSlim>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApiEntryPoint" /> class. /// Initializes a new instance of the <see cref="ApiEntryPoint" /> class.
@ -67,6 +74,21 @@ namespace MediaBrowser.Api
_sessionManager.PlaybackStart += _sessionManager_PlaybackStart; _sessionManager.PlaybackStart += _sessionManager_PlaybackStart;
} }
public SemaphoreSlim GetTranscodingLock(string outputPath)
{
lock (_transcodingLocks)
{
SemaphoreSlim result;
if (!_transcodingLocks.TryGetValue(outputPath, out result))
{
result = new SemaphoreSlim(1, 1);
_transcodingLocks[outputPath] = result;
}
return result;
}
}
private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e) private void _sessionManager_PlaybackStart(object sender, PlaybackProgressEventArgs e)
{ {
if (!string.IsNullOrWhiteSpace(e.PlaySessionId)) if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
@ -148,11 +170,6 @@ namespace MediaBrowser.Api
} }
} }
/// <summary>
/// The active transcoding jobs
/// </summary>
private readonly List<TranscodingJob> _activeTranscodingJobs = new List<TranscodingJob>();
/// <summary> /// <summary>
/// Called when [transcode beginning]. /// Called when [transcode beginning].
/// </summary> /// </summary>
@ -258,6 +275,11 @@ namespace MediaBrowser.Api
} }
} }
lock (_transcodingLocks)
{
_transcodingLocks.Remove(path);
}
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
{ {
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId); _sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
@ -497,6 +519,11 @@ namespace MediaBrowser.Api
} }
} }
lock (_transcodingLocks)
{
_transcodingLocks.Remove(job.Path);
}
lock (job.ProcessLock) lock (job.ProcessLock)
{ {
if (job.TranscodingThrottler != null) if (job.TranscodingThrottler != null)

View File

@ -12,9 +12,13 @@ using ServiceStack;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommonIO;
using MediaBrowser.Api.Playback.Progressive;
using MediaBrowser.Controller.Configuration;
namespace MediaBrowser.Api.LiveTv namespace MediaBrowser.Api.LiveTv
{ {
@ -613,16 +617,24 @@ namespace MediaBrowser.Api.LiveTv
} }
[Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
public class GetLiveStreamFile
{
public string Id { get; set; }
public string Container { get; set; }
}
public class LiveTvService : BaseApiService public class LiveTvService : BaseApiService
{ {
private readonly ILiveTvManager _liveTvManager; private readonly ILiveTvManager _liveTvManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IFileSystem _fileSystem;
public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager, IConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService) public LiveTvService(ILiveTvManager liveTvManager, IUserManager userManager, IServerConfigurationManager config, IHttpClient httpClient, ILibraryManager libraryManager, IDtoService dtoService, IFileSystem fileSystem)
{ {
_liveTvManager = liveTvManager; _liveTvManager = liveTvManager;
_userManager = userManager; _userManager = userManager;
@ -630,6 +642,23 @@ namespace MediaBrowser.Api.LiveTv
_httpClient = httpClient; _httpClient = httpClient;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_dtoService = dtoService; _dtoService = dtoService;
_fileSystem = fileSystem;
}
public object Get(GetLiveStreamFile request)
{
var filePath = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, request.Id + ".ts");
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
var streamSource = new ProgressiveFileCopier(_fileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None)
{
AllowEndOfFile = false
};
return ResultFactory.GetAsyncStreamWriter(streamSource);
} }
public object Get(GetDefaultListingProvider request) public object Get(GetDefaultListingProvider request)

View File

@ -87,7 +87,8 @@ namespace MediaBrowser.Api.Playback.Hls
if (!FileSystem.FileExists(playlist)) if (!FileSystem.FileExists(playlist))
{ {
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlist);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try try
{ {
if (!FileSystem.FileExists(playlist)) if (!FileSystem.FileExists(playlist))
@ -104,13 +105,13 @@ namespace MediaBrowser.Api.Playback.Hls
throw; throw;
} }
var waitForSegments = state.SegmentLength >= 10 ? 2 : (state.SegmentLength > 3 || !isLive ? 3 : 4); var waitForSegments = state.SegmentLength >= 10 ? 2 : (state.SegmentLength > 3 || !isLive ? 3 : 3);
await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false); await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false);
} }
} }
finally finally
{ {
ApiEntryPoint.Instance.TranscodingStartLock.Release(); transcodingLock.Release();
} }
} }
@ -182,7 +183,9 @@ namespace MediaBrowser.Api.Playback.Hls
{ {
Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist); Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist);
while (true) while (!cancellationToken.IsCancellationRequested)
{
try
{ {
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
using (var fileStream = GetPlaylistFileStream(playlist)) using (var fileStream = GetPlaylistFileStream(playlist))
@ -209,6 +212,13 @@ namespace MediaBrowser.Api.Playback.Hls
} }
} }
} }
catch (IOException)
{
// May get an error if the file is locked
}
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
} }
protected Stream GetPlaylistFileStream(string path) protected Stream GetPlaylistFileStream(string path)

View File

@ -171,14 +171,15 @@ namespace MediaBrowser.Api.Playback.Hls
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false); return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
} }
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
var released = false; var released = false;
try try
{ {
if (FileSystem.FileExists(segmentPath)) if (FileSystem.FileExists(segmentPath))
{ {
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
ApiEntryPoint.Instance.TranscodingStartLock.Release(); transcodingLock.Release();
released = true; released = true;
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false); return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
} }
@ -242,7 +243,7 @@ namespace MediaBrowser.Api.Playback.Hls
{ {
if (!released) if (!released)
{ {
ApiEntryPoint.Instance.TranscodingStartLock.Release(); transcodingLock.Release();
} }
} }

View File

@ -17,6 +17,7 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommonIO; using CommonIO;
using ServiceStack;
namespace MediaBrowser.Api.Playback.Progressive namespace MediaBrowser.Api.Playback.Progressive
{ {
@ -129,6 +130,23 @@ namespace MediaBrowser.Api.Playback.Progressive
using (state) using (state)
{ {
if (state.MediaPath.IndexOf("/livestreamfiles/", StringComparison.OrdinalIgnoreCase) != -1)
{
var parts = state.MediaPath.Split('/');
var filename = parts[parts.Length - 2] + Path.GetExtension(parts[parts.Length - 1]);
var filePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, filename);
var outputHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
outputHeaders["Content-Type"] = MimeTypes.GetMimeType(filePath);
var streamSource = new ProgressiveFileCopier(FileSystem, filePath, outputHeaders, null, Logger, CancellationToken.None)
{
AllowEndOfFile = false
};
return ResultFactory.GetAsyncStreamWriter(streamSource);
}
return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource) return await GetStaticRemoteStreamResult(state, responseHeaders, isHeadRequest, cancellationTokenSource)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -345,7 +363,8 @@ namespace MediaBrowser.Api.Playback.Progressive
return streamResult; return streamResult;
} }
await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(outputPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try try
{ {
TranscodingJob job; TranscodingJob job;
@ -376,7 +395,7 @@ namespace MediaBrowser.Api.Playback.Progressive
} }
finally finally
{ {
ApiEntryPoint.Instance.TranscodingStartLock.Release(); transcodingLock.Release();
} }
} }

View File

@ -24,6 +24,8 @@ namespace MediaBrowser.Api.Playback.Progressive
private long _bytesWritten = 0; private long _bytesWritten = 0;
public bool AllowEndOfFile = true;
public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken) public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, TranscodingJob job, ILogger logger, CancellationToken cancellationToken)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
@ -50,7 +52,7 @@ namespace MediaBrowser.Api.Playback.Progressive
using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) using (var fs = _fileSystem.GetFileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
{ {
while (eofCount < 15) while (eofCount < 15 || !AllowEndOfFile)
{ {
var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false); var bytesRead = await CopyToAsyncInternal(fs, outputStream, BufferSize, _cancellationToken).ConfigureAwait(false);

View File

@ -73,10 +73,6 @@ namespace MediaBrowser.Api.Playback
{ {
get get
{ {
if (!RunTimeTicks.HasValue)
{
return 6;
}
if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{ {
var userAgent = UserAgent ?? string.Empty; var userAgent = UserAgent ?? string.Empty;
@ -92,12 +88,16 @@ namespace MediaBrowser.Api.Playback
return 10; return 10;
} }
if (!RunTimeTicks.HasValue)
{
return 3;
}
return 6; return 6;
} }
if (!RunTimeTicks.HasValue) if (!RunTimeTicks.HasValue)
{ {
return 6; return 3;
} }
return 3; return 3;
} }

View File

@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="streamId">The stream identifier.</param> /// <param name="streamId">The stream identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;MediaSourceInfo&gt;.</returns> /// <returns>Task&lt;MediaSourceInfo&gt;.</returns>
Task<Tuple<MediaSourceInfo,SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken); Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Gets the channel stream media sources. /// Gets the channel stream media sources.
/// </summary> /// </summary>

View File

@ -0,0 +1,30 @@
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.LiveTv
{
public class LiveStream
{
public MediaSourceInfo OriginalMediaSource { get; set; }
public MediaSourceInfo PublicMediaSource { get; set; }
public string Id { get; set; }
public LiveStream(MediaSourceInfo mediaSource)
{
OriginalMediaSource = mediaSource;
PublicMediaSource = mediaSource;
Id = mediaSource.Id;
}
public virtual Task Open(CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
public virtual Task Close()
{
return Task.FromResult(true);
}
}
}

View File

@ -201,6 +201,7 @@
<Compile Include="Library\UserDataSaveEventArgs.cs" /> <Compile Include="Library\UserDataSaveEventArgs.cs" />
<Compile Include="LiveTv\IListingsProvider.cs" /> <Compile Include="LiveTv\IListingsProvider.cs" />
<Compile Include="LiveTv\ITunerHost.cs" /> <Compile Include="LiveTv\ITunerHost.cs" />
<Compile Include="LiveTv\LiveStream.cs" />
<Compile Include="LiveTv\RecordingGroup.cs" /> <Compile Include="LiveTv\RecordingGroup.cs" />
<Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" /> <Compile Include="LiveTv\RecordingStatusChangedEventArgs.cs" />
<Compile Include="LiveTv\ILiveTvRecording.cs" /> <Compile Include="LiveTv\ILiveTvRecording.cs" />

View File

@ -43,16 +43,14 @@ namespace MediaBrowser.Server.Implementations.IO
// WMC temp recording directories that will constantly be written to // WMC temp recording directories that will constantly be written to
"TempRec", "TempRec",
"TempSBE", "TempSBE"
"@eaDir",
"eaDir",
"#recycle"
}; };
private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string> private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string>
{ {
// Synology // Synology
"@eaDir", "eaDir",
"#recycle",
".wd_tv", ".wd_tv",
".actors" ".actors"
}; };

View File

@ -2803,6 +2803,17 @@ namespace MediaBrowser.Server.Implementations.Library
} }
} }
private bool ValidateNetworkPath(string path)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT || !path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase))
{
return Directory.Exists(path);
}
// Without native support for unc, we cannot validate this when running under mono
return true;
}
private const string ShortcutFileExtension = ".mblink"; private const string ShortcutFileExtension = ".mblink";
private const string ShortcutFileSearch = "*" + ShortcutFileExtension; private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
@ -2829,12 +2840,7 @@ namespace MediaBrowser.Server.Implementations.Library
throw new DirectoryNotFoundException("The path does not exist."); throw new DirectoryNotFoundException("The path does not exist.");
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{
throw new DirectoryNotFoundException("The network path does not exist.");
}
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath))
{ {
throw new DirectoryNotFoundException("The network path does not exist."); throw new DirectoryNotFoundException("The network path does not exist.");
} }
@ -2877,7 +2883,7 @@ namespace MediaBrowser.Server.Implementations.Library
throw new ArgumentNullException("path"); throw new ArgumentNullException("path");
} }
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !_fileSystem.DirectoryExists(pathInfo.NetworkPath)) if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
{ {
throw new DirectoryNotFoundException("The network path does not exist."); throw new DirectoryNotFoundException("The network path does not exist.");
} }

View File

@ -69,11 +69,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
} }
private const int BufferSize = 81920; private const int BufferSize = 81920;
public static async Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken) public static Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken)
{
return CopyUntilCancelled(source, target, null, cancellationToken);
}
public static async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken)
{ {
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, cancellationToken).ConfigureAwait(false); var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false);
onStarted = null;
//var position = fs.Position; //var position = fs.Position;
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path); //_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
@ -85,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
} }
} }
private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken) private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
{ {
byte[] buffer = new byte[bufferSize]; byte[] buffer = new byte[bufferSize];
int bytesRead; int bytesRead;
@ -96,6 +102,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
if (onStarted != null)
{
onStarted();
}
onStarted = null;
} }
return totalBytesRead; return totalBytesRead;

View File

@ -746,33 +746,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException(); throw new NotImplementedException();
} }
private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
private readonly Dictionary<string, LiveStream> _liveStreams = new Dictionary<string, LiveStream>();
public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{ {
_logger.Info("Streaming Channel " + channelId); var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
foreach (var hostInstance in _liveTvManager.TunerHosts) return result.Item1.PublicMediaSource;
{
try
{
var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
result.Item2.Release();
return result.Item1;
}
catch (FileNotFoundException)
{
}
catch (Exception e)
{
_logger.ErrorException("Error getting channel stream", e);
}
} }
throw new ApplicationException("Tuner not found."); private async Task<Tuple<LiveStream, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
}
private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
{ {
_logger.Info("Streaming Channel " + channelId); _logger.Info("Streaming Channel " + channelId);
@ -782,7 +766,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{ {
var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2); await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
_liveStreams[result.Id] = result;
_liveStreamsSemaphore.Release();
return new Tuple<LiveStream, ITunerHost>(result, hostInstance);
} }
catch (FileNotFoundException) catch (FileNotFoundException)
{ {
@ -823,9 +811,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task CloseLiveStream(string id, CancellationToken cancellationToken) public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
{ {
return Task.FromResult(0); await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
try
{
LiveStream stream;
if (_liveStreams.TryGetValue(id, out stream))
{
_liveStreams.Remove(id);
try
{
await stream.Close().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error closing live stream", ex);
}
}
}
finally
{
_liveStreamsSemaphore.Release();
}
} }
public Task RecordLiveStream(string id, CancellationToken cancellationToken) public Task RecordLiveStream(string id, CancellationToken cancellationToken)
@ -999,15 +1009,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
string seriesPath = null; string seriesPath = null;
var recordPath = GetRecordingPath(timer, out seriesPath); var recordPath = GetRecordingPath(timer, out seriesPath);
var recordingStatus = RecordingStatus.New; var recordingStatus = RecordingStatus.New;
var isResourceOpen = false;
SemaphoreSlim semaphore = null; LiveStream liveStream = null;
try try
{ {
var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false);
isResourceOpen = true;
semaphore = result.Item3; var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None).ConfigureAwait(false);
var mediaStreamInfo = result.Item1; liveStream = liveStreamInfo.Item1;
var mediaStreamInfo = liveStreamInfo.Item1.PublicMediaSource;
var tunerHost = liveStreamInfo.Item2;
// HDHR doesn't seem to release the tuner right away after first probing with ffmpeg // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
//await Task.Delay(3000, cancellationToken).ConfigureAwait(false); //await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
@ -1034,13 +1046,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
timer.Status = RecordingStatus.InProgress; timer.Status = RecordingStatus.InProgress;
_timerProvider.AddOrUpdate(timer, false); _timerProvider.AddOrUpdate(timer, false);
result.Item3.Release();
isResourceOpen = false;
SaveNfo(timer, recordPath, seriesPath); SaveNfo(timer, recordPath, seriesPath);
}; };
var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration); var pathWithDuration = tunerHost.ApplyDuration(mediaStreamInfo.Path, duration);
// If it supports supplying duration via url // If it supports supplying duration via url
if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
@ -1064,11 +1073,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.ErrorException("Error recording to {0}", ex, recordPath); _logger.ErrorException("Error recording to {0}", ex, recordPath);
recordingStatus = RecordingStatus.Error; recordingStatus = RecordingStatus.Error;
} }
finally
if (liveStream != null)
{ {
if (isResourceOpen && semaphore != null) try
{ {
semaphore.Release(); await CloseLiveStream(liveStream.Id, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error closing live stream", ex);
}
} }
_libraryManager.UnRegisterIgnoredPath(recordPath); _libraryManager.UnRegisterIgnoredPath(recordPath);
@ -1076,7 +1091,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
ActiveRecordingInfo removed; ActiveRecordingInfo removed;
_activeRecordings.TryRemove(timer.Id, out removed); _activeRecordings.TryRemove(timer.Id, out removed);
}
if (recordingStatus == RecordingStatus.Completed) if (recordingStatus == RecordingStatus.Completed)
{ {

View File

@ -68,18 +68,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{ {
if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1) var durationToken = new CancellationTokenSource(duration);
{ cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken)
.ConfigureAwait(false);
return; await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false);
}
var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); _logger.Info("Recording completed to file {0}", targetFile);
await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken)
.ConfigureAwait(false);
} }
private async void DeleteTempFile(string path) private async void DeleteTempFile(string path)
@ -108,76 +102,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
} }
} }
private async Task RecordWithoutTempFile(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
var durationToken = new CancellationTokenSource(duration);
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, false, duration, onStarted, cancellationToken).ConfigureAwait(false);
_logger.Info("Recording completed to file {0}", targetFile);
}
private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
var httpRequestOptions = new HttpRequestOptions()
{
Url = mediaSource.Path
};
httpRequestOptions.BufferContent = false;
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
{
_logger.Info("Opened recording stream from tuner provider");
Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{
//onStarted();
_logger.Info("Copying recording stream to file {0}", tempFile);
var bufferMs = 5000;
if (mediaSource.RunTimeTicks.HasValue)
{
// The media source already has a fixed duration
// But add another stop 1 minute later just in case the recording gets stuck for any reason
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1)));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
}
else
{
// The media source if infinite so we need to handle stopping ourselves
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs)));
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
}
var tempFileTask = DirectRecorder.CopyUntilCancelled(response.Content, output, cancellationToken);
// Give the temp file a little time to build up
await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false);
var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, true, duration, onStarted, cancellationToken), CancellationToken.None);
try
{
await tempFileTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
await recordTask.ConfigureAwait(false);
}
}
_logger.Info("Recording completed to file {0}", targetFile);
}
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, bool deleteInputFileAfterCompletion, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, bool deleteInputFileAfterCompletion, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{ {
_targetPath = targetFile; _targetPath = targetFile;

View File

@ -10,6 +10,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
@ -18,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{ {
public abstract class BaseTunerHost public abstract class BaseTunerHost
{ {
protected readonly IConfigurationManager Config; protected readonly IServerConfigurationManager Config;
protected readonly ILogger Logger; protected readonly ILogger Logger;
protected IJsonSerializer JsonSerializer; protected IJsonSerializer JsonSerializer;
protected readonly IMediaEncoder MediaEncoder; protected readonly IMediaEncoder MediaEncoder;
@ -26,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache = private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase); new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder) protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
{ {
Config = config; Config = config;
Logger = logger; Logger = logger;
@ -125,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
foreach (var host in hostsWithChannel) foreach (var host in hostsWithChannel)
{ {
var resourcePool = GetLock(host.Url);
Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool");
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool");
try try
{ {
// Check to make sure the tuner is available // Check to make sure the tuner is available
@ -156,22 +151,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{ {
Logger.Error("Error opening tuner", ex); Logger.Error("Error opening tuner", ex);
} }
finally
{
resourcePool.Release();
}
} }
} }
return new List<MediaSourceInfo>(); return new List<MediaSourceInfo>();
} }
protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken); protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{ {
if (IsValidChannelId(channelId)) if (!IsValidChannelId(channelId))
{ {
throw new FileNotFoundException();
}
var hosts = GetTunerHosts(); var hosts = GetTunerHosts();
var hostsWithChannel = new List<TunerHostInfo>(); var hostsWithChannel = new List<TunerHostInfo>();
@ -204,46 +198,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
foreach (var host in hostsWithChannel) foreach (var host in hostsWithChannel)
{ {
var resourcePool = GetLock(host.Url);
Logger.Debug("GetChannelStream - Waiting on tuner resource pool");
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
Logger.Debug("GetChannelStream - Unlocked resource pool");
try try
{ {
// Check to make sure the tuner is available var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
// If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error await liveStream.Open(cancellationToken).ConfigureAwait(false);
// If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources return liveStream;
if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1)
{
if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
{
Logger.Error("Tuner is not currently available");
resourcePool.Release();
continue;
}
}
var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
if (EnableMediaProbing)
{
await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
}
return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error("Error opening tuner", ex); Logger.Error("Error opening tuner", ex);
resourcePool.Release();
} }
} }
}
else
{
throw new FileNotFoundException();
}
throw new LiveTvConflictException(); throw new LiveTvConflictException();
} }
@ -268,37 +233,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken); protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
/// <summary> private async Task AddMediaInfo(LiveStream stream, bool isAudio, CancellationToken cancellationToken)
/// The _semaphoreLocks
/// </summary>
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the lock.
/// </summary>
/// <param name="url">The filename.</param>
/// <returns>System.Object.</returns>
private SemaphoreSlim GetLock(string url)
{ {
return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1)); //await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
}
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken) //try
{ //{
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); // await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
try // // Leave the resource locked. it will be released upstream
{ //}
await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false); //catch (Exception)
//{
// // Release the resource if there's some kind of failure.
// resourcePool.Release();
// Leave the resource locked. it will be released upstream // throw;
} //}
catch (Exception)
{
// Release the resource if there's some kind of failure.
resourcePool.Release();
throw;
}
} }
private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken) private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)

View File

@ -14,7 +14,10 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommonIO;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
@ -24,11 +27,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationHost _appHost;
public HdHomerunHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient) public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost)
: base(config, logger, jsonSerializer, mediaEncoder) : base(config, logger, jsonSerializer, mediaEncoder)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_fileSystem = fileSystem;
_appHost = appHost;
} }
public string Name public string Name
@ -355,6 +362,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
url += "?transcode=" + profile; url += "?transcode=" + profile;
} }
var id = profile;
if (string.IsNullOrWhiteSpace(id))
{
id = "native";
}
id += "_" + url.GetMD5().ToString("N");
var mediaSource = new MediaSourceInfo var mediaSource = new MediaSourceInfo
{ {
Path = url, Path = url,
@ -387,9 +401,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
RequiresClosing = false, RequiresClosing = false,
BufferMs = 0, BufferMs = 0,
Container = "ts", Container = "ts",
Id = profile, Id = id,
SupportsDirectPlay = true, SupportsDirectPlay = false,
SupportsDirectStream = false, SupportsDirectStream = true,
SupportsTranscoding = true SupportsTranscoding = true
}; };
@ -452,9 +466,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
} }
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
{ {
Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty); var profile = streamId.Split('_')[0];
Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase)) if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
{ {
@ -462,7 +478,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
var hdhrId = GetHdHrIdFromChannelId(channelId); var hdhrId = GetHdHrIdFromChannelId(channelId);
return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false); var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false);
var liveStream = new HdHomerunLiveStream(mediaSource, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
return liveStream;
} }
public async Task Validate(TunerHostInfo info) public async Task Validate(TunerHostInfo info)

View File

@ -0,0 +1,156 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using CommonIO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
public class HdHomerunLiveStream : LiveStream
{
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IFileSystem _fileSystem;
private readonly IServerApplicationPaths _appPaths;
private readonly IServerApplicationHost _appHost;
private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource();
public HdHomerunLiveStream(MediaSourceInfo mediaSource, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
: base(mediaSource)
{
_fileSystem = fileSystem;
_httpClient = httpClient;
_logger = logger;
_appPaths = appPaths;
_appHost = appHost;
}
public override async Task Open(CancellationToken openCancellationToken)
{
_liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
var mediaSource = OriginalMediaSource;
var url = mediaSource.Path;
var tempFile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
_logger.Info("Opening HDHR Live stream from {0} to {1}", url, tempFile);
var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read);
var taskCompletionSource = new TaskCompletionSource<bool>();
StartStreamingToTempFile(output, tempFile, url, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
await taskCompletionSource.Task.ConfigureAwait(false);
PublicMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + Path.GetFileNameWithoutExtension(tempFile) + "/stream.ts";
PublicMediaSource.Protocol = MediaProtocol.Http;
}
public override Task Close()
{
_liveStreamCancellationTokenSource.Cancel();
return base.Close();
}
private async Task StartStreamingToTempFile(Stream outputStream, string tempFilePath, string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
await Task.Run(async () =>
{
using (outputStream)
{
var isFirstAttempt = true;
while (!cancellationToken.IsCancellationRequested)
{
try
{
using (var response = await _httpClient.SendAsync(new HttpRequestOptions
{
Url = url,
CancellationToken = cancellationToken,
BufferContent = false
}, "GET").ConfigureAwait(false))
{
_logger.Info("Opened HDHR stream from {0}", url);
if (!cancellationToken.IsCancellationRequested)
{
_logger.Info("Beginning DirectRecorder.CopyUntilCancelled");
Action onStarted = null;
if (isFirstAttempt)
{
onStarted = () => openTaskCompletionSource.TrySetResult(true);
}
await DirectRecorder.CopyUntilCancelled(response.Content, outputStream, onStarted, cancellationToken).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
if (isFirstAttempt)
{
_logger.ErrorException("Error opening live stream:", ex);
openTaskCompletionSource.TrySetException(ex);
break;
}
_logger.ErrorException("Error copying live stream, will reopen", ex);
}
isFirstAttempt = false;
}
}
await Task.Delay(5000).ConfigureAwait(false);
DeleteTempFile(tempFilePath);
}).ConfigureAwait(false);
}
private async void DeleteTempFile(string path)
{
for (var i = 0; i < 10; i++)
{
try
{
File.Delete(path);
return;
}
catch (FileNotFoundException)
{
return;
}
catch (DirectoryNotFoundException)
{
return;
}
catch (Exception ex)
{
_logger.ErrorException("Error deleting temp file {0}", ex, path);
}
await Task.Delay(1000).ConfigureAwait(false);
}
}
}
}

View File

@ -13,8 +13,10 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommonIO; using CommonIO;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
{ {
@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
: base(config, logger, jsonSerializer, mediaEncoder) : base(config, logger, jsonSerializer, mediaEncoder)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list); return Task.FromResult(list);
} }
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken) protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
{ {
var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false); var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false);
return sources.First(); var liveStream = new LiveStream(sources.First());
return liveStream;
} }
public async Task Validate(TunerHostInfo info) public async Task Validate(TunerHostInfo info)
@ -136,7 +139,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
RequiresOpening = false, RequiresOpening = false,
RequiresClosing = false, RequiresClosing = false,
ReadAtNativeFramerate = false ReadAtNativeFramerate = false,
Id = channel.Path.GetMD5().ToString("N")
}; };
return new List<MediaSourceInfo> { mediaSource }; return new List<MediaSourceInfo> { mediaSource };

View File

@ -8,6 +8,7 @@ using CommonIO;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -16,6 +17,7 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
{ {
@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient) public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
: base(config, logger, jsonSerializer, mediaEncoder) : base(config, logger, jsonSerializer, mediaEncoder)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
return new List<MediaSourceInfo>(); return new List<MediaSourceInfo>();
} }
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken) protected override async Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
{ {
var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false); var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false);
return sources.First(); var liveStream = new LiveStream(sources.First());
return liveStream;
} }
protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)

View File

@ -241,6 +241,7 @@
<Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" /> <Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" /> <Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunLiveStream.cs" />
<Compile Include="LiveTv\TunerHosts\M3uParser.cs" /> <Compile Include="LiveTv\TunerHosts\M3uParser.cs" />
<Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" /> <Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
<Compile Include="LiveTv\ProgramImageProvider.cs" /> <Compile Include="LiveTv\ProgramImageProvider.cs" />

View File

@ -104,6 +104,12 @@
<Content Include="dashboard-ui\camerauploadsettings.html"> <Content Include="dashboard-ui\camerauploadsettings.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\components\accessschedule\accessschedule.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\accessschedule\accessschedule.template.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\components\appfooter\appfooter.css"> <Content Include="dashboard-ui\components\appfooter\appfooter.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@ -437,15 +443,6 @@
<Content Include="dashboard-ui\scripts\sections.js"> <Content Include="dashboard-ui\scripts\sections.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.collapsible.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.collapsible.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.controlgroup.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.css"> <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.listview.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
@ -470,9 +467,6 @@
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.widget.js"> <Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jqm.widget.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="dashboard-ui\thirdparty\jquerymobile-1.4.5\jquery.mobile.custom.theme.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="dashboard-ui\thirdparty\paper-button-style.css"> <Content Include="dashboard-ui\thirdparty\paper-button-style.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>

View File

@ -91,6 +91,16 @@ namespace MediaBrowser.XbmcMetadata
return; return;
} }
if (!item.SupportsLocalMetadata)
{
return;
}
if (!item.IsSaveLocalMetadataEnabled())
{
return;
}
try try
{ {
await _providerManager.SaveMetadata(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false); await _providerManager.SaveMetadata(item, updateReason, new[] { BaseNfoSaver.SaverName }).ConfigureAwait(false);