mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
rework live stream handling
This commit is contained in:
parent
48d7f686eb
commit
d596053ec7
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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<MediaSourceInfo>.</returns>
|
/// <returns>Task<MediaSourceInfo>.</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>
|
||||||
|
30
MediaBrowser.Controller/LiveTv/LiveStream.cs
Normal file
30
MediaBrowser.Controller/LiveTv/LiveStream.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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" />
|
||||||
|
@ -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"
|
||||||
};
|
};
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 };
|
||||||
|
@ -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)
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user