commit
88e3fcfdc7
|
@ -6,7 +6,7 @@ using System.Security;
|
|||
using System.Text;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.ServerApplication.Native
|
||||
namespace Emby.Common.Implementations.IO
|
||||
{
|
||||
public class LnkShortcutHandler :IShortcutHandler
|
||||
{
|
||||
|
@ -35,7 +35,6 @@ namespace MediaBrowser.ServerApplication.Native
|
|||
/// <summary>
|
||||
/// Class NativeMethods
|
||||
/// </summary>
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
public static class NativeMethods
|
||||
{
|
||||
/// <summary>
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
namespace Emby.Common.Implementations.IO
|
||||
{
|
||||
|
@ -18,17 +19,21 @@ namespace Emby.Common.Implementations.IO
|
|||
private readonly bool _supportsAsyncFileStreams;
|
||||
private char[] _invalidFileNameChars;
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||
private bool EnableFileSystemRequestConcat = true;
|
||||
private bool EnableFileSystemRequestConcat;
|
||||
|
||||
private string _tempPath;
|
||||
|
||||
public ManagedFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, bool enableFileSystemRequestConcat, string tempPath)
|
||||
public ManagedFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string tempPath)
|
||||
{
|
||||
Logger = logger;
|
||||
_supportsAsyncFileStreams = supportsAsyncFileStreams;
|
||||
_supportsAsyncFileStreams = true;
|
||||
_tempPath = tempPath;
|
||||
EnableFileSystemRequestConcat = enableFileSystemRequestConcat;
|
||||
SetInvalidFileNameChars(enableManagedInvalidFileNameChars);
|
||||
|
||||
// On Linux, this needs to be true or symbolic links are ignored
|
||||
EnableFileSystemRequestConcat = environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows &&
|
||||
environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.OSX;
|
||||
|
||||
SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows);
|
||||
}
|
||||
|
||||
public void AddShortcutHandler(IShortcutHandler handler)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Common.Implementations.Networking;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
@ -96,6 +97,46 @@ namespace Emby.Common.Implementations.Net
|
|||
_acceptor.StartAccept();
|
||||
}
|
||||
|
||||
#if NET46
|
||||
public Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = TransmitFileOptions.UseKernelApc;
|
||||
|
||||
var completionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
var result = Socket.BeginSendFile(path, preBuffer, postBuffer, options, new AsyncCallback(FileSendCallback), new Tuple<Socket, string, TaskCompletionSource<bool>>(Socket, path, completionSource));
|
||||
|
||||
return completionSource.Task;
|
||||
}
|
||||
|
||||
private void FileSendCallback(IAsyncResult ar)
|
||||
{
|
||||
// Retrieve the socket from the state object.
|
||||
Tuple<Socket, string, TaskCompletionSource<bool>> data = (Tuple<Socket, string, TaskCompletionSource<bool>>)ar.AsyncState;
|
||||
|
||||
var client = data.Item1;
|
||||
var path = data.Item2;
|
||||
var taskCompletion = data.Item3;
|
||||
|
||||
// Complete sending the data to the remote device.
|
||||
try {
|
||||
client.EndSendFile(ar);
|
||||
taskCompletion.TrySetResult(true);
|
||||
}
|
||||
catch(SocketException ex){
|
||||
_logger.Info("Socket.SendFile failed for {0}. error code {1}", path, ex.SocketErrorCode);
|
||||
taskCompletion.TrySetException(ex);
|
||||
}catch(Exception ex){
|
||||
taskCompletion.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
#else
|
||||
public Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endif
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Socket.Dispose();
|
||||
|
|
|
@ -52,6 +52,18 @@ namespace Emby.Common.Implementations.Net
|
|||
{
|
||||
throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
if (dualMode)
|
||||
{
|
||||
// Mono for BSD incorrectly throws ArgumentException instead of SocketException
|
||||
throw new SocketCreateException("AddressFamilyNotSupported", ex);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort)
|
||||
|
@ -59,6 +71,7 @@ namespace Emby.Common.Implementations.Net
|
|||
if (remotePort < 0) throw new ArgumentException("remotePort cannot be less than zero.", "remotePort");
|
||||
|
||||
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);
|
||||
|
||||
try
|
||||
{
|
||||
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
|
@ -96,6 +109,27 @@ namespace Emby.Common.Implementations.Net
|
|||
}
|
||||
}
|
||||
|
||||
public ISocket CreateUdpBroadcastSocket(int localPort)
|
||||
{
|
||||
if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort");
|
||||
|
||||
var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
|
||||
try
|
||||
{
|
||||
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
|
||||
|
||||
return new UdpSocket(retVal, localPort, IPAddress.Any);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (retVal != null)
|
||||
retVal.Dispose();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UDP acceptSocket that is a member of the SSDP multicast local admin group and binds it to the specified local port.
|
||||
/// </summary>
|
||||
|
|
|
@ -16,12 +16,23 @@ namespace Emby.Common.Implementations.Net
|
|||
|
||||
internal sealed class UdpSocket : DisposableManagedObjectBase, ISocket
|
||||
{
|
||||
|
||||
#region Fields
|
||||
|
||||
private Socket _Socket;
|
||||
private int _LocalPort;
|
||||
#endregion
|
||||
|
||||
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||
{
|
||||
SocketFlags = SocketFlags.None
|
||||
};
|
||||
|
||||
private readonly SocketAsyncEventArgs _sendSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||
{
|
||||
SocketFlags = SocketFlags.None
|
||||
};
|
||||
|
||||
private TaskCompletionSource<SocketReceiveResult> _currentReceiveTaskCompletionSource;
|
||||
private TaskCompletionSource<int> _currentSendTaskCompletionSource;
|
||||
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
public UdpSocket(Socket socket, int localPort, IPAddress ip)
|
||||
{
|
||||
|
@ -32,6 +43,61 @@ namespace Emby.Common.Implementations.Net
|
|||
LocalIPAddress = NetworkManager.ToIpAddressInfo(ip);
|
||||
|
||||
_Socket.Bind(new IPEndPoint(ip, _LocalPort));
|
||||
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
private void InitReceiveSocketAsyncEventArgs()
|
||||
{
|
||||
var receiveBuffer = new byte[8192];
|
||||
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
||||
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
|
||||
|
||||
var sendBuffer = new byte[8192];
|
||||
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
||||
_sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
|
||||
}
|
||||
|
||||
private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
var tcs = _currentReceiveTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
{
|
||||
_currentReceiveTaskCompletionSource = null;
|
||||
|
||||
if (e.SocketError == SocketError.Success)
|
||||
{
|
||||
tcs.TrySetResult(new SocketReceiveResult
|
||||
{
|
||||
Buffer = e.Buffer,
|
||||
ReceivedBytes = e.BytesTransferred,
|
||||
RemoteEndPoint = ToIpEndPointInfo(e.RemoteEndPoint as IPEndPoint),
|
||||
LocalIPAddress = LocalIPAddress
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
var tcs = _currentSendTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
{
|
||||
_currentSendTaskCompletionSource = null;
|
||||
|
||||
if (e.SocketError == SocketError.Success)
|
||||
{
|
||||
tcs.TrySetResult(e.BytesTransferred);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UdpSocket(Socket socket, IpEndPointInfo endPoint)
|
||||
|
@ -40,6 +106,8 @@ namespace Emby.Common.Implementations.Net
|
|||
|
||||
_Socket = socket;
|
||||
_Socket.Connect(NetworkManager.ToIPEndPoint(endPoint));
|
||||
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public IpAddressInfo LocalIPAddress
|
||||
|
@ -48,32 +116,33 @@ namespace Emby.Common.Implementations.Net
|
|||
private set;
|
||||
}
|
||||
|
||||
#region ISocket Members
|
||||
|
||||
public Task<SocketReceiveResult> ReceiveAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
var tcs = new TaskCompletionSource<SocketReceiveResult>();
|
||||
|
||||
EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0);
|
||||
|
||||
var state = new AsyncReceiveState(_Socket, receivedFromEndPoint);
|
||||
state.TaskCompletionSource = tcs;
|
||||
|
||||
#if NETSTANDARD1_6
|
||||
_Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer), SocketFlags.None, state.RemoteEndPoint)
|
||||
.ContinueWith((task, asyncState) =>
|
||||
cancellationToken.Register(() => tcs.TrySetCanceled());
|
||||
|
||||
_receiveSocketAsyncEventArgs.RemoteEndPoint = receivedFromEndPoint;
|
||||
_currentReceiveTaskCompletionSource = tcs;
|
||||
|
||||
try
|
||||
{
|
||||
if (task.Status != TaskStatus.Faulted)
|
||||
var willRaiseEvent = _Socket.ReceiveFromAsync(_receiveSocketAsyncEventArgs);
|
||||
|
||||
if (!willRaiseEvent)
|
||||
{
|
||||
var receiveState = asyncState as AsyncReceiveState;
|
||||
receiveState.RemoteEndPoint = task.Result.RemoteEndPoint;
|
||||
ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress);
|
||||
_receiveSocketAsyncEventArgs_Completed(this, _receiveSocketAsyncEventArgs);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
}, state);
|
||||
#else
|
||||
_Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state);
|
||||
#endif
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
@ -129,15 +198,48 @@ namespace Emby.Common.Implementations.Net
|
|||
taskSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
//_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.Port));
|
||||
|
||||
return taskSource.Task;
|
||||
#endif
|
||||
//ThrowIfDisposed();
|
||||
|
||||
//if (buffer == null) throw new ArgumentNullException("messageData");
|
||||
//if (endPoint == null) throw new ArgumentNullException("endPoint");
|
||||
|
||||
//cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
//var tcs = new TaskCompletionSource<int>();
|
||||
|
||||
//cancellationToken.Register(() => tcs.TrySetCanceled());
|
||||
|
||||
//_sendSocketAsyncEventArgs.SetBuffer(buffer, 0, size);
|
||||
//_sendSocketAsyncEventArgs.RemoteEndPoint = NetworkManager.ToIPEndPoint(endPoint);
|
||||
//_currentSendTaskCompletionSource = tcs;
|
||||
|
||||
//var willRaiseEvent = _Socket.SendAsync(_sendSocketAsyncEventArgs);
|
||||
|
||||
//if (!willRaiseEvent)
|
||||
//{
|
||||
// _sendSocketAsyncEventArgs_Completed(this, _sendSocketAsyncEventArgs);
|
||||
//}
|
||||
|
||||
//return tcs.Task;
|
||||
}
|
||||
|
||||
#endregion
|
||||
public async Task SendWithLockAsync(byte[] buffer, int size, IpEndPointInfo endPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
#region Overrides
|
||||
//await _sendLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await SendAsync(buffer, size, endPoint, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
|
@ -146,44 +248,19 @@ namespace Emby.Common.Implementations.Net
|
|||
var socket = _Socket;
|
||||
if (socket != null)
|
||||
socket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
_sendLock.Dispose();
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private static void ProcessResponse(AsyncReceiveState state, Func<int> receiveData, IpAddressInfo localIpAddress)
|
||||
var tcs = _currentReceiveTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytesRead = receiveData();
|
||||
|
||||
var ipEndPoint = state.RemoteEndPoint as IPEndPoint;
|
||||
state.TaskCompletionSource.SetResult(
|
||||
new SocketReceiveResult
|
||||
{
|
||||
Buffer = state.Buffer,
|
||||
ReceivedBytes = bytesRead,
|
||||
RemoteEndPoint = ToIpEndPointInfo(ipEndPoint),
|
||||
LocalIPAddress = localIpAddress
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
var sendTcs = _currentSendTaskCompletionSource;
|
||||
if (sendTcs != null)
|
||||
{
|
||||
state.TaskCompletionSource.SetCanceled();
|
||||
sendTcs.TrySetCanceled();
|
||||
}
|
||||
catch (SocketException se)
|
||||
{
|
||||
if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown)
|
||||
state.TaskCompletionSource.SetException(se);
|
||||
else
|
||||
state.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,10 +304,6 @@ namespace Emby.Common.Implementations.Net
|
|||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Classes
|
||||
|
||||
private class AsyncReceiveState
|
||||
{
|
||||
public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint)
|
||||
|
@ -247,8 +320,5 @@ namespace Emby.Common.Implementations.Net
|
|||
public TaskCompletionSource<SocketReceiveResult> TaskCompletionSource { get; set; }
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ namespace Emby.Dlna.Didl
|
|||
var targetHeight = streamInfo.TargetHeight;
|
||||
|
||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
|
||||
streamInfo.VideoCodec,
|
||||
streamInfo.TargetVideoCodec,
|
||||
streamInfo.TargetAudioCodec,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
|
@ -352,7 +352,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec,
|
||||
streamInfo.VideoCodec,
|
||||
streamInfo.TargetVideoCodec,
|
||||
streamInfo.TargetAudioBitrate,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
|
|
|
@ -542,7 +542,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
var list = new ContentFeatureBuilder(profile)
|
||||
.BuildVideoHeader(streamInfo.Container,
|
||||
streamInfo.VideoCodec,
|
||||
streamInfo.TargetVideoCodec,
|
||||
streamInfo.TargetAudioCodec,
|
||||
streamInfo.TargetWidth,
|
||||
streamInfo.TargetHeight,
|
||||
|
|
|
@ -238,7 +238,7 @@ namespace Emby.Drawing
|
|||
var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]);
|
||||
var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
|
||||
|
||||
var imageProcessingLockTaken = false;
|
||||
//var imageProcessingLockTaken = false;
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -253,9 +253,9 @@ namespace Emby.Drawing
|
|||
var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(cacheFilePath));
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath));
|
||||
|
||||
await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
//await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
imageProcessingLockTaken = true;
|
||||
//imageProcessingLockTaken = true;
|
||||
|
||||
_imageEncoder.EncodeImage(originalImagePath, tmpPath, AutoOrient(options.Item), newWidth, newHeight, quality, options, outputFormat);
|
||||
CopyFile(tmpPath, cacheFilePath);
|
||||
|
@ -273,13 +273,13 @@ namespace Emby.Drawing
|
|||
// Just spit out the original file if all the options are default
|
||||
return new Tuple<string, string, DateTime>(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (imageProcessingLockTaken)
|
||||
{
|
||||
_imageProcessingSemaphore.Release();
|
||||
}
|
||||
}
|
||||
//finally
|
||||
//{
|
||||
// if (imageProcessingLockTaken)
|
||||
// {
|
||||
// _imageProcessingSemaphore.Release();
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
private void CopyFile(string src, string destination)
|
||||
|
|
|
@ -61,7 +61,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Emby.Common.Implementations;
|
||||
using Emby.Common.Implementations.Archiving;
|
||||
using Emby.Common.Implementations.Networking;
|
||||
using Emby.Common.Implementations.IO;
|
||||
using Emby.Common.Implementations.Reflection;
|
||||
using Emby.Common.Implementations.Serialization;
|
||||
using Emby.Common.Implementations.TextEncoding;
|
||||
|
@ -93,7 +93,7 @@ using Emby.Server.Implementations.Social;
|
|||
using Emby.Server.Implementations.Channels;
|
||||
using Emby.Server.Implementations.Collections;
|
||||
using Emby.Server.Implementations.Dto;
|
||||
using Emby.Server.Implementations.EntryPoints;
|
||||
using Emby.Server.Implementations.IO;
|
||||
using Emby.Server.Implementations.FileOrganization;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.Security;
|
||||
|
@ -107,7 +107,6 @@ using Emby.Server.Implementations.Playlists;
|
|||
using Emby.Server.Implementations;
|
||||
using Emby.Server.Implementations.ServerManager;
|
||||
using Emby.Server.Implementations.Session;
|
||||
using Emby.Server.Implementations.Social;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using MediaBrowser.Model.Activity;
|
||||
|
@ -294,6 +293,13 @@ namespace Emby.Server.Core
|
|||
ImageEncoder = imageEncoder;
|
||||
|
||||
SetBaseExceptionMessage();
|
||||
|
||||
if (environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
|
||||
{
|
||||
fileSystem.AddShortcutHandler(new LnkShortcutHandler());
|
||||
}
|
||||
|
||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||
}
|
||||
|
||||
private Version _version;
|
||||
|
@ -606,7 +612,7 @@ namespace Emby.Server.Core
|
|||
CertificatePath = GetCertificatePath(true);
|
||||
Certificate = GetCertificate(CertificatePath);
|
||||
|
||||
HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, SupportsDualModeSockets);
|
||||
HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, FileSystemManager, SupportsDualModeSockets);
|
||||
HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
|
||||
RegisterSingleInstance(HttpServer, false);
|
||||
progress.Report(10);
|
||||
|
@ -796,17 +802,25 @@ namespace Emby.Server.Core
|
|||
info.FFMpegFilename = "ffmpeg";
|
||||
info.FFProbeFilename = "ffprobe";
|
||||
info.ArchiveType = "7z";
|
||||
info.Version = "20160215";
|
||||
info.Version = "20170308";
|
||||
info.DownloadUrls = GetLinuxDownloadUrls();
|
||||
}
|
||||
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows)
|
||||
{
|
||||
info.FFMpegFilename = "ffmpeg.exe";
|
||||
info.FFProbeFilename = "ffprobe.exe";
|
||||
info.Version = "20160410";
|
||||
info.Version = "20170308";
|
||||
info.ArchiveType = "7z";
|
||||
info.DownloadUrls = GetWindowsDownloadUrls();
|
||||
}
|
||||
else if (EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.OSX)
|
||||
{
|
||||
info.FFMpegFilename = "ffmpeg";
|
||||
info.FFProbeFilename = "ffprobe";
|
||||
info.ArchiveType = "7z";
|
||||
info.Version = "20170308";
|
||||
info.DownloadUrls = GetMacDownloadUrls();
|
||||
}
|
||||
else
|
||||
{
|
||||
// No version available - user requirement
|
||||
|
@ -816,6 +830,20 @@ namespace Emby.Server.Core
|
|||
return info;
|
||||
}
|
||||
|
||||
private string[] GetMacDownloadUrls()
|
||||
{
|
||||
switch (EnvironmentInfo.SystemArchitecture)
|
||||
{
|
||||
case Architecture.X64:
|
||||
return new[]
|
||||
{
|
||||
"https://embydata.com/downloads/ffmpeg/osx/ffmpeg-x64-20170308.7z"
|
||||
};
|
||||
}
|
||||
|
||||
return new string[] { };
|
||||
}
|
||||
|
||||
private string[] GetWindowsDownloadUrls()
|
||||
{
|
||||
switch (EnvironmentInfo.SystemArchitecture)
|
||||
|
@ -823,12 +851,12 @@ namespace Emby.Server.Core
|
|||
case Architecture.X64:
|
||||
return new[]
|
||||
{
|
||||
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win64.7z"
|
||||
"https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win64.7z"
|
||||
};
|
||||
case Architecture.X86:
|
||||
return new[]
|
||||
{
|
||||
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/windows/ffmpeg-20160410-win32.7z"
|
||||
"https://embydata.com/downloads/ffmpeg/windows/ffmpeg-20170308-win32.7z"
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -842,12 +870,12 @@ namespace Emby.Server.Core
|
|||
case Architecture.X64:
|
||||
return new[]
|
||||
{
|
||||
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-64bit-static.7z"
|
||||
"https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-64bit-static.7z"
|
||||
};
|
||||
case Architecture.X86:
|
||||
return new[]
|
||||
{
|
||||
"https://github.com/MediaBrowser/Emby.Resources/raw/master/ffmpeg/linux/ffmpeg-git-20160215-32bit-static.7z"
|
||||
"https://embydata.com/downloads/ffmpeg/linux/ffmpeg-git-20170301-32bit-static.7z"
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1714,14 +1742,8 @@ namespace Emby.Server.Core
|
|||
((IProcess)sender).Dispose();
|
||||
}
|
||||
|
||||
public void EnableLoopback(string appName)
|
||||
public virtual void EnableLoopback(string appName)
|
||||
{
|
||||
EnableLoopbackInternal(appName);
|
||||
}
|
||||
|
||||
protected virtual void EnableLoopbackInternal(string appName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void RegisterModules()
|
||||
|
|
|
@ -45,6 +45,7 @@ namespace Emby.Server.Core
|
|||
IXmlSerializer xml,
|
||||
IEnvironmentInfo environment,
|
||||
ICertificate certificate,
|
||||
IFileSystem fileSystem,
|
||||
bool enableDualModeSockets)
|
||||
{
|
||||
var logger = logManager.GetLogger("HttpServer");
|
||||
|
@ -65,7 +66,8 @@ namespace Emby.Server.Core
|
|||
certificate,
|
||||
new StreamFactory(),
|
||||
GetParseFn,
|
||||
enableDualModeSockets);
|
||||
enableDualModeSockets,
|
||||
fileSystem);
|
||||
}
|
||||
|
||||
private static Func<string, object> GetParseFn(Type propertyType)
|
||||
|
|
|
@ -421,17 +421,6 @@ namespace Emby.Server.Core.IO
|
|||
|
||||
var path = e.FullPath;
|
||||
|
||||
// For deletes, use the parent path
|
||||
if (e.ChangeType == WatcherChangeTypes.Deleted)
|
||||
{
|
||||
var parentPath = Path.GetDirectoryName(path);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parentPath))
|
||||
{
|
||||
path = parentPath;
|
||||
}
|
||||
}
|
||||
|
||||
ReportFileSystemChanged(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
|
@ -22,13 +23,15 @@ namespace Emby.Server.Implementations.Data
|
|||
private readonly IItemRepository _itemRepo;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem)
|
||||
public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_itemRepo = itemRepo;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
public string Name
|
||||
|
@ -150,13 +153,27 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
try
|
||||
{
|
||||
var isPathInLibrary = false;
|
||||
|
||||
if (allLibraryPaths.Any(i => path.StartsWith(i, StringComparison.Ordinal)) ||
|
||||
allLibraryPaths.Contains(path, StringComparer.Ordinal) ||
|
||||
path.StartsWith(_appPaths.ProgramDataPath, StringComparison.Ordinal))
|
||||
{
|
||||
isPathInLibrary = true;
|
||||
|
||||
if (_fileSystem.FileExists(path) || _fileSystem.DirectoryExists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var libraryItem = _libraryManager.GetItemById(item.Item1);
|
||||
|
||||
if (libraryItem == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (libraryItem.IsTopParent)
|
||||
{
|
||||
continue;
|
||||
|
@ -180,7 +197,14 @@ namespace Emby.Server.Implementations.Data
|
|||
continue;
|
||||
}
|
||||
|
||||
if (isPathInLibrary)
|
||||
{
|
||||
_logger.Info("Deleting item from database {0} because path no longer exists. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("Deleting item from database {0} because path is no longer in the server library. type: {1} path: {2}", libraryItem.Name, libraryItem.GetType().Name, libraryItemPath ?? string.Empty);
|
||||
}
|
||||
|
||||
await libraryItem.OnFileDeleted().ConfigureAwait(false);
|
||||
}
|
||||
|
|
|
@ -3204,6 +3204,40 @@ namespace Emby.Server.Implementations.Data
|
|||
}
|
||||
}
|
||||
|
||||
private bool IsAlphaNumeric(string str)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
if (!(char.IsLetter(str[i])) && (!(char.IsNumber(str[i]))))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsValidType(string value)
|
||||
{
|
||||
return IsAlphaNumeric(value);
|
||||
}
|
||||
|
||||
private bool IsValidMediaType(string value)
|
||||
{
|
||||
return IsAlphaNumeric(value);
|
||||
}
|
||||
|
||||
private bool IsValidId(string value)
|
||||
{
|
||||
return IsAlphaNumeric(value);
|
||||
}
|
||||
|
||||
private bool IsValidPersonType(string value)
|
||||
{
|
||||
return IsAlphaNumeric(value);
|
||||
}
|
||||
|
||||
private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement, string paramSuffix = "")
|
||||
{
|
||||
if (query.IsResumable ?? false)
|
||||
|
@ -3423,9 +3457,9 @@ namespace Emby.Server.Implementations.Data
|
|||
statement.TryBind("@ChannelId", query.ChannelIds[0]);
|
||||
}
|
||||
}
|
||||
if (query.ChannelIds.Length > 1)
|
||||
else if (query.ChannelIds.Length > 1)
|
||||
{
|
||||
var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i + "'").ToArray());
|
||||
var inClause = string.Join(",", query.ChannelIds.Where(IsValidId).Select(i => "'" + i + "'").ToArray());
|
||||
whereClauses.Add(string.Format("ChannelId in ({0})", inClause));
|
||||
}
|
||||
|
||||
|
@ -4157,17 +4191,18 @@ namespace Emby.Server.Implementations.Data
|
|||
whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))");
|
||||
}
|
||||
}
|
||||
if (query.MediaTypes.Length == 1)
|
||||
var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray();
|
||||
if (queryMediaTypes.Length == 1)
|
||||
{
|
||||
whereClauses.Add("MediaType=@MediaTypes");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@MediaTypes", query.MediaTypes[0]);
|
||||
statement.TryBind("@MediaTypes", queryMediaTypes[0]);
|
||||
}
|
||||
}
|
||||
if (query.MediaTypes.Length > 1)
|
||||
else if (queryMediaTypes.Length > 1)
|
||||
{
|
||||
var val = string.Join(",", query.MediaTypes.Select(i => "'" + i + "'").ToArray());
|
||||
var val = string.Join(",", queryMediaTypes.Select(i => "'" + i + "'").ToArray());
|
||||
|
||||
whereClauses.Add("MediaType in (" + val + ")");
|
||||
}
|
||||
|
@ -4273,7 +4308,9 @@ namespace Emby.Server.Implementations.Data
|
|||
//var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0;
|
||||
var enableItemsByName = query.IncludeItemsByName ?? false;
|
||||
|
||||
if (query.TopParentIds.Length == 1)
|
||||
var queryTopParentIds = query.TopParentIds.Where(IsValidId).ToArray();
|
||||
|
||||
if (queryTopParentIds.Length == 1)
|
||||
{
|
||||
if (enableItemsByName)
|
||||
{
|
||||
|
@ -4289,12 +4326,12 @@ namespace Emby.Server.Implementations.Data
|
|||
}
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@TopParentId", query.TopParentIds[0]);
|
||||
statement.TryBind("@TopParentId", queryTopParentIds[0]);
|
||||
}
|
||||
}
|
||||
if (query.TopParentIds.Length > 1)
|
||||
else if (queryTopParentIds.Length > 1)
|
||||
{
|
||||
var val = string.Join(",", query.TopParentIds.Select(i => "'" + i + "'").ToArray());
|
||||
var val = string.Join(",", queryTopParentIds.Select(i => "'" + i + "'").ToArray());
|
||||
|
||||
if (enableItemsByName)
|
||||
{
|
||||
|
@ -4544,7 +4581,7 @@ namespace Emby.Server.Implementations.Data
|
|||
return result;
|
||||
}
|
||||
|
||||
return new[] { value };
|
||||
return new[] { value }.Where(IsValidType);
|
||||
}
|
||||
|
||||
public async Task DeleteItem(Guid id, CancellationToken cancellationToken)
|
||||
|
@ -4696,31 +4733,35 @@ namespace Emby.Server.Implementations.Data
|
|||
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToGuidParamValue());
|
||||
}
|
||||
}
|
||||
if (query.PersonTypes.Count == 1)
|
||||
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
|
||||
|
||||
if (queryPersonTypes.Count == 1)
|
||||
{
|
||||
whereClauses.Add("PersonType=@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", query.PersonTypes[0]);
|
||||
statement.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
}
|
||||
if (query.PersonTypes.Count > 1)
|
||||
else if (queryPersonTypes.Count > 1)
|
||||
{
|
||||
var val = string.Join(",", query.PersonTypes.Select(i => "'" + i + "'").ToArray());
|
||||
var val = string.Join(",", queryPersonTypes.Select(i => "'" + i + "'").ToArray());
|
||||
|
||||
whereClauses.Add("PersonType in (" + val + ")");
|
||||
}
|
||||
if (query.ExcludePersonTypes.Count == 1)
|
||||
var queryExcludePersonTypes = query.ExcludePersonTypes.Where(IsValidPersonType).ToList();
|
||||
|
||||
if (queryExcludePersonTypes.Count == 1)
|
||||
{
|
||||
whereClauses.Add("PersonType<>@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", query.ExcludePersonTypes[0]);
|
||||
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
}
|
||||
if (query.ExcludePersonTypes.Count > 1)
|
||||
else if (queryExcludePersonTypes.Count > 1)
|
||||
{
|
||||
var val = string.Join(",", query.ExcludePersonTypes.Select(i => "'" + i + "'").ToArray());
|
||||
var val = string.Join(",", queryExcludePersonTypes.Select(i => "'" + i + "'").ToArray());
|
||||
|
||||
whereClauses.Add("PersonType not in (" + val + ")");
|
||||
}
|
||||
|
|
|
@ -491,7 +491,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
}
|
||||
}
|
||||
|
||||
//if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess))
|
||||
if (!(item is LiveTvProgram) || fields.Contains(ItemFields.PlayAccess))
|
||||
{
|
||||
dto.PlayAccess = item.GetPlayAccess(user);
|
||||
}
|
||||
|
@ -1639,7 +1639,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
var width = size.Width;
|
||||
var height = size.Height;
|
||||
|
||||
if (width == 0 || height == 0)
|
||||
if (width.Equals(0) || height.Equals(0))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
<Compile Include="FileOrganization\NameUtils.cs" />
|
||||
<Compile Include="FileOrganization\OrganizerScheduledTask.cs" />
|
||||
<Compile Include="FileOrganization\TvFolderOrganizer.cs" />
|
||||
<Compile Include="HttpServer\GetSwaggerResource.cs" />
|
||||
<Compile Include="HttpServer\FileWriter.cs" />
|
||||
<Compile Include="HttpServer\HttpListenerHost.cs" />
|
||||
<Compile Include="HttpServer\HttpResultFactory.cs" />
|
||||
<Compile Include="HttpServer\LoggerUtils.cs" />
|
||||
|
@ -102,7 +102,6 @@
|
|||
<Compile Include="HttpServer\SocketSharp\WebSocketSharpRequest.cs" />
|
||||
<Compile Include="HttpServer\SocketSharp\WebSocketSharpResponse.cs" />
|
||||
<Compile Include="HttpServer\StreamWriter.cs" />
|
||||
<Compile Include="HttpServer\SwaggerService.cs" />
|
||||
<Compile Include="Images\BaseDynamicImageProvider.cs" />
|
||||
<Compile Include="IO\FileRefresher.cs" />
|
||||
<Compile Include="IO\MbLinkShortcutHandler.cs" />
|
||||
|
@ -170,7 +169,6 @@
|
|||
<Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunManager.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHttpStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunUdpStream.cs" />
|
||||
|
@ -302,8 +300,8 @@
|
|||
<HintPath>..\packages\Emby.XmlTv.1.0.7\lib\portable-net45+win8\Emby.XmlTv.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="MediaBrowser.Naming, Version=1.0.6201.24431, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MediaBrowser.Naming.1.0.4\lib\portable-net45+win8\MediaBrowser.Naming.dll</HintPath>
|
||||
<Reference Include="MediaBrowser.Naming, Version=1.0.6279.25941, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MediaBrowser.Naming.1.0.5\lib\portable-net45+win8\MediaBrowser.Naming.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="SQLitePCL.pretty, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
|
|
|
@ -677,20 +677,7 @@ namespace Emby.Server.Implementations.FileOrganization
|
|||
|
||||
var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options);
|
||||
|
||||
// MAX_PATH - trailing <NULL> charachter - drive component: 260 - 1 - 3 = 256
|
||||
// Usually newPath would include the drive component, but use 256 to be sure
|
||||
var maxFilenameLength = 256 - newPath.Length;
|
||||
|
||||
if (!newPath.EndsWith(@"\"))
|
||||
{
|
||||
// Remove 1 for missing backslash combining path and filename
|
||||
maxFilenameLength--;
|
||||
}
|
||||
|
||||
// Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt)
|
||||
maxFilenameLength -= 4;
|
||||
|
||||
var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength);
|
||||
var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options);
|
||||
|
||||
if (string.IsNullOrEmpty(episodeFileName))
|
||||
{
|
||||
|
@ -742,7 +729,7 @@ namespace Emby.Server.Implementations.FileOrganization
|
|||
return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName));
|
||||
}
|
||||
|
||||
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength)
|
||||
private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options)
|
||||
{
|
||||
seriesName = _fileSystem.GetValidFilename(seriesName).Trim();
|
||||
|
||||
|
@ -786,32 +773,15 @@ namespace Emby.Server.Implementations.FileOrganization
|
|||
.Replace("%0e", episodeNumber.ToString("00", _usCulture))
|
||||
.Replace("%00e", episodeNumber.ToString("000", _usCulture));
|
||||
|
||||
if (maxLength.HasValue && result.Contains("%#"))
|
||||
if (result.Contains("%#"))
|
||||
{
|
||||
// Substract 3 for the temp token length (%#1, %#2 or %#3)
|
||||
int maxRemainingTitleLength = maxLength.Value - result.Length + 3;
|
||||
string shortenedEpisodeTitle = string.Empty;
|
||||
|
||||
if (maxRemainingTitleLength > 5)
|
||||
{
|
||||
// A title with fewer than 5 letters wouldn't be of much value
|
||||
shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length));
|
||||
result = result.Replace("%#1", episodeTitle)
|
||||
.Replace("%#2", episodeTitle.Replace(" ", "."))
|
||||
.Replace("%#3", episodeTitle.Replace(" ", "_"));
|
||||
}
|
||||
|
||||
result = result.Replace("%#1", shortenedEpisodeTitle)
|
||||
.Replace("%#2", shortenedEpisodeTitle.Replace(" ", "."))
|
||||
.Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_"));
|
||||
}
|
||||
|
||||
if (maxLength.HasValue && result.Length > maxLength.Value)
|
||||
{
|
||||
// There may be cases where reducing the title length may still not be sufficient to
|
||||
// stay below maxLength
|
||||
var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength);
|
||||
throw new Exception(msg);
|
||||
}
|
||||
|
||||
return result;
|
||||
// Finally, call GetValidFilename again in case user customized the episode expression with any invalid filename characters
|
||||
return _fileSystem.GetValidFilename(result).Trim();
|
||||
}
|
||||
|
||||
private bool IsSameEpisode(string sourcePath, string newPath)
|
||||
|
|
191
Emby.Server.Implementations/HttpServer/FileWriter.cs
Normal file
191
Emby.Server.Implementations/HttpServer/FileWriter.cs
Normal file
|
@ -0,0 +1,191 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class FileWriter : IHttpResult
|
||||
{
|
||||
private ILogger Logger { get; set; }
|
||||
|
||||
private string RangeHeader { get; set; }
|
||||
private bool IsHeadRequest { get; set; }
|
||||
|
||||
private long RangeStart { get; set; }
|
||||
private long RangeEnd { get; set; }
|
||||
private long RangeLength { get; set; }
|
||||
private long TotalContentLength { get; set; }
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
public Action OnError { get; set; }
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
public List<Cookie> Cookies { get; private set; }
|
||||
|
||||
public FileShareMode FileShare { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _options
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
|
||||
/// <summary>
|
||||
/// Gets the options.
|
||||
/// </summary>
|
||||
/// <value>The options.</value>
|
||||
public IDictionary<string, string> Headers
|
||||
{
|
||||
get { return _options; }
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
throw new ArgumentNullException("contentType");
|
||||
}
|
||||
|
||||
Path = path;
|
||||
Logger = logger;
|
||||
RangeHeader = rangeHeader;
|
||||
|
||||
Headers["Content-Type"] = contentType;
|
||||
|
||||
TotalContentLength = fileSystem.GetFileInfo(path).Length;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rangeHeader))
|
||||
{
|
||||
Headers["Content-Length"] = TotalContentLength.ToString(UsCulture);
|
||||
StatusCode = HttpStatusCode.OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
Headers["Accept-Ranges"] = "bytes";
|
||||
StatusCode = HttpStatusCode.PartialContent;
|
||||
SetRangeValues();
|
||||
}
|
||||
|
||||
FileShare = FileShareMode.Read;
|
||||
Cookies = new List<Cookie>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the range values.
|
||||
/// </summary>
|
||||
private void SetRangeValues()
|
||||
{
|
||||
var requestedRange = RequestedRanges[0];
|
||||
|
||||
// If the requested range is "0-", we can optimize by just doing a stream copy
|
||||
if (!requestedRange.Value.HasValue)
|
||||
{
|
||||
RangeEnd = TotalContentLength - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
RangeEnd = requestedRange.Value.Value;
|
||||
}
|
||||
|
||||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
// Content-Length is the length of what we're serving, not the original content
|
||||
Headers["Content-Length"] = RangeLength.ToString(UsCulture);
|
||||
Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _requested ranges
|
||||
/// </summary>
|
||||
private List<KeyValuePair<long, long?>> _requestedRanges;
|
||||
/// <summary>
|
||||
/// Gets the requested ranges.
|
||||
/// </summary>
|
||||
/// <value>The requested ranges.</value>
|
||||
protected List<KeyValuePair<long, long?>> RequestedRanges
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_requestedRanges == null)
|
||||
{
|
||||
_requestedRanges = new List<KeyValuePair<long, long?>>();
|
||||
|
||||
// Example: bytes=0-,32-63
|
||||
var ranges = RangeHeader.Split('=')[1].Split(',');
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var vals = range.Split('-');
|
||||
|
||||
long start = 0;
|
||||
long? end = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(vals[0]))
|
||||
{
|
||||
start = long.Parse(vals[0], UsCulture);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(vals[1]))
|
||||
{
|
||||
end = long.Parse(vals[1], UsCulture);
|
||||
}
|
||||
|
||||
_requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
return _requestedRanges;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteToAsync(IResponse response, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Headers only
|
||||
if (IsHeadRequest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(RangeHeader) || (RangeStart <= 0 && RangeEnd >= TotalContentLength - 1))
|
||||
{
|
||||
Logger.Info("Transmit file {0}", Path);
|
||||
await response.TransmitFile(Path, 0, 0, FileShare, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await response.TransmitFile(Path, RangeStart, RangeLength, FileShare, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnComplete != null)
|
||||
{
|
||||
OnComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public IRequest RequestContext { get; set; }
|
||||
|
||||
public object Response { get; set; }
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public HttpStatusCode StatusCode
|
||||
{
|
||||
get { return (HttpStatusCode)Status; }
|
||||
set { Status = (int)value; }
|
||||
}
|
||||
|
||||
public string StatusDescription { get; set; }
|
||||
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Class GetDashboardResource
|
||||
/// </summary>
|
||||
[Route("/swagger-ui/{ResourceName*}", "GET")]
|
||||
public class GetSwaggerResource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string ResourceName { get; set; }
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly ICertificate _certificate;
|
||||
|
@ -70,8 +71,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
ILogger logger,
|
||||
IServerConfigurationManager config,
|
||||
string serviceName,
|
||||
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets)
|
||||
: base()
|
||||
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func<Type, Func<string, object>> funcParseFn, bool enableDualModeSockets, IFileSystem fileSystem)
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
|
@ -89,6 +89,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
_streamFactory = streamFactory;
|
||||
_funcParseFn = funcParseFn;
|
||||
_enableDualModeSockets = enableDualModeSockets;
|
||||
_fileSystem = fileSystem;
|
||||
_config = config;
|
||||
|
||||
_logger = logger;
|
||||
|
@ -226,7 +227,8 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
_cryptoProvider,
|
||||
_streamFactory,
|
||||
_enableDualModeSockets,
|
||||
GetRequest);
|
||||
GetRequest,
|
||||
_fileSystem);
|
||||
}
|
||||
|
||||
private IHttpRequest GetRequest(HttpListenerContext httpContext)
|
||||
|
|
|
@ -474,10 +474,6 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
{
|
||||
throw new ArgumentNullException("cacheKey");
|
||||
}
|
||||
if (options.ContentFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException("factoryFn");
|
||||
}
|
||||
|
||||
var key = cacheKey.ToString("N");
|
||||
|
||||
|
@ -560,15 +556,28 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
{
|
||||
var rangeHeader = requestContext.Headers.Get("Range");
|
||||
|
||||
var stream = await factoryFn().ConfigureAwait(false);
|
||||
if (!isHeadRequest && !string.IsNullOrWhiteSpace(options.Path))
|
||||
{
|
||||
return new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem)
|
||||
{
|
||||
OnComplete = options.OnComplete,
|
||||
OnError = options.OnError,
|
||||
FileShare = options.FileShare
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(rangeHeader))
|
||||
{
|
||||
var stream = await factoryFn().ConfigureAwait(false);
|
||||
|
||||
return new RangeRequestWriter(rangeHeader, stream, contentType, isHeadRequest, _logger)
|
||||
{
|
||||
OnComplete = options.OnComplete
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var stream = await factoryFn().ConfigureAwait(false);
|
||||
|
||||
responseHeaders["Content-Length"] = stream.Length.ToString(UsCulture);
|
||||
|
||||
|
@ -585,6 +594,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
OnError = options.OnError
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
using (var stream = await factoryFn().ConfigureAwait(false))
|
||||
{
|
||||
|
|
|
@ -27,10 +27,11 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
|||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private readonly IStreamFactory _streamFactory;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly Func<HttpListenerContext, IHttpRequest> _httpRequestFactory;
|
||||
private readonly bool _enableDualMode;
|
||||
|
||||
public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func<HttpListenerContext, IHttpRequest> httpRequestFactory)
|
||||
public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func<HttpListenerContext, IHttpRequest> httpRequestFactory, IFileSystem fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_certificate = certificate;
|
||||
|
@ -42,6 +43,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
|||
_streamFactory = streamFactory;
|
||||
_enableDualMode = enableDualMode;
|
||||
_httpRequestFactory = httpRequestFactory;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public Action<Exception, IRequest, bool> ErrorHandler { get; set; }
|
||||
|
@ -54,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
|||
public void Start(IEnumerable<string> urlPrefixes)
|
||||
{
|
||||
if (_listener == null)
|
||||
_listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider);
|
||||
_listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider, _fileSystem);
|
||||
|
||||
_listener.EnableDualMode = _enableDualMode;
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using SocketHttpListener.Net;
|
||||
using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
|
||||
|
@ -189,5 +192,10 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp
|
|||
public void ClearCookies()
|
||||
{
|
||||
}
|
||||
|
||||
public Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken)
|
||||
{
|
||||
return _response.TransmitFile(path, offset, count, fileShareMode, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class SwaggerService : IService, IRequiresRequest
|
||||
{
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public SwaggerService(IServerApplicationPaths appPaths, IFileSystem fileSystem, IHttpResultFactory resultFactory)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_resultFactory = resultFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified request.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetSwaggerResource request)
|
||||
{
|
||||
var swaggerDirectory = Path.Combine(_appPaths.ApplicationResourcesPath, "swagger-ui");
|
||||
|
||||
var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', _fileSystem.DirectorySeparatorChar));
|
||||
|
||||
return _resultFactory.GetStaticFileResult(Request, requestedFile).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the result factory.
|
||||
/// </summary>
|
||||
/// <value>The result factory.</value>
|
||||
private readonly IHttpResultFactory _resultFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the request context.
|
||||
/// </summary>
|
||||
/// <value>The request context.</value>
|
||||
public IRequest Request { get; set; }
|
||||
}
|
||||
}
|
|
@ -258,7 +258,7 @@ namespace Emby.Server.Implementations.Images
|
|||
{
|
||||
return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false);
|
||||
}
|
||||
if (item is Playlist || item is MusicGenre)
|
||||
if (item is Playlist || item is MusicGenre || item is Genre || item is GameGenre)
|
||||
{
|
||||
return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false);
|
||||
}
|
||||
|
|
|
@ -513,6 +513,11 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
public Guid GetNewItemId(string key, Type type)
|
||||
{
|
||||
return GetNewItemIdInternal(key, type, false);
|
||||
}
|
||||
|
||||
private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
|
@ -531,7 +536,7 @@ namespace Emby.Server.Implementations.Library
|
|||
.Replace("/", "\\");
|
||||
}
|
||||
|
||||
if (!ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
|
||||
if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
|
||||
{
|
||||
key = key.ToLower();
|
||||
}
|
||||
|
@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>Task{Person}.</returns>
|
||||
public Person GetPerson(string name)
|
||||
{
|
||||
return CreateItemByName<Person>(Person.GetPath(name), name);
|
||||
return CreateItemByName<Person>(Person.GetPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -875,7 +880,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>Task{Studio}.</returns>
|
||||
public Studio GetStudio(string name)
|
||||
{
|
||||
return CreateItemByName<Studio>(Studio.GetPath(name), name);
|
||||
return CreateItemByName<Studio>(Studio.GetPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -885,7 +890,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>Task{Genre}.</returns>
|
||||
public Genre GetGenre(string name)
|
||||
{
|
||||
return CreateItemByName<Genre>(Genre.GetPath(name), name);
|
||||
return CreateItemByName<Genre>(Genre.GetPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -895,7 +900,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>Task{MusicGenre}.</returns>
|
||||
public MusicGenre GetMusicGenre(string name)
|
||||
{
|
||||
return CreateItemByName<MusicGenre>(MusicGenre.GetPath(name), name);
|
||||
return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -905,7 +910,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>Task{GameGenre}.</returns>
|
||||
public GameGenre GetGameGenre(string name)
|
||||
{
|
||||
return CreateItemByName<GameGenre>(GameGenre.GetPath(name), name);
|
||||
return CreateItemByName<GameGenre>(GameGenre.GetPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -923,7 +928,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
var name = value.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return CreateItemByName<Year>(Year.GetPath(name), name);
|
||||
return CreateItemByName<Year>(Year.GetPath, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -933,10 +938,10 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <returns>Task{Genre}.</returns>
|
||||
public MusicArtist GetArtist(string name)
|
||||
{
|
||||
return CreateItemByName<MusicArtist>(MusicArtist.GetPath(name), name);
|
||||
return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name);
|
||||
}
|
||||
|
||||
private T CreateItemByName<T>(string path, string name)
|
||||
private T CreateItemByName<T>(Func<string,string> getPathFn, string name)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
if (typeof(T) == typeof(MusicArtist))
|
||||
|
@ -957,7 +962,9 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
var id = GetNewItemId(path, typeof(T));
|
||||
var path = getPathFn(name);
|
||||
var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds;
|
||||
var id = GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
|
||||
|
||||
var item = GetItemById(id) as T;
|
||||
|
||||
|
|
|
@ -369,6 +369,8 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
enableAutoClose = false;
|
||||
|
||||
try
|
||||
{
|
||||
var tuple = GetProvider(request.OpenToken);
|
||||
|
|
|
@ -442,6 +442,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
result = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var channel in result)
|
||||
{
|
||||
_logger.Info("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id);
|
||||
}
|
||||
|
||||
_epgChannels.AddOrUpdate(info.Id, result, (k, v) => result);
|
||||
}
|
||||
|
||||
|
@ -493,11 +498,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
|
||||
{
|
||||
var mappedTunerChannelId = GetMappedChannel(tunerChannel.TunerChannelId, mappings);
|
||||
var tunerChannelId = tunerChannel.TunerChannelId;
|
||||
if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
|
||||
}
|
||||
|
||||
var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mappedTunerChannelId))
|
||||
{
|
||||
mappedTunerChannelId = tunerChannel.TunerChannelId;
|
||||
mappedTunerChannelId = tunerChannelId;
|
||||
}
|
||||
|
||||
var channel = epgChannels.FirstOrDefault(i => string.Equals(mappedTunerChannelId, i.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
@ -639,8 +650,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
|
||||
{
|
||||
var existingTimer = _timerProvider.GetAll()
|
||||
.FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase));
|
||||
var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
|
||||
null :
|
||||
_timerProvider.GetTimerByProgramId(timer.ProgramId);
|
||||
|
||||
if (existingTimer != null)
|
||||
{
|
||||
|
@ -710,7 +722,34 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
throw new InvalidOperationException("SeriesId for program not found");
|
||||
}
|
||||
|
||||
// If any timers have already been manually created, make sure they don't get cancelled
|
||||
var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false))
|
||||
.Where(i =>
|
||||
{
|
||||
if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.ProgramId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.SeriesId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_seriesTimerProvider.Add(info);
|
||||
|
||||
foreach (var timer in existingTimers)
|
||||
{
|
||||
timer.SeriesTimerId = info.Id;
|
||||
timer.IsManual = true;
|
||||
|
||||
_timerProvider.AddOrUpdate(timer, false);
|
||||
}
|
||||
|
||||
await UpdateTimersForSeriesTimer(epgData, info, true, false).ConfigureAwait(false);
|
||||
|
||||
return info.Id;
|
||||
|
@ -991,6 +1030,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
if (epgChannel == null)
|
||||
{
|
||||
_logger.Debug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
|
||||
programs = new List<ProgramInfo>();
|
||||
}
|
||||
else
|
||||
|
@ -1276,6 +1316,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
return;
|
||||
}
|
||||
|
||||
var registration = await _liveTvManager.GetRegistrationInfo("dvr").ConfigureAwait(false);
|
||||
if (!registration.IsValid)
|
||||
{
|
||||
_logger.Warn("Emby Premiere required to use Emby DVR.");
|
||||
OnTimerOutOfDate(timer);
|
||||
return;
|
||||
}
|
||||
|
||||
var activeRecordingInfo = new ActiveRecordingInfo
|
||||
{
|
||||
CancellationTokenSource = new CancellationTokenSource(),
|
||||
|
@ -2299,6 +2347,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
var existingTimer = _timerProvider.GetTimer(timer.Id);
|
||||
|
||||
if (existingTimer == null)
|
||||
{
|
||||
existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
|
||||
? null
|
||||
: _timerProvider.GetTimerByProgramId(timer.ProgramId);
|
||||
}
|
||||
|
||||
if (existingTimer == null)
|
||||
{
|
||||
if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
|
||||
|
@ -2313,12 +2368,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
}
|
||||
else
|
||||
{
|
||||
// Only update if not currently active
|
||||
// Only update if not currently active - test both new timer and existing in case Id's are different
|
||||
// Id's could be different if the timer was created manually prior to series timer creation
|
||||
ActiveRecordingInfo activeRecordingInfo;
|
||||
if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo))
|
||||
if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo) && !_activeRecordings.TryGetValue(existingTimer.Id, out activeRecordingInfo))
|
||||
{
|
||||
UpdateExistingTimerWithNewMetadata(existingTimer, timer);
|
||||
|
||||
// Needed by ShouldCancelTimerForSeriesTimer
|
||||
timer.IsManual = existingTimer.IsManual;
|
||||
|
||||
if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
|
||||
{
|
||||
existingTimer.Status = RecordingStatus.Cancelled;
|
||||
|
@ -2516,7 +2575,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
list.Add(new VirtualFolderInfo
|
||||
{
|
||||
Locations = new List<string> { customPath },
|
||||
Name = "Recorded Series",
|
||||
Name = "Recorded Shows",
|
||||
CollectionType = CollectionType.TvShows
|
||||
});
|
||||
}
|
||||
|
@ -2531,6 +2590,86 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
public ProgramInfo Program { get; set; }
|
||||
public CancellationTokenSource CancellationTokenSource { get; set; }
|
||||
}
|
||||
|
||||
private const int TunerDiscoveryDurationMs = 3000;
|
||||
|
||||
public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<TunerHostInfo>();
|
||||
|
||||
var configuredDeviceIds = GetConfiguration().TunerHosts
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.DeviceId))
|
||||
.Select(i => i.DeviceId)
|
||||
.ToList();
|
||||
|
||||
foreach (var host in _liveTvManager.TunerHosts)
|
||||
{
|
||||
var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (newDevicesOnly)
|
||||
{
|
||||
discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
list.AddRange(discoveredDevices);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var host in _liveTvManager.TunerHosts)
|
||||
{
|
||||
await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken)
|
||||
{
|
||||
var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var configuredDevices = GetConfiguration().TunerHosts
|
||||
.Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
foreach (var device in discoveredDevices)
|
||||
{
|
||||
var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (configuredDevice != null)
|
||||
{
|
||||
if (!string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.Info("Tuner url has changed from {0} to {1}", configuredDevice.Url, device.Url);
|
||||
|
||||
configuredDevice.Url = device.Url;
|
||||
await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDuationMs, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var discoveredDevices = await host.DiscoverDevices(discoveryDuationMs, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var device in discoveredDevices)
|
||||
{
|
||||
_logger.Info("Discovered tuner device {0} at {1}", host.Name, device.Url);
|
||||
}
|
||||
|
||||
return discoveredDevices;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error discovering tuner devices", ex);
|
||||
|
||||
return new List<TunerHostInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
public static class ConfigurationExtension
|
||||
{
|
||||
|
|
|
@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
|
||||
var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
|
||||
var commandLineArgs = "-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4} -y \"{1}\"";
|
||||
var commandLineArgs = "-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4}{6} -y \"{1}\"";
|
||||
|
||||
long startTimeTicks = 0;
|
||||
//if (mediaSource.DateLiveStreamOpened.HasValue)
|
||||
|
@ -193,7 +193,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn";
|
||||
|
||||
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam);
|
||||
var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ?
|
||||
" -f mp4 -movflags frag_keyframe+empty_moov" :
|
||||
string.Empty;
|
||||
|
||||
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, durationParam, outputParam);
|
||||
|
||||
return inputModifiers + " " + commandLineArgs;
|
||||
}
|
||||
|
|
|
@ -166,5 +166,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public TimerInfo GetTimerByProgramId(string programId)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -205,6 +205,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
}
|
||||
|
||||
programInfo.ShowId = uniqueString.GetMD5().ToString("N");
|
||||
|
||||
// If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped
|
||||
if (programInfo.IsSeries && !programInfo.IsRepeat)
|
||||
{
|
||||
if ((programInfo.EpisodeNumber ?? 0) == 0)
|
||||
{
|
||||
programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct an id from the channel and start date
|
||||
|
|
|
@ -150,6 +150,21 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
get { return _listingProviders; }
|
||||
}
|
||||
|
||||
public List<NameIdPair> GetTunerHostTypes()
|
||||
{
|
||||
return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair
|
||||
{
|
||||
Name = i.Name,
|
||||
Id = i.Type
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken)
|
||||
{
|
||||
return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken);
|
||||
}
|
||||
|
||||
void service_DataSourceChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
|
@ -1063,6 +1078,8 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
var channel = GetInternalChannel(program.ChannelId);
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
var channelUserdata = _userDataManager.GetUserData(userId, channel);
|
||||
|
||||
if (channelUserdata.Likes ?? false)
|
||||
|
@ -1083,6 +1100,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
score += channelUserdata.PlayCount;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
@ -1180,6 +1198,8 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
{
|
||||
EmbyTV.EmbyTV.Current.CreateRecordingFolders();
|
||||
|
||||
await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var numComplete = 0;
|
||||
double progressPerService = _services.Count == 0
|
||||
? 0
|
||||
|
@ -2748,7 +2768,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
private bool IsLiveTvEnabled(User user)
|
||||
{
|
||||
return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count(i => i.IsEnabled) > 0);
|
||||
return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Count > 0);
|
||||
}
|
||||
|
||||
public IEnumerable<User> GetEnabledUsers()
|
||||
|
@ -2986,7 +3006,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
if (string.Equals(feature, "dvr-l", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var config = GetConfiguration();
|
||||
if (config.TunerHosts.Count(i => i.IsEnabled) > 0 &&
|
||||
if (config.TunerHosts.Count > 0 &&
|
||||
config.ListingProviders.Count(i => (i.EnableAllTuners || i.EnabledTuners.Length > 0) && string.Equals(i.Type, SchedulesDirect.TypeName, StringComparison.OrdinalIgnoreCase)) > 0)
|
||||
{
|
||||
return Task.FromResult(new MBRegistrationRecord
|
||||
|
@ -3000,50 +3020,6 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
return _security.GetRegistrationStatus(feature);
|
||||
}
|
||||
|
||||
public List<NameValuePair> GetSatIniMappings()
|
||||
{
|
||||
return new List<NameValuePair>();
|
||||
//var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini", StringComparison.OrdinalIgnoreCase) != -1).ToList();
|
||||
|
||||
//return names.Select(GetSatIniMappings).Where(i => i != null).DistinctBy(i => i.Value.Split('|')[0]).ToList();
|
||||
}
|
||||
|
||||
public NameValuePair GetSatIniMappings(string resource)
|
||||
{
|
||||
return new NameValuePair();
|
||||
//using (var stream = GetType().Assembly.GetManifestResourceStream(resource))
|
||||
//{
|
||||
// using (var reader = new StreamReader(stream))
|
||||
// {
|
||||
// var parser = new StreamIniDataParser();
|
||||
// IniData data = parser.ReadData(reader);
|
||||
|
||||
// var satType1 = data["SATTYPE"]["1"];
|
||||
// var satType2 = data["SATTYPE"]["2"];
|
||||
|
||||
// if (string.IsNullOrWhiteSpace(satType2))
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// var srch = "SatIp.ini.";
|
||||
// var filename = Path.GetFileName(resource);
|
||||
|
||||
// return new NameValuePair
|
||||
// {
|
||||
// Name = satType1 + " " + satType2,
|
||||
// Value = satType2 + "|" + filename.Substring(filename.IndexOf(srch) + srch.Length)
|
||||
// };
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
public Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new List<ChannelInfo>());
|
||||
//return new TunerHosts.SatIp.ChannelScan(_logger).Scan(info, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||
|
||||
public bool IsHidden
|
||||
{
|
||||
get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count(i => i.IsEnabled) == 0; }
|
||||
get { return _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Count == 0; }
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
|
|
|
@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
var result = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false);
|
||||
var list = result.ToList();
|
||||
Logger.Debug("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
|
||||
Logger.Info("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(key) && list.Count > 0)
|
||||
{
|
||||
|
@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
protected virtual List<TunerHostInfo> GetTunerHosts()
|
||||
{
|
||||
return GetConfiguration().TunerHosts
|
||||
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -135,8 +135,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
// Check to make sure the tuner is available
|
||||
// If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
|
||||
if (hostsWithChannel.Count > 1 &&
|
||||
!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
|
||||
if (hostsWithChannel.Count > 1 && !await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Logger.Error("Tuner is not currently available");
|
||||
continue;
|
||||
|
@ -208,6 +207,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
foreach (var host in hostsWithChannel)
|
||||
{
|
||||
if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -243,7 +247,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
|
||||
|
||||
protected abstract bool IsValidChannelId(string channelId);
|
||||
protected virtual string ChannelIdPrefix
|
||||
{
|
||||
get
|
||||
{
|
||||
return Type + "_";
|
||||
}
|
||||
}
|
||||
protected virtual bool IsValidChannelId(string channelId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(channelId))
|
||||
{
|
||||
throw new ArgumentNullException("channelId");
|
||||
}
|
||||
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected LiveTvOptions GetConfiguration()
|
||||
{
|
||||
|
|
|
@ -1,158 +0,0 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunDiscovery : IServerEntryPoint
|
||||
{
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IJsonSerializer _json;
|
||||
|
||||
public HdHomerunDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json)
|
||||
{
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_liveTvManager = liveTvManager;
|
||||
_httpClient = httpClient;
|
||||
_json = json;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
|
||||
}
|
||||
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
string server = null;
|
||||
var info = e.Argument;
|
||||
|
||||
if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
string location;
|
||||
if (info.Headers.TryGetValue("Location", out location))
|
||||
{
|
||||
//_logger.Debug("HdHomerun found at {0}", location);
|
||||
|
||||
// Just get the beginning of the url
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(location, UriKind.Absolute, out uri))
|
||||
{
|
||||
var apiUrl = location.Replace(uri.LocalPath, String.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd('/');
|
||||
|
||||
//_logger.Debug("HdHomerun api url: {0}", apiUrl);
|
||||
AddDevice(apiUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void AddDevice(string url)
|
||||
{
|
||||
await _semaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
var options = GetConfiguration();
|
||||
|
||||
if (options.TunerHosts.Any(i =>
|
||||
string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
|
||||
UriEquals(i.Url, url)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip off the port
|
||||
url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
|
||||
|
||||
// Test it by pulling down the lineup
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", url),
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = _json.DeserializeFromStream<HdHomerunHost.DiscoverResponse>(stream);
|
||||
|
||||
var existing = GetConfiguration().TunerHosts
|
||||
.FirstOrDefault(i => string.Equals(i.Type, HdHomerunHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.DeviceId, response.DeviceID, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
await _liveTvManager.SaveTunerHost(new TunerHostInfo
|
||||
{
|
||||
Type = HdHomerunHost.DeviceType,
|
||||
Url = url,
|
||||
DeviceId = response.DeviceID
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.Equals(existing.Url, url, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
existing.Url = url;
|
||||
await _liveTvManager.SaveTunerHost(existing).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error saving device", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private bool UriEquals(string savedUri, string location)
|
||||
{
|
||||
return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string NormalizeUrl(string url)
|
||||
{
|
||||
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url = "http://" + url;
|
||||
}
|
||||
|
||||
url = url.TrimEnd('/');
|
||||
|
||||
// Strip off the port
|
||||
return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
|
||||
}
|
||||
|
||||
private LiveTvOptions GetConfiguration()
|
||||
{
|
||||
return _config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
|
@ -30,8 +31,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IEnvironmentInfo _environment;
|
||||
|
||||
public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager)
|
||||
public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
|
@ -39,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
_appHost = appHost;
|
||||
_socketFactory = socketFactory;
|
||||
_networkManager = networkManager;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public string Name
|
||||
|
@ -56,7 +59,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
get { return "hdhomerun"; }
|
||||
}
|
||||
|
||||
private const string ChannelIdPrefix = "hdhr_";
|
||||
protected override string ChannelIdPrefix
|
||||
{
|
||||
get
|
||||
{
|
||||
return "hdhr_";
|
||||
}
|
||||
}
|
||||
|
||||
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||
{
|
||||
|
@ -124,10 +133,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
{
|
||||
DiscoverResponse response;
|
||||
if (_modelCache.TryGetValue(info.Url, out response))
|
||||
{
|
||||
if ((DateTime.UtcNow - response.DateQueried).TotalHours <= 12)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -135,8 +147,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
{
|
||||
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
|
||||
CancellationToken = cancellationToken,
|
||||
CacheLength = TimeSpan.FromDays(1),
|
||||
CacheMode = CacheMode.Unconditional,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
|
||||
|
@ -215,7 +225,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
var list = new List<LiveTvTunerInfo>();
|
||||
|
||||
foreach (var host in GetConfiguration().TunerHosts
|
||||
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -268,6 +278,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
public int HD { get; set; }
|
||||
}
|
||||
|
||||
protected EncodingOptions GetEncodingOptions()
|
||||
{
|
||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
||||
}
|
||||
|
||||
private string GetHdHrIdFromChannelId(string channelId)
|
||||
{
|
||||
return channelId.Split('_')[1];
|
||||
}
|
||||
|
||||
private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile)
|
||||
{
|
||||
int? width = null;
|
||||
|
@ -355,14 +375,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
nal = "0";
|
||||
}
|
||||
|
||||
var url = GetApiUrl(info, true) + "/auto/v" + channelId;
|
||||
|
||||
// If raw was used, the tuner doesn't support params
|
||||
if (!string.IsNullOrWhiteSpace(profile)
|
||||
&& !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url += "?transcode=" + profile;
|
||||
}
|
||||
var url = GetApiUrl(info, false);
|
||||
|
||||
var id = profile;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
|
@ -371,92 +384,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
id += "_" + url.GetMD5().ToString("N");
|
||||
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = url,
|
||||
Protocol = MediaProtocol.Http,
|
||||
MediaStreams = new List<MediaStream>
|
||||
{
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Video,
|
||||
// Set the index to -1 because we don't know the exact index of the video stream within the container
|
||||
Index = -1,
|
||||
IsInterlaced = isInterlaced,
|
||||
Codec = videoCodec,
|
||||
Width = width,
|
||||
Height = height,
|
||||
BitRate = videoBitrate,
|
||||
NalLengthSize = nal
|
||||
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Audio,
|
||||
// Set the index to -1 because we don't know the exact index of the audio stream within the container
|
||||
Index = -1,
|
||||
Codec = audioCodec,
|
||||
BitRate = audioBitrate
|
||||
}
|
||||
},
|
||||
RequiresOpening = true,
|
||||
RequiresClosing = false,
|
||||
BufferMs = 0,
|
||||
Container = "ts",
|
||||
Id = id,
|
||||
SupportsDirectPlay = false,
|
||||
SupportsDirectStream = true,
|
||||
SupportsTranscoding = true,
|
||||
IsInfiniteStream = true
|
||||
};
|
||||
|
||||
mediaSource.InferTotalBitrate();
|
||||
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
protected EncodingOptions GetEncodingOptions()
|
||||
{
|
||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
||||
}
|
||||
|
||||
private string GetHdHrIdFromChannelId(string channelId)
|
||||
{
|
||||
return channelId.Split('_')[1];
|
||||
}
|
||||
|
||||
private MediaSourceInfo GetLegacyMediaSource(TunerHostInfo info, string channelId, ChannelInfo channel)
|
||||
{
|
||||
int? width = null;
|
||||
int? height = null;
|
||||
bool isInterlaced = true;
|
||||
string videoCodec = null;
|
||||
string audioCodec = null;
|
||||
|
||||
int? videoBitrate = null;
|
||||
int? audioBitrate = null;
|
||||
|
||||
if (channel != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(videoCodec))
|
||||
{
|
||||
videoCodec = channel.VideoCodec;
|
||||
}
|
||||
audioCodec = channel.AudioCodec;
|
||||
}
|
||||
|
||||
// normalize
|
||||
if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
videoCodec = "mpeg2video";
|
||||
}
|
||||
|
||||
string nal = null;
|
||||
|
||||
var url = GetApiUrl(info, false);
|
||||
var id = channelId;
|
||||
id += "_" + url.GetMD5().ToString("N");
|
||||
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = url,
|
||||
|
@ -520,7 +447,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
if (isLegacyTuner)
|
||||
{
|
||||
list.Add(GetLegacyMediaSource(info, hdhrId, channelInfo));
|
||||
list.Add(GetMediaSource(info, hdhrId, channelInfo, "native"));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -559,26 +486,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
return list;
|
||||
}
|
||||
|
||||
protected override bool IsValidChannelId(string channelId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(channelId))
|
||||
{
|
||||
throw new ArgumentNullException("channelId");
|
||||
}
|
||||
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new ArgumentException("Channel not found");
|
||||
}
|
||||
var hdhrId = GetHdHrIdFromChannelId(channelId);
|
||||
|
||||
var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
|
||||
|
@ -586,30 +499,40 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
var hdhomerunChannel = channelInfo as HdHomerunChannelInfo;
|
||||
|
||||
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
|
||||
{
|
||||
var mediaSource = GetLegacyMediaSource(info, hdhrId, channelInfo);
|
||||
var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
|
||||
var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner)
|
||||
{
|
||||
return new HdHomerunUdpStream(mediaSource, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Url), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
|
||||
}
|
||||
else
|
||||
|
||||
// The UDP method is not working reliably on OSX, and on BSD it hasn't been tested yet
|
||||
var enableHttpStream = _environment.OperatingSystem == OperatingSystem.OSX ||
|
||||
_environment.OperatingSystem == OperatingSystem.BSD;
|
||||
enableHttpStream = true;
|
||||
if (enableHttpStream)
|
||||
{
|
||||
var mediaSource = GetMediaSource(info, hdhrId, channelInfo, profile);
|
||||
//var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
mediaSource.Protocol = MediaProtocol.Http;
|
||||
|
||||
var httpUrl = GetApiUrl(info, true) + "/auto/v" + hdhrId;
|
||||
|
||||
// If raw was used, the tuner doesn't support params
|
||||
if (!string.IsNullOrWhiteSpace(profile)
|
||||
&& !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpUrl += "?transcode=" + profile;
|
||||
}
|
||||
mediaSource.Path = httpUrl;
|
||||
|
||||
return new HdHomerunHttpStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
|
||||
//return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
|
||||
}
|
||||
|
||||
return new HdHomerunUdpStream(mediaSource, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number), modelInfo.TunerCount, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager);
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
if (!info.IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache.Clear();
|
||||
|
@ -651,6 +574,83 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
public string BaseURL { get; set; }
|
||||
public string LineupURL { get; set; }
|
||||
public int TunerCount { get; set; }
|
||||
|
||||
public DateTime DateQueried { get; set; }
|
||||
|
||||
public DiscoverResponse()
|
||||
{
|
||||
DateQueried = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
|
||||
var list = new List<TunerHostInfo>();
|
||||
|
||||
// Create udp broadcast discovery message
|
||||
byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 };
|
||||
using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0))
|
||||
{
|
||||
// Need a way to set the Receive timeout on the socket otherwise this might never timeout?
|
||||
try
|
||||
{
|
||||
await udpClient.SendAsync(discBytes, discBytes.Length, new IpEndPointInfo(new IpAddressInfo("255.255.255.255", IpAddressFamily.InterNetwork), 65001), cancellationToken);
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
|
||||
var deviceIp = response.RemoteEndPoint.IpAddress.Address;
|
||||
|
||||
// check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
|
||||
if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
|
||||
{
|
||||
var deviceAddress = "http://" + deviceIp;
|
||||
|
||||
var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (info != null)
|
||||
{
|
||||
list.Add(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Socket timeout indicates all messages have been received.
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var hostInfo = new TunerHostInfo
|
||||
{
|
||||
Type = Type,
|
||||
Url = url
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
hostInfo.DeviceId = modelInfo.DeviceID;
|
||||
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
||||
|
||||
return hostInfo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// logged at lower levels
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,7 +120,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
// send url to start streaming
|
||||
await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false);
|
||||
_logger.Info("Opened HDHR UDP stream from {0}", remoteAddress);
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
|
@ -131,12 +130,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
onStarted = () => openTaskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
|
||||
var stream = new UdpClientStream(udpClient);
|
||||
await _multicastStream.CopyUntilCancelled(stream, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
await _multicastStream.CopyUntilCancelled(new UdpClientStream(udpClient), onStarted, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.Info("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -155,7 +155,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
|
||||
await hdHomerunManager.StopStreaming().ConfigureAwait(false);
|
||||
udpClient.Dispose();
|
||||
_liveStreamTaskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
get { return "M3U Tuner"; }
|
||||
}
|
||||
|
||||
private const string ChannelIdPrefix = "m3u_";
|
||||
|
||||
protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await new M3uParser(Logger, _fileSystem, _httpClient, _appHost).Parse(info.Url, ChannelIdPrefix, info.Id, !info.EnableTvgId, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -87,16 +85,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
}
|
||||
}
|
||||
|
||||
protected override bool IsValidChannelId(string channelId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(channelId))
|
||||
{
|
||||
throw new ArgumentNullException("channelId");
|
||||
}
|
||||
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = info.Url.GetMD5().ToString("N");
|
||||
|
@ -176,5 +164,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(new List<TunerHostInfo>());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
|
@ -33,14 +34,74 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
var allStreams = _outputStreams.ToList();
|
||||
|
||||
if (allStreams.Count == 1)
|
||||
{
|
||||
await allStreams[0].Value.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] copy = new byte[bytesRead];
|
||||
Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
|
||||
|
||||
var allStreams = _outputStreams.ToList();
|
||||
foreach (var stream in allStreams)
|
||||
{
|
||||
stream.Value.Queue(copy);
|
||||
stream.Value.Queue(copy, 0, copy.Length);
|
||||
}
|
||||
}
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
var onStartedCopy = onStarted;
|
||||
onStarted = null;
|
||||
Task.Run(onStartedCopy);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int RtpHeaderBytes = 12;
|
||||
public async Task CopyUntilCancelled(ISocket udpClient, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var receiveToken = cancellationToken;
|
||||
|
||||
// On the first connection attempt, put a timeout to avoid being stuck indefinitely in the event of failure
|
||||
if (onStarted != null)
|
||||
{
|
||||
receiveToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(5000).Token, cancellationToken).Token;
|
||||
}
|
||||
|
||||
var data = await udpClient.ReceiveAsync(receiveToken).ConfigureAwait(false);
|
||||
var bytesRead = data.ReceivedBytes - RtpHeaderBytes;
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
var allStreams = _outputStreams.ToList();
|
||||
|
||||
if (allStreams.Count == 1)
|
||||
{
|
||||
await allStreams[0].Value.WriteAsync(data.Buffer, 0, bytesRead).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] copy = new byte[bytesRead];
|
||||
Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, copy, 0, bytesRead);
|
||||
|
||||
foreach (var stream in allStreams)
|
||||
{
|
||||
stream.Value.Queue(copy, 0, copy.Length);
|
||||
}
|
||||
}
|
||||
|
||||
if (onStarted != null)
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
public class QueueStream
|
||||
{
|
||||
private readonly Stream _outputStream;
|
||||
private readonly ConcurrentQueue<byte[]> _queue = new ConcurrentQueue<byte[]>();
|
||||
private readonly ConcurrentQueue<Tuple<byte[], int, int>> _queue = new ConcurrentQueue<Tuple<byte[], int, int>>();
|
||||
private CancellationToken _cancellationToken;
|
||||
public TaskCompletionSource<bool> TaskCompletion { get; private set; }
|
||||
|
||||
|
@ -28,9 +28,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
TaskCompletion = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
public void Queue(byte[] bytes)
|
||||
public void Queue(byte[] bytes, int offset, int count)
|
||||
{
|
||||
_queue.Enqueue(bytes);
|
||||
_queue.Enqueue(new Tuple<byte[], int, int>(bytes, offset, count));
|
||||
}
|
||||
|
||||
public void Start(CancellationToken cancellationToken)
|
||||
|
@ -39,17 +39,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
Task.Run(() => StartInternal());
|
||||
}
|
||||
|
||||
private byte[] Dequeue()
|
||||
private Tuple<byte[], int, int> Dequeue()
|
||||
{
|
||||
byte[] bytes;
|
||||
if (_queue.TryDequeue(out bytes))
|
||||
Tuple<byte[], int, int> result;
|
||||
if (_queue.TryDequeue(out result))
|
||||
{
|
||||
return bytes;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnClosed()
|
||||
{
|
||||
GC.Collect();
|
||||
if (OnFinished != null)
|
||||
{
|
||||
OnFinished(this);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task WriteAsync(byte[] bytes, int offset, int count)
|
||||
{
|
||||
//return _outputStream.WriteAsync(bytes, offset, count, cancellationToken);
|
||||
var cancellationToken = _cancellationToken;
|
||||
|
||||
try
|
||||
{
|
||||
await _outputStream.WriteAsync(bytes, offset, count, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Debug("QueueStream cancelled");
|
||||
TaskCompletion.TrySetCanceled();
|
||||
OnClosed();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in QueueStream", ex);
|
||||
TaskCompletion.TrySetException(ex);
|
||||
OnClosed();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartInternal()
|
||||
{
|
||||
var cancellationToken = _cancellationToken;
|
||||
|
@ -58,10 +90,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
var bytes = Dequeue();
|
||||
if (bytes != null)
|
||||
var result = Dequeue();
|
||||
if (result != null)
|
||||
{
|
||||
await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
await _outputStream.WriteAsync(result.Item1, result.Item2, result.Item3, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -81,10 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
}
|
||||
finally
|
||||
{
|
||||
if (OnFinished != null)
|
||||
{
|
||||
OnFinished(this);
|
||||
}
|
||||
OnClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -408,6 +408,7 @@ namespace Emby.Server.Implementations.Localization
|
|||
new LocalizatonOption{ Name="Italian", Value="it"},
|
||||
new LocalizatonOption{ Name="Kazakh", Value="kk"},
|
||||
new LocalizatonOption{ Name="Norwegian Bokmål", Value="nb"},
|
||||
new LocalizatonOption{ Name="Persian", Value="fa"},
|
||||
new LocalizatonOption{ Name="Polish", Value="pl"},
|
||||
new LocalizatonOption{ Name="Portuguese (Brazil)", Value="pt-BR"},
|
||||
new LocalizatonOption{ Name="Portuguese (Portugal)", Value="pt-PT"},
|
||||
|
|
|
@ -11,6 +11,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -101,4 +102,35 @@ namespace Emby.Server.Implementations.Playlists
|
|||
//}
|
||||
}
|
||||
|
||||
public class GenreImageProvider : BaseDynamicImageProvider<Genre>
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public GenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item)
|
||||
{
|
||||
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Genres = new[] { item.Name },
|
||||
IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name },
|
||||
SortBy = new[] { ItemSortBy.Random },
|
||||
Limit = 4,
|
||||
Recursive = true,
|
||||
ImageTypes = new[] { ImageType.Primary }
|
||||
|
||||
}).ToList();
|
||||
|
||||
return Task.FromResult(GetFinalItems(items));
|
||||
}
|
||||
|
||||
//protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
|
||||
//{
|
||||
// return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
|
||||
//}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -301,10 +301,12 @@ namespace Emby.Server.Implementations.Security
|
|||
|
||||
if (reg.registered)
|
||||
{
|
||||
_logger.Info("Registered for feature {0}", feature);
|
||||
LicenseFile.AddRegCheck(feature, reg.expDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Info("Not registered for feature {0}", feature);
|
||||
LicenseFile.RemoveRegCheck(feature);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,45 +12,6 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
public static class ResponseHelper
|
||||
{
|
||||
private static async Task<bool> WriteToOutputStream(IResponse response, object result)
|
||||
{
|
||||
var asyncStreamWriter = result as IAsyncStreamWriter;
|
||||
if (asyncStreamWriter != null)
|
||||
{
|
||||
await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
var streamWriter = result as IStreamWriter;
|
||||
if (streamWriter != null)
|
||||
{
|
||||
streamWriter.WriteTo(response.OutputStream);
|
||||
return true;
|
||||
}
|
||||
|
||||
var stream = result as Stream;
|
||||
if (stream != null)
|
||||
{
|
||||
using (stream)
|
||||
{
|
||||
await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var bytes = result as byte[];
|
||||
if (bytes != null)
|
||||
{
|
||||
response.ContentType = "application/octet-stream";
|
||||
response.SetContentLength(bytes.Length);
|
||||
|
||||
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Task WriteToResponse(IResponse httpRes, IRequest httpReq, object result)
|
||||
{
|
||||
if (result == null)
|
||||
|
@ -141,16 +102,51 @@ namespace Emby.Server.Implementations.Services
|
|||
response.ContentType += "; charset=utf-8";
|
||||
}
|
||||
|
||||
var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false);
|
||||
if (writeToOutputStreamResult)
|
||||
var asyncStreamWriter = result as IAsyncStreamWriter;
|
||||
if (asyncStreamWriter != null)
|
||||
{
|
||||
await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var streamWriter = result as IStreamWriter;
|
||||
if (streamWriter != null)
|
||||
{
|
||||
streamWriter.WriteTo(response.OutputStream);
|
||||
return;
|
||||
}
|
||||
|
||||
var fileWriter = result as FileWriter;
|
||||
if (fileWriter != null)
|
||||
{
|
||||
await fileWriter.WriteToAsync(response, CancellationToken.None).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var stream = result as Stream;
|
||||
if (stream != null)
|
||||
{
|
||||
using (stream)
|
||||
{
|
||||
await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var bytes = result as byte[];
|
||||
if (bytes != null)
|
||||
{
|
||||
response.ContentType = "application/octet-stream";
|
||||
response.SetContentLength(bytes.Length);
|
||||
|
||||
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var responseText = result as string;
|
||||
if (responseText != null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(responseText);
|
||||
bytes = Encoding.UTF8.GetBytes(responseText);
|
||||
response.SetContentLength(bytes.Length);
|
||||
await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
return;
|
||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.TV
|
|||
// Avoid implicitly captured closure
|
||||
var episodes = GetNextUpEpisodes(request, user, items);
|
||||
|
||||
return GetResult(episodes, null, request);
|
||||
return GetResult(episodes, request);
|
||||
}
|
||||
|
||||
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, List<Folder> parentsFolders)
|
||||
|
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.TV
|
|||
// Avoid implicitly captured closure
|
||||
var episodes = GetNextUpEpisodes(request, user, items);
|
||||
|
||||
return GetResult(episodes, null, request);
|
||||
return GetResult(episodes, request);
|
||||
}
|
||||
|
||||
public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<string> seriesKeys)
|
||||
|
@ -163,8 +163,7 @@ namespace Emby.Server.Implementations.TV
|
|||
return false;
|
||||
})
|
||||
.Select(i => i.Item2())
|
||||
.Where(i => i != null)
|
||||
.Take(request.Limit ?? int.MaxValue);
|
||||
.Where(i => i != null);
|
||||
}
|
||||
|
||||
private string GetUniqueSeriesKey(BaseItem series)
|
||||
|
@ -232,24 +231,30 @@ namespace Emby.Server.Implementations.TV
|
|||
return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode);
|
||||
}
|
||||
|
||||
private QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, int? totalRecordLimit, NextUpQuery query)
|
||||
private QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)
|
||||
{
|
||||
var itemsArray = totalRecordLimit.HasValue ? items.Take(totalRecordLimit.Value).ToArray() : items.ToArray();
|
||||
var totalCount = itemsArray.Length;
|
||||
int totalCount = 0;
|
||||
|
||||
if (query.EnableTotalRecordCount)
|
||||
{
|
||||
var list = items.ToList();
|
||||
totalCount = list.Count;
|
||||
items = list;
|
||||
}
|
||||
|
||||
if (query.StartIndex.HasValue)
|
||||
{
|
||||
items = items.Skip(query.StartIndex.Value);
|
||||
}
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
itemsArray = itemsArray.Skip(query.StartIndex ?? 0).Take(query.Limit.Value).ToArray();
|
||||
}
|
||||
else if (query.StartIndex.HasValue)
|
||||
{
|
||||
itemsArray = itemsArray.Skip(query.StartIndex.Value).ToArray();
|
||||
items = items.Take(query.Limit.Value);
|
||||
}
|
||||
|
||||
return new QueryResult<BaseItem>
|
||||
{
|
||||
TotalRecordCount = totalCount,
|
||||
Items = itemsArray
|
||||
Items = items.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -203,19 +203,6 @@ namespace Emby.Server.Implementations.Udp
|
|||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops this instance.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_isDisposed = true;
|
||||
|
||||
if (_udpClient != null)
|
||||
{
|
||||
_udpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
|
@ -224,7 +211,12 @@ namespace Emby.Server.Implementations.Udp
|
|||
{
|
||||
if (dispose)
|
||||
{
|
||||
Stop();
|
||||
_isDisposed = true;
|
||||
|
||||
if (_udpClient != null)
|
||||
{
|
||||
_udpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,9 +239,13 @@ namespace Emby.Server.Implementations.Udp
|
|||
|
||||
try
|
||||
{
|
||||
await _udpClient.SendAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false);
|
||||
await _udpClient.SendWithLockAsync(bytes, bytes.Length, remoteEndPoint, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("Udp message sent to {0}", remoteEndPoint);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Emby.XmlTv" version="1.0.7" targetFramework="portable45-net45+win8" />
|
||||
<package id="MediaBrowser.Naming" version="1.0.4" targetFramework="portable45-net45+win8" />
|
||||
<package id="MediaBrowser.Naming" version="1.0.5" targetFramework="portable45-net45+win8" />
|
||||
<package id="SQLitePCL.pretty" version="1.1.0" targetFramework="portable45-net45+win8" />
|
||||
<package id="SQLitePCLRaw.core" version="1.1.2" targetFramework="portable45-net45+win8" />
|
||||
<package id="UniversalDetector" version="1.0.1" targetFramework="portable45-net45+win8" />
|
||||
|
|
|
@ -582,13 +582,13 @@ namespace MediaBrowser.Api.LiveTv
|
|||
}
|
||||
|
||||
[Route("/LiveTv/ListingProviders/Default", "GET")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class GetDefaultListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/LiveTv/ListingProviders", "POST", Summary = "Adds a listing provider")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class AddListingProvider : ListingsProviderInfo, IReturn<ListingsProviderInfo>
|
||||
{
|
||||
public bool ValidateLogin { get; set; }
|
||||
|
@ -596,7 +596,7 @@ namespace MediaBrowser.Api.LiveTv
|
|||
}
|
||||
|
||||
[Route("/LiveTv/ListingProviders", "DELETE", Summary = "Deletes a listing provider")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class DeleteListingProvider : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
|
@ -604,7 +604,7 @@ namespace MediaBrowser.Api.LiveTv
|
|||
}
|
||||
|
||||
[Route("/LiveTv/ListingProviders/Lineups", "GET", Summary = "Gets available lineups")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class GetLineups : IReturn<List<NameIdPair>>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Provider id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
|
@ -621,13 +621,13 @@ namespace MediaBrowser.Api.LiveTv
|
|||
}
|
||||
|
||||
[Route("/LiveTv/ListingProviders/SchedulesDirect/Countries", "GET", Summary = "Gets available lineups")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class GetSchedulesDirectCountries
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/LiveTv/ChannelMappingOptions")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class GetChannelMappingOptions
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
|
@ -635,7 +635,7 @@ namespace MediaBrowser.Api.LiveTv
|
|||
}
|
||||
|
||||
[Route("/LiveTv/ChannelMappings")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
[Authenticated]
|
||||
public class SetChannelMapping
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Provider id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
|
@ -660,20 +660,6 @@ namespace MediaBrowser.Api.LiveTv
|
|||
public string Feature { get; set; }
|
||||
}
|
||||
|
||||
[Route("/LiveTv/TunerHosts/Satip/IniMappings", "GET", Summary = "Gets available mappings")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
public class GetSatIniMappings : IReturn<List<NameValuePair>>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/LiveTv/TunerHosts/Satip/ChannelScan", "GET", Summary = "Scans for available channels")]
|
||||
[Authenticated(AllowBeforeStartupWizard = true)]
|
||||
public class GetSatChannnelScanResult : TunerHostInfo
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/LiveTv/LiveStreamFiles/{Id}/stream.{Container}", "GET", Summary = "Gets a live tv channel")]
|
||||
public class GetLiveStreamFile
|
||||
{
|
||||
|
@ -687,6 +673,20 @@ namespace MediaBrowser.Api.LiveTv
|
|||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/LiveTv/TunerHosts/Types", "GET")]
|
||||
[Authenticated]
|
||||
public class GetTunerHostTypes : IReturn<List<NameIdPair>>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Route("/LiveTv/Tuners/Discvover", "GET")]
|
||||
[Authenticated]
|
||||
public class DiscoverTuners : IReturn<List<TunerHostInfo>>
|
||||
{
|
||||
public bool NewDevicesOnly { get; set; }
|
||||
}
|
||||
|
||||
public class LiveTvService : BaseApiService
|
||||
{
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
|
@ -712,6 +712,12 @@ namespace MediaBrowser.Api.LiveTv
|
|||
_sessionContext = sessionContext;
|
||||
}
|
||||
|
||||
public object Get(GetTunerHostTypes request)
|
||||
{
|
||||
var list = _liveTvManager.GetTunerHostTypes();
|
||||
return ToOptimizedResult(list);
|
||||
}
|
||||
|
||||
public object Get(GetLiveRecordingFile request)
|
||||
{
|
||||
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(request.Id);
|
||||
|
@ -731,6 +737,12 @@ namespace MediaBrowser.Api.LiveTv
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<object> Get(DiscoverTuners request)
|
||||
{
|
||||
var result = await _liveTvManager.DiscoverTuners(request.NewDevicesOnly, CancellationToken.None).ConfigureAwait(false);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetLiveStreamFile request)
|
||||
{
|
||||
var directStreamProvider = (await _liveTvManager.GetEmbyTvLiveStream(request.Id).ConfigureAwait(false)) as IDirectStreamProvider;
|
||||
|
@ -749,13 +761,6 @@ namespace MediaBrowser.Api.LiveTv
|
|||
return ToOptimizedResult(new ListingsProviderInfo());
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetSatChannnelScanResult request)
|
||||
{
|
||||
var result = await _liveTvManager.GetSatChannelScanResult(request, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetLiveTvRegistrationInfo request)
|
||||
{
|
||||
var result = await _liveTvManager.GetRegistrationInfo(request.Feature).ConfigureAwait(false);
|
||||
|
@ -803,11 +808,6 @@ namespace MediaBrowser.Api.LiveTv
|
|||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Get(GetSatIniMappings request)
|
||||
{
|
||||
return ToOptimizedResult(_liveTvManager.GetSatIniMappings());
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetSchedulesDirectCountries request)
|
||||
{
|
||||
// https://json.schedulesdirect.org/20141201/available/countries
|
||||
|
|
|
@ -289,8 +289,10 @@ namespace MediaBrowser.Api.Playback
|
|||
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
|
||||
//process.BeginOutputReadLine();
|
||||
|
||||
state.TranscodingJob = transcodingJob;
|
||||
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
var task = Task.Run(() => StartStreamingLog(transcodingJob, state, process.StandardError.BaseStream, state.LogFileStream));
|
||||
new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream);
|
||||
|
||||
// Wait for the file to exist before proceeeding
|
||||
while (!FileSystem.FileExists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
|
||||
|
@ -340,134 +342,6 @@ namespace MediaBrowser.Api.Playback
|
|||
// string.Equals(GetVideoEncoder(state), "libx264", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var reader = new StreamReader(source))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
ParseLogLine(line, transcodingJob, state);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
||||
|
||||
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await target.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error reading ffmpeg log", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseLogLine(string line, TranscodingJob transcodingJob, StreamState state)
|
||||
{
|
||||
float? framerate = null;
|
||||
double? percent = null;
|
||||
TimeSpan? transcodingPosition = null;
|
||||
long? bytesTranscoded = null;
|
||||
int? bitRate = null;
|
||||
|
||||
var parts = line.Split(' ');
|
||||
|
||||
var totalMs = state.RunTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
|
||||
: 0;
|
||||
|
||||
var startMs = state.Request.StartTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(state.Request.StartTimeTicks.Value).TotalMilliseconds
|
||||
: 0;
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) &&
|
||||
(i + 1 < parts.Length))
|
||||
{
|
||||
var rate = parts[i + 1];
|
||||
float val;
|
||||
|
||||
if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
|
||||
{
|
||||
framerate = val;
|
||||
}
|
||||
}
|
||||
else if (state.RunTimeTicks.HasValue &&
|
||||
part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var time = part.Split(new[] { '=' }, 2).Last();
|
||||
TimeSpan val;
|
||||
|
||||
if (TimeSpan.TryParse(time, UsCulture, out val))
|
||||
{
|
||||
var currentMs = startMs + val.TotalMilliseconds;
|
||||
|
||||
var percentVal = currentMs / totalMs;
|
||||
percent = 100 * percentVal;
|
||||
|
||||
transcodingPosition = val;
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var size = part.Split(new[] { '=' }, 2).Last();
|
||||
|
||||
int? scale = null;
|
||||
if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
scale = 1024;
|
||||
size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (scale.HasValue)
|
||||
{
|
||||
long val;
|
||||
|
||||
if (long.TryParse(size, NumberStyles.Any, UsCulture, out val))
|
||||
{
|
||||
bytesTranscoded = val * scale.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var rate = part.Split(new[] { '=' }, 2).Last();
|
||||
|
||||
int? scale = null;
|
||||
if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
scale = 1024;
|
||||
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (scale.HasValue)
|
||||
{
|
||||
float val;
|
||||
|
||||
if (float.TryParse(rate, NumberStyles.Any, UsCulture, out val))
|
||||
{
|
||||
bitRate = (int)Math.Ceiling(val * scale.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (framerate.HasValue || percent.HasValue)
|
||||
{
|
||||
ApiEntryPoint.Instance.ReportTranscodingProgress(transcodingJob, state, transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the exited.
|
||||
/// </summary>
|
||||
|
@ -697,6 +571,20 @@ namespace MediaBrowser.Api.Playback
|
|||
{
|
||||
request.SubtitleCodec = val;
|
||||
}
|
||||
else if (i == 31)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
else if (i == 32)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,9 +41,16 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
/// <summary>
|
||||
/// Gets the segment file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected abstract string GetSegmentFileExtension(StreamState state);
|
||||
protected string GetSegmentFileExtension(StreamRequest request)
|
||||
{
|
||||
var segmentContainer = request.SegmentContainer;
|
||||
if (!string.IsNullOrWhiteSpace(segmentContainer))
|
||||
{
|
||||
return "." + segmentContainer;
|
||||
}
|
||||
|
||||
return ".ts";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the transcoding job.
|
||||
|
@ -103,8 +110,11 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
throw;
|
||||
}
|
||||
|
||||
var waitForSegments = state.SegmentLength >= 10 ? 2 : 3;
|
||||
await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
var minSegments = state.MinSegments;
|
||||
if (minSegments > 0)
|
||||
{
|
||||
await WaitForMinimumSegmentCount(playlist, minSegments, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
@ -258,14 +268,22 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
"hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
||||
}
|
||||
|
||||
var useGenericSegmenter = false;
|
||||
var useGenericSegmenter = true;
|
||||
if (useGenericSegmenter)
|
||||
{
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
|
||||
|
||||
var timeDeltaParam = String.Empty;
|
||||
|
||||
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
||||
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
segmentFormat = "mpegts";
|
||||
}
|
||||
|
||||
baseUrlParam = string.Format("\"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath));
|
||||
|
||||
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_entry_prefix {12} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
||||
inputModifier,
|
||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
||||
threads,
|
||||
|
@ -276,7 +294,9 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
startNumberParam,
|
||||
outputPath,
|
||||
outputTsArg,
|
||||
timeDeltaParam
|
||||
timeDeltaParam,
|
||||
segmentFormat,
|
||||
baseUrlParam
|
||||
).Trim();
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
{
|
||||
}
|
||||
|
||||
[Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")]
|
||||
[Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
|
||||
public class GetHlsVideoSegment : VideoStreamRequest
|
||||
{
|
||||
public string PlaylistId { get; set; }
|
||||
|
@ -77,8 +77,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
public string SegmentId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.aac", "GET")]
|
||||
[Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")]
|
||||
[Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
|
||||
public class GetHlsAudioSegment : StreamRequest
|
||||
{
|
||||
public string PlaylistId { get; set; }
|
||||
|
@ -158,7 +157,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
|
||||
|
||||
var segmentExtension = GetSegmentFileExtension(state);
|
||||
var segmentExtension = GetSegmentFileExtension(state.Request);
|
||||
|
||||
TranscodingJob job = null;
|
||||
|
||||
|
@ -420,7 +419,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
var filename = Path.GetFileNameWithoutExtension(playlist);
|
||||
|
||||
return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state));
|
||||
return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state.Request));
|
||||
}
|
||||
|
||||
private async Task<object> GetSegmentResult(StreamState state, string playlistPath,
|
||||
|
@ -740,7 +739,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
name,
|
||||
index.ToString(UsCulture),
|
||||
GetSegmentFileExtension(isOutputVideo),
|
||||
GetSegmentFileExtension(request),
|
||||
queryString));
|
||||
|
||||
index++;
|
||||
|
@ -848,7 +847,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
|
||||
|
||||
args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
|
||||
args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
|
||||
|
||||
//args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
|
||||
|
||||
|
@ -897,7 +896,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
if (useGenericSegmenter)
|
||||
{
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
|
||||
|
||||
var timeDeltaParam = String.Empty;
|
||||
|
||||
|
@ -907,7 +906,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
timeDeltaParam = string.Format("-segment_time_delta -{0}", startTime);
|
||||
}
|
||||
|
||||
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
||||
var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
segmentFormat = "mpegts";
|
||||
}
|
||||
|
||||
return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
|
||||
inputModifier,
|
||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
||||
threads,
|
||||
|
@ -918,7 +923,8 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
startNumberParam,
|
||||
outputPath,
|
||||
outputTsArg,
|
||||
timeDeltaParam
|
||||
timeDeltaParam,
|
||||
segmentFormat
|
||||
).Trim();
|
||||
}
|
||||
|
||||
|
@ -935,20 +941,5 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
outputPath
|
||||
).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string GetSegmentFileExtension(StreamState state)
|
||||
{
|
||||
return GetSegmentFileExtension(state.IsOutputVideo);
|
||||
}
|
||||
|
||||
protected string GetSegmentFileExtension(bool isOutputVideo)
|
||||
{
|
||||
return isOutputVideo ? ".ts" : ".ts";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,7 +63,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
/// <summary>
|
||||
/// Class GetHlsVideoSegment
|
||||
/// </summary>
|
||||
[Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
|
||||
[Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}", "GET")]
|
||||
public class GetHlsVideoSegmentLegacy : VideoStreamRequest
|
||||
{
|
||||
public string PlaylistId { get; set; }
|
||||
|
@ -109,11 +109,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
public Task<object> Get(GetHlsVideoSegmentLegacy request)
|
||||
{
|
||||
var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
|
||||
file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file);
|
||||
|
||||
var transcodeFolderPath = _config.ApplicationPaths.TranscodingTempPath;
|
||||
file = Path.Combine(transcodeFolderPath, file);
|
||||
|
||||
var normalizedPlaylistId = request.PlaylistId;
|
||||
|
||||
var playlistPath = _fileSystem.GetFilePaths(_config.ApplicationPaths.TranscodingTempPath)
|
||||
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
|
||||
.FirstOrDefault(i => string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) && i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
|
||||
|
||||
return GetFileResult(file, playlistPath);
|
||||
|
|
|
@ -99,7 +99,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
|
||||
args += " " + EncodingHelper.GetVideoQualityParam(state, EncodingHelper.GetH264Encoder(state, encodingOptions), encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
|
||||
args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg;
|
||||
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
|
@ -118,16 +118,6 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment file extension.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string GetSegmentFileExtension(StreamState state)
|
||||
{
|
||||
return ".ts";
|
||||
}
|
||||
|
||||
public VideoHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
SetDeviceSpecificData(item, result.MediaSource, profile, authInfo, request.MaxStreamingBitrate,
|
||||
request.StartTimeTicks ?? 0, result.MediaSource.Id, request.AudioStreamIndex,
|
||||
request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true);
|
||||
request.SubtitleStreamIndex, request.MaxAudioChannels, request.PlaySessionId, request.UserId, true, true, true, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -169,7 +169,7 @@ namespace MediaBrowser.Api.Playback
|
|||
{
|
||||
var mediaSourceId = request.MediaSourceId;
|
||||
|
||||
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.EnableDirectStream, request.EnableTranscoding);
|
||||
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate ?? profile.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex, request.MaxAudioChannels, request.UserId, request.EnableDirectPlay, request.ForceDirectPlayRemoteMediaSource, request.EnableDirectStream, request.EnableTranscoding);
|
||||
}
|
||||
|
||||
return info;
|
||||
|
@ -253,6 +253,7 @@ namespace MediaBrowser.Api.Playback
|
|||
int? maxAudioChannels,
|
||||
string userId,
|
||||
bool enableDirectPlay,
|
||||
bool forceDirectPlayRemoteMediaSource,
|
||||
bool enableDirectStream,
|
||||
bool enableTranscoding)
|
||||
{
|
||||
|
@ -260,7 +261,7 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
foreach (var mediaSource in result.MediaSources)
|
||||
{
|
||||
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, enableDirectStream, enableTranscoding);
|
||||
SetDeviceSpecificData(item, mediaSource, profile, auth, maxBitrate, startTimeTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, maxAudioChannels, result.PlaySessionId, userId, enableDirectPlay, forceDirectPlayRemoteMediaSource, enableDirectStream, enableTranscoding);
|
||||
}
|
||||
|
||||
SortMediaSources(result, maxBitrate);
|
||||
|
@ -279,6 +280,7 @@ namespace MediaBrowser.Api.Playback
|
|||
string playSessionId,
|
||||
string userId,
|
||||
bool enableDirectPlay,
|
||||
bool forceDirectPlayRemoteMediaSource,
|
||||
bool enableDirectStream,
|
||||
bool enableTranscoding)
|
||||
{
|
||||
|
@ -317,6 +319,11 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
|
||||
if (mediaSource.SupportsDirectPlay)
|
||||
{
|
||||
if (mediaSource.IsRemote && forceDirectPlayRemoteMediaSource)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
var supportsDirectStream = mediaSource.SupportsDirectStream;
|
||||
|
||||
|
@ -357,6 +364,7 @@ namespace MediaBrowser.Api.Playback
|
|||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaSource.SupportsDirectStream)
|
||||
{
|
||||
|
|
|
@ -15,9 +15,6 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
|
@ -298,7 +295,8 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
responseHeaders["Accept-Ranges"] = "none";
|
||||
}
|
||||
|
||||
if (response.ContentLength.HasValue)
|
||||
// Seeing cases of -1 here
|
||||
if (response.ContentLength.HasValue && response.ContentLength.Value >= 0)
|
||||
{
|
||||
responseHeaders["Content-Length"] = response.ContentLength.Value.ToString(UsCulture);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
|
@ -7,15 +6,8 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Progressive
|
||||
|
@ -100,181 +92,7 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
{
|
||||
var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
|
||||
|
||||
// Get the output codec name
|
||||
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
|
||||
var format = string.Empty;
|
||||
var keyFrame = string.Empty;
|
||||
|
||||
if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
||||
format = " -f mp4 -movflags frag_keyframe+empty_moov";
|
||||
}
|
||||
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
|
||||
|
||||
var subtitleArguments = state.SubtitleStream != null &&
|
||||
state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed
|
||||
? GetSubtitleArguments(state)
|
||||
: string.Empty;
|
||||
|
||||
return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
||||
keyFrame,
|
||||
EncodingHelper.GetMapArgs(state),
|
||||
GetVideoArguments(state, videoCodec),
|
||||
threads,
|
||||
GetAudioArguments(state),
|
||||
subtitleArguments,
|
||||
format,
|
||||
outputPath
|
||||
).Trim();
|
||||
}
|
||||
|
||||
private string GetSubtitleArguments(StreamState state)
|
||||
{
|
||||
var format = state.SupportedSubtitleCodecs.FirstOrDefault();
|
||||
string codec;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "copy";
|
||||
}
|
||||
else
|
||||
{
|
||||
codec = format;
|
||||
}
|
||||
|
||||
return " -codec:s:0 " + codec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets video arguments to pass to ffmpeg
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="videoCodec">The video codec.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetVideoArguments(StreamState state, string videoCodec)
|
||||
{
|
||||
var args = "-codec:v:0 " + videoCodec;
|
||||
|
||||
if (state.EnableMpegtsM2TsMode)
|
||||
{
|
||||
args += " -mpegts_m2ts_mode 1";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -bsf:v h264_mp4toannexb";
|
||||
}
|
||||
|
||||
if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps)
|
||||
{
|
||||
args += " -copyts -avoid_negative_ts disabled -start_at_zero";
|
||||
}
|
||||
|
||||
if (!state.RunTimeTicks.HasValue)
|
||||
{
|
||||
args += " -flags -global_header -fflags +genpts";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
|
||||
5.ToString(UsCulture));
|
||||
|
||||
args += keyFrameArg;
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
var hasCopyTs = false;
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
var outputSizeParam = EncodingHelper.GetOutputSizeParam(state, videoCodec);
|
||||
args += outputSizeParam;
|
||||
hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps)
|
||||
{
|
||||
if (!hasCopyTs)
|
||||
{
|
||||
args += " -copyts";
|
||||
}
|
||||
args += " -avoid_negative_ts disabled -start_at_zero";
|
||||
}
|
||||
|
||||
var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
|
||||
var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, encodingOptions, GetDefaultH264Preset());
|
||||
|
||||
if (!string.IsNullOrEmpty(qualityParam))
|
||||
{
|
||||
args += " " + qualityParam.Trim();
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec);
|
||||
}
|
||||
|
||||
if (!state.RunTimeTicks.HasValue)
|
||||
{
|
||||
args += " -flags -global_header";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets audio arguments to pass to ffmpeg
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetAudioArguments(StreamState state)
|
||||
{
|
||||
// If the video doesn't have an audio stream, return a default.
|
||||
if (state.AudioStream == null && state.VideoStream != null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Get the output codec name
|
||||
var codec = EncodingHelper.GetAudioEncoder(state);
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
|
||||
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
// Add the number of audio channels
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
if (channels.HasValue)
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
args += " " + EncodingHelper.GetAudioFilterParam(state, ApiEntryPoint.Instance.GetEncodingOptions(), false);
|
||||
|
||||
return args;
|
||||
return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultH264Preset());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,8 +32,6 @@ namespace MediaBrowser.Api.Playback
|
|||
[ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public string SubtitleCodec { get; set; }
|
||||
|
||||
[ApiMember(Name = "DeviceProfileId", Description = "Optional. The dlna device profile id to utilize.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string DeviceProfileId { get; set; }
|
||||
|
||||
|
@ -41,6 +39,10 @@ namespace MediaBrowser.Api.Playback
|
|||
public string PlaySessionId { get; set; }
|
||||
public string LiveStreamId { get; set; }
|
||||
public string Tag { get; set; }
|
||||
public string SegmentContainer { get; set; }
|
||||
|
||||
public int? SegmentLength { get; set; }
|
||||
public int? MinSegments { get; set; }
|
||||
}
|
||||
|
||||
public class VideoStreamRequest : StreamRequest
|
||||
|
|
|
@ -60,6 +60,11 @@ namespace MediaBrowser.Api.Playback
|
|||
{
|
||||
get
|
||||
{
|
||||
if (Request.SegmentLength.HasValue)
|
||||
{
|
||||
return Request.SegmentLength.Value;
|
||||
}
|
||||
|
||||
if (string.Equals(OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var userAgent = UserAgent ?? string.Empty;
|
||||
|
@ -86,6 +91,19 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
public int MinSegments
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Request.MinSegments.HasValue)
|
||||
{
|
||||
return Request.MinSegments.Value;
|
||||
}
|
||||
|
||||
return SegmentLength >= 10 ? 2 : 3;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSegmentedLiveStream
|
||||
{
|
||||
get
|
||||
|
@ -102,7 +120,6 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
public List<string> SupportedSubtitleCodecs { get; set; }
|
||||
public string UserAgent { get; set; }
|
||||
public TranscodingJobType TranscodingType { get; set; }
|
||||
|
||||
|
@ -111,14 +128,12 @@ namespace MediaBrowser.Api.Playback
|
|||
{
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_logger = logger;
|
||||
SupportedSubtitleCodecs = new List<string>();
|
||||
TranscodingType = transcodingType;
|
||||
}
|
||||
|
||||
public string MimeType { get; set; }
|
||||
|
||||
public bool EstimateContentLength { get; set; }
|
||||
public bool EnableMpegtsM2TsMode { get; set; }
|
||||
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
|
||||
|
||||
public long? EncodingDurationTicks { get; set; }
|
||||
|
@ -139,6 +154,8 @@ namespace MediaBrowser.Api.Playback
|
|||
DisposeLiveStream();
|
||||
DisposeLogStream();
|
||||
DisposeIsoMount();
|
||||
|
||||
TranscodingJob = null;
|
||||
}
|
||||
|
||||
private void DisposeLogStream()
|
||||
|
@ -191,7 +208,6 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
|
||||
public string OutputFilePath { get; set; }
|
||||
public int? OutputAudioBitrate;
|
||||
|
||||
public string ActualOutputVideoCodec
|
||||
{
|
||||
|
@ -462,5 +478,11 @@ namespace MediaBrowser.Api.Playback
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public TranscodingJob TranscodingJob;
|
||||
public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
|
||||
{
|
||||
ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Connect;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -52,16 +49,14 @@ namespace MediaBrowser.Api
|
|||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IConnectManager _connectManager;
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, ILiveTvManager liveTvManager, IMediaEncoder mediaEncoder)
|
||||
public StartupWizardService(IServerConfigurationManager config, IServerApplicationHost appHost, IUserManager userManager, IConnectManager connectManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
_userManager = userManager;
|
||||
_connectManager = connectManager;
|
||||
_liveTvManager = liveTvManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
|
@ -92,20 +87,6 @@ namespace MediaBrowser.Api
|
|||
PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage
|
||||
};
|
||||
|
||||
var tvConfig = GetLiveTVConfiguration();
|
||||
|
||||
if (tvConfig.TunerHosts.Count > 0)
|
||||
{
|
||||
result.LiveTvTunerPath = tvConfig.TunerHosts[0].Url;
|
||||
result.LiveTvTunerType = tvConfig.TunerHosts[0].Type;
|
||||
}
|
||||
|
||||
if (tvConfig.ListingProviders.Count > 0)
|
||||
{
|
||||
result.LiveTvGuideProviderId = tvConfig.ListingProviders[0].Id;
|
||||
result.LiveTvGuideProviderType = tvConfig.ListingProviders[0].Type;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -120,6 +101,7 @@ namespace MediaBrowser.Api
|
|||
config.EnableSeriesPresentationUniqueKey = true;
|
||||
config.EnableLocalizedGuids = true;
|
||||
config.EnableSimpleArtistDetection = true;
|
||||
config.EnableNormalizedItemByNameIds = true;
|
||||
}
|
||||
|
||||
public void Post(UpdateStartupConfiguration request)
|
||||
|
@ -128,9 +110,6 @@ namespace MediaBrowser.Api
|
|||
_config.Configuration.MetadataCountryCode = request.MetadataCountryCode;
|
||||
_config.Configuration.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
|
||||
_config.SaveConfiguration();
|
||||
|
||||
var task = UpdateTuners(request);
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Get(GetStartupUser request)
|
||||
|
@ -165,51 +144,6 @@ namespace MediaBrowser.Api
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task UpdateTuners(UpdateStartupConfiguration request)
|
||||
{
|
||||
var config = GetLiveTVConfiguration();
|
||||
var save = false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.LiveTvTunerPath) ||
|
||||
string.IsNullOrWhiteSpace(request.LiveTvTunerType))
|
||||
{
|
||||
if (config.TunerHosts.Count > 0)
|
||||
{
|
||||
config.TunerHosts.Clear();
|
||||
save = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!config.TunerHosts.Any(i => string.Equals(i.Type, request.LiveTvTunerType, StringComparison.OrdinalIgnoreCase) && string.Equals(i.Url, request.LiveTvTunerPath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// Add tuner
|
||||
await _liveTvManager.SaveTunerHost(new TunerHostInfo
|
||||
{
|
||||
IsEnabled = true,
|
||||
Type = request.LiveTvTunerType,
|
||||
Url = request.LiveTvTunerPath
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (save)
|
||||
{
|
||||
SaveLiveTVConfiguration(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLiveTVConfiguration(LiveTvOptions config)
|
||||
{
|
||||
_config.SaveConfiguration("livetv", config);
|
||||
}
|
||||
|
||||
private LiveTvOptions GetLiveTVConfiguration()
|
||||
{
|
||||
return _config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
}
|
||||
|
||||
public class StartupConfiguration
|
||||
|
@ -217,10 +151,6 @@ namespace MediaBrowser.Api
|
|||
public string UICulture { get; set; }
|
||||
public string MetadataCountryCode { get; set; }
|
||||
public string PreferredMetadataLanguage { get; set; }
|
||||
public string LiveTvTunerType { get; set; }
|
||||
public string LiveTvTunerPath { get; set; }
|
||||
public string LiveTvGuideProviderId { get; set; }
|
||||
public string LiveTvGuideProviderType { get; set; }
|
||||
}
|
||||
|
||||
public class StartupInfo
|
||||
|
|
|
@ -72,6 +72,12 @@ namespace MediaBrowser.Api
|
|||
|
||||
[ApiMember(Name = "EnableUserData", Description = "Optional, include user data", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
|
||||
public bool? EnableUserData { get; set; }
|
||||
public bool EnableTotalRecordCount { get; set; }
|
||||
|
||||
public GetNextUpEpisodes()
|
||||
{
|
||||
EnableTotalRecordCount = true;
|
||||
}
|
||||
}
|
||||
|
||||
[Route("/Shows/Upcoming", "GET", Summary = "Gets a list of upcoming episodes")]
|
||||
|
@ -376,7 +382,8 @@ namespace MediaBrowser.Api
|
|||
ParentId = request.ParentId,
|
||||
SeriesId = request.SeriesId,
|
||||
StartIndex = request.StartIndex,
|
||||
UserId = request.UserId
|
||||
UserId = request.UserId,
|
||||
EnableTotalRecordCount = request.EnableTotalRecordCount
|
||||
});
|
||||
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
|
|
|
@ -289,7 +289,12 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
|
|
|
@ -118,7 +118,12 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
|
|
|
@ -96,7 +96,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
|
|
|
@ -108,7 +108,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
|
|
|
@ -206,6 +206,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
void SetImage(ItemImageInfo image, int index);
|
||||
|
||||
double? GetDefaultPrimaryImageAspectRatio();
|
||||
|
||||
int? ProductionYear { get; set; }
|
||||
}
|
||||
|
||||
public static class HasImagesExtensions
|
||||
|
|
|
@ -133,7 +133,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validFilename = normalizeName ?
|
||||
|
|
|
@ -114,7 +114,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
|
|
|
@ -614,7 +614,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
Timestamp = i.Timestamp,
|
||||
Type = type,
|
||||
PlayableStreamFileNames = i.PlayableStreamFileNames.ToList(),
|
||||
SupportsDirectStream = i.VideoType == VideoType.VideoFile
|
||||
SupportsDirectStream = i.VideoType == VideoType.VideoFile,
|
||||
IsRemote = i.IsShortcut
|
||||
};
|
||||
|
||||
if (info.Protocol == MediaProtocol.File)
|
||||
|
|
|
@ -122,7 +122,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName = true)
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
|
|
|
@ -376,19 +376,14 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Task.</returns>
|
||||
Task OnRecordingFileDeleted(BaseItem recording);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sat ini mappings.
|
||||
/// </summary>
|
||||
/// <returns>List<NameValuePair>.</returns>
|
||||
List<NameValuePair> GetSatIniMappings();
|
||||
|
||||
Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken);
|
||||
|
||||
Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
|
||||
Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
|
||||
|
||||
List<IListingsProvider> ListingProviders { get; }
|
||||
|
||||
List<NameIdPair> GetTunerHostTypes();
|
||||
Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
|
||||
|
||||
event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
|
||||
event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled;
|
||||
event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated;
|
||||
|
|
|
@ -44,6 +44,8 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<List<MediaSourceInfo>>.</returns>
|
||||
Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
|
||||
|
||||
Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken);
|
||||
}
|
||||
public interface IConfigurableTunerHost
|
||||
{
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
|
||||
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
|
||||
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
|
||||
<Compile Include="MediaEncoding\JobLogger.cs" />
|
||||
<Compile Include="MediaEncoding\MediaInfoRequest.cs" />
|
||||
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
|
||||
<Compile Include="Net\AuthenticatedAttribute.cs" />
|
||||
|
|
|
@ -154,6 +154,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
return "mpegts";
|
||||
}
|
||||
|
||||
// For these need to find out the ffmpeg names
|
||||
if (string.Equals(container, "m2ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -163,6 +164,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
return null;
|
||||
}
|
||||
if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
|
@ -179,12 +184,23 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
return null;
|
||||
}
|
||||
if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Seeing reported failures here, not sure yet if this is related to specfying input format
|
||||
if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// obviously don't do this for strm files
|
||||
if (string.Equals(container, "strm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
|
@ -663,7 +679,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
// h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
|
||||
// also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
|
||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
switch (level)
|
||||
|
@ -700,10 +715,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
break;
|
||||
}
|
||||
}
|
||||
// nvenc doesn't decode with param -level set ?!
|
||||
if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)){
|
||||
param += "";
|
||||
}
|
||||
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -level " + level;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -726,14 +746,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var request = state.BaseRequest;
|
||||
|
||||
if (videoStream.IsInterlaced)
|
||||
{
|
||||
if (request.DeInterlace)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (videoStream.IsAnamorphic ?? false)
|
||||
{
|
||||
if (request.RequireNonAnamorphic)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Can't stream copy if we're burning in subtitles
|
||||
if (request.SubtitleStreamIndex.HasValue)
|
||||
|
@ -1561,6 +1587,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
MediaSourceInfo mediaSource,
|
||||
string requestedUrl)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException("state");
|
||||
}
|
||||
if (mediaSource == null)
|
||||
{
|
||||
throw new ArgumentNullException("mediaSource");
|
||||
}
|
||||
|
||||
state.MediaPath = mediaSource.Path;
|
||||
state.InputProtocol = mediaSource.Protocol;
|
||||
state.InputContainer = mediaSource.Container;
|
||||
|
@ -1670,9 +1705,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
case "h264":
|
||||
if (_mediaEncoder.SupportsDecoder("h264_qsv"))
|
||||
{
|
||||
// qsv decoder does not support 10-bit input
|
||||
if ((state.VideoStream.BitDepth ?? 8) > 8)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return "-c:v h264_qsv ";
|
||||
}
|
||||
break;
|
||||
//case "hevc":
|
||||
//case "h265":
|
||||
// if (_mediaEncoder.SupportsDecoder("hevc_qsv"))
|
||||
// {
|
||||
// return "-c:v hevc_qsv ";
|
||||
// }
|
||||
// break;
|
||||
case "mpeg2video":
|
||||
if (_mediaEncoder.SupportsDecoder("mpeg2_qsv"))
|
||||
{
|
||||
|
@ -1715,5 +1762,187 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
return threads;
|
||||
}
|
||||
|
||||
public string GetSubtitleEmbedArguments(EncodingJobInfo state)
|
||||
{
|
||||
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var format = state.SupportedSubtitleCodecs.FirstOrDefault();
|
||||
string codec;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format) || string.Equals(format, state.SubtitleStream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "copy";
|
||||
}
|
||||
else
|
||||
{
|
||||
codec = format;
|
||||
}
|
||||
|
||||
// Muxing in dvbsub via either copy or -codec dvbsub does not seem to work
|
||||
// It doesn't throw any errors but vlc on android will not render them
|
||||
// They will need to be converted to an alternative format
|
||||
// TODO: This is incorrectly assuming that dvdsub will be supported by the player
|
||||
// The api will need to be expanded to accomodate this.
|
||||
if (string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
codec = "dvdsub";
|
||||
}
|
||||
|
||||
var args = " -codec:s:0 " + codec;
|
||||
|
||||
args += " -disposition:s:0 default";
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset)
|
||||
{
|
||||
// Get the output codec name
|
||||
var videoCodec = GetVideoEncoder(state, encodingOptions);
|
||||
|
||||
var format = string.Empty;
|
||||
var keyFrame = string.Empty;
|
||||
|
||||
if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) &&
|
||||
state.BaseRequest.Context == EncodingContext.Streaming)
|
||||
{
|
||||
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
||||
format = " -f mp4 -movflags frag_keyframe+empty_moov";
|
||||
}
|
||||
|
||||
var threads = GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var inputModifier = GetInputModifier(state, encodingOptions);
|
||||
|
||||
return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(state, encodingOptions),
|
||||
keyFrame,
|
||||
GetMapArgs(state),
|
||||
GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset),
|
||||
threads,
|
||||
GetProgressiveVideoAudioArguments(state, encodingOptions),
|
||||
GetSubtitleEmbedArguments(state),
|
||||
format,
|
||||
outputPath
|
||||
).Trim();
|
||||
}
|
||||
|
||||
public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset)
|
||||
{
|
||||
var args = "-codec:v:0 " + videoCodec;
|
||||
|
||||
if (state.EnableMpegtsM2TsMode)
|
||||
{
|
||||
args += " -mpegts_m2ts_mode 1";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.VideoStream != null && IsH264(state.VideoStream) && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -bsf:v h264_mp4toannexb";
|
||||
}
|
||||
|
||||
if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
|
||||
{
|
||||
args += " -copyts -avoid_negative_ts disabled -start_at_zero";
|
||||
}
|
||||
|
||||
if (!state.RunTimeTicks.HasValue)
|
||||
{
|
||||
args += " -flags -global_header -fflags +genpts";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
|
||||
5.ToString(_usCulture));
|
||||
|
||||
args += keyFrameArg;
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.BaseRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
var hasCopyTs = false;
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
var outputSizeParam = GetOutputSizeParam(state, videoCodec);
|
||||
args += outputSizeParam;
|
||||
hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps)
|
||||
{
|
||||
if (!hasCopyTs)
|
||||
{
|
||||
args += " -copyts";
|
||||
}
|
||||
args += " -avoid_negative_ts disabled -start_at_zero";
|
||||
}
|
||||
|
||||
var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset);
|
||||
|
||||
if (!string.IsNullOrEmpty(qualityParam))
|
||||
{
|
||||
args += " " + qualityParam.Trim();
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += GetGraphicalSubtitleParam(state, videoCodec);
|
||||
}
|
||||
|
||||
if (!state.RunTimeTicks.HasValue)
|
||||
{
|
||||
args += " -flags -global_header";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public string GetProgressiveVideoAudioArguments(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||
{
|
||||
// If the video doesn't have an audio stream, return a default.
|
||||
if (state.AudioStream == null && state.VideoStream != null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Get the output codec name
|
||||
var codec = GetAudioEncoder(state);
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
|
||||
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
// Add the number of audio channels
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
if (channels.HasValue)
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(_usCulture);
|
||||
}
|
||||
|
||||
args += " " + GetAudioFilterParam(state, encodingOptions, false);
|
||||
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ using MediaBrowser.Model.MediaInfo;
|
|||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// For now, a common base class until the API and MediaEncoding classes are unified
|
||||
public class EncodingJobInfo
|
||||
public abstract class EncodingJobInfo
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
public int? OutputVideoBitrate { get; set; }
|
||||
public MediaStream SubtitleStream { get; set; }
|
||||
public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
|
||||
public List<string> SupportedSubtitleCodecs { get; set; }
|
||||
|
||||
public int InternalSubtitleStreamOffset { get; set; }
|
||||
public MediaSourceInfo MediaSource { get; set; }
|
||||
|
@ -52,6 +53,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
public string InputContainer { get; set; }
|
||||
public IsoType? IsoType { get; set; }
|
||||
|
||||
public bool EnableMpegtsM2TsMode { get; set; }
|
||||
|
||||
public BaseEncodingJobOptions BaseRequest { get; set; }
|
||||
|
||||
public long? StartTimeTicks
|
||||
|
@ -64,6 +67,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
get { return BaseRequest.CopyTimestamps; }
|
||||
}
|
||||
|
||||
public int? OutputAudioBitrate;
|
||||
public int? OutputAudioChannels;
|
||||
public int? OutputAudioSampleRate;
|
||||
public bool DeInterlace { get; set; }
|
||||
|
@ -74,8 +78,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
_logger = logger;
|
||||
RemoteHttpHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
PlayableStreamFileNames = new List<string>();
|
||||
SupportedAudioCodecs = new List<string>();
|
||||
SupportedVideoCodecs = new List<string>();
|
||||
SupportedVideoCodecs = new List<string>();
|
||||
SupportedSubtitleCodecs = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -110,5 +115,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
IsoMount = null;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
public string AudioCodec { get; set; }
|
||||
|
||||
public DeviceProfile DeviceProfile { get; set; }
|
||||
public EncodingContext Context { get; set; }
|
||||
|
||||
public bool ReadInputAtNativeFramerate { get; set; }
|
||||
|
||||
|
@ -46,7 +45,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
AudioBitRate = info.AudioBitrate;
|
||||
AudioSampleRate = info.TargetAudioSampleRate;
|
||||
DeviceProfile = deviceProfile;
|
||||
VideoCodec = info.VideoCodec;
|
||||
VideoCodec = info.TargetVideoCodec;
|
||||
VideoBitRate = info.VideoBitrate;
|
||||
AudioStreamIndex = info.AudioStreamIndex;
|
||||
MaxRefFrames = info.MaxRefFrames;
|
||||
|
@ -185,6 +184,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
[ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? MaxVideoBitDepth { get; set; }
|
||||
public bool RequireAvc { get; set; }
|
||||
public bool DeInterlace { get; set; }
|
||||
public bool RequireNonAnamorphic { get; set; }
|
||||
public int? TranscodingMaxAudioChannels { get; set; }
|
||||
public int? CpuCoreLimit { get; set; }
|
||||
public string OutputContainer { get; set; }
|
||||
|
@ -196,6 +197,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
[ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string VideoCodec { get; set; }
|
||||
|
||||
public string SubtitleCodec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the audio stream.
|
||||
/// </summary>
|
||||
|
@ -210,9 +213,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
[ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? VideoStreamIndex { get; set; }
|
||||
|
||||
public EncodingContext Context { get; set; }
|
||||
|
||||
public BaseEncodingJobOptions()
|
||||
{
|
||||
EnableAutoStreamCopy = true;
|
||||
Context = EncodingContext.Streaming;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class JobLogger
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public async void StartStreamingLog(EncodingJob transcodingJob, Stream source, Stream target)
|
||||
public async void StartStreamingLog(EncodingJobInfo state, Stream source, Stream target)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -28,35 +28,41 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
ParseLogLine(line, transcodingJob);
|
||||
ParseLogLine(line, state);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
|
||||
|
||||
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
|
||||
await target.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reading ffmpeg log", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseLogLine(string line, EncodingJob transcodingJob)
|
||||
private void ParseLogLine(string line, EncodingJobInfo state)
|
||||
{
|
||||
float? framerate = null;
|
||||
double? percent = null;
|
||||
TimeSpan? transcodingPosition = null;
|
||||
long? bytesTranscoded = null;
|
||||
int? bitRate = null;
|
||||
|
||||
var parts = line.Split(' ');
|
||||
|
||||
var totalMs = transcodingJob.RunTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(transcodingJob.RunTimeTicks.Value).TotalMilliseconds
|
||||
var totalMs = state.RunTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds
|
||||
: 0;
|
||||
|
||||
var startMs = transcodingJob.Options.StartTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(transcodingJob.Options.StartTimeTicks.Value).TotalMilliseconds
|
||||
var startMs = state.BaseRequest.StartTimeTicks.HasValue
|
||||
? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds
|
||||
: 0;
|
||||
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
|
@ -74,7 +80,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
framerate = val;
|
||||
}
|
||||
}
|
||||
else if (transcodingJob.RunTimeTicks.HasValue &&
|
||||
else if (state.RunTimeTicks.HasValue &&
|
||||
part.StartsWith("time=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var time = part.Split(new[] { '=' }, 2).Last();
|
||||
|
@ -111,11 +117,32 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
}
|
||||
}
|
||||
else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var rate = part.Split(new[] { '=' }, 2).Last();
|
||||
|
||||
int? scale = null;
|
||||
if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
scale = 1024;
|
||||
rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (scale.HasValue)
|
||||
{
|
||||
float val;
|
||||
|
||||
if (float.TryParse(rate, NumberStyles.Any, _usCulture, out val))
|
||||
{
|
||||
bitRate = (int)Math.Ceiling(val * scale.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (framerate.HasValue || percent.HasValue)
|
||||
{
|
||||
transcodingJob.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded);
|
||||
state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,21 +23,18 @@ namespace MediaBrowser.Controller.Net
|
|||
public Action OnComplete { get; set; }
|
||||
public Action OnError { get; set; }
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public FileShareMode FileShare { get; set; }
|
||||
|
||||
public StaticResultOptions()
|
||||
{
|
||||
ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
FileShare = FileShareMode.Read;
|
||||
}
|
||||
}
|
||||
|
||||
public class StaticFileResultOptions : StaticResultOptions
|
||||
{
|
||||
public string Path { get; set; }
|
||||
|
||||
public FileShareMode FileShare { get; set; }
|
||||
|
||||
public StaticFileResultOptions()
|
||||
{
|
||||
FileShare = FileShareMode.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
}
|
||||
|
||||
protected override Task<string> GetCommandLineArguments(EncodingJob state)
|
||||
protected override string GetCommandLineArguments(EncodingJob state)
|
||||
{
|
||||
var audioTranscodeParams = new List<string>();
|
||||
|
||||
|
@ -78,7 +78,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
mapArgs,
|
||||
metadata).Trim();
|
||||
|
||||
return Task.FromResult(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override string GetOutputFileExtension(EncodingJob state)
|
||||
|
|
|
@ -66,7 +66,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
IProgress<double> progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager)
|
||||
var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager, MediaEncoder)
|
||||
.CreateJob(options, EncodingHelper, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
|
||||
|
@ -76,7 +76,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var commandLineArgs = await GetCommandLineArguments(encodingJob).ConfigureAwait(false);
|
||||
var commandLineArgs = GetCommandLineArguments(encodingJob);
|
||||
|
||||
var process = ProcessFactory.Create(new ProcessOptions
|
||||
{
|
||||
|
@ -242,7 +242,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
private void OnTranscodeBeginning(EncodingJob job)
|
||||
{
|
||||
job.ReportTranscodingProgress(null, null, null, null);
|
||||
job.ReportTranscodingProgress(null, null, null, null, null);
|
||||
}
|
||||
|
||||
private void OnTranscodeFailedToStart(string path, EncodingJob job)
|
||||
|
@ -265,7 +265,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
return ConfigurationManager.GetConfiguration<EncodingOptions>("encoding");
|
||||
}
|
||||
|
||||
protected abstract Task<string> GetCommandLineArguments(EncodingJob job);
|
||||
protected abstract string GetCommandLineArguments(EncodingJob job);
|
||||
|
||||
private string GetOutputFilePath(EncodingJob state)
|
||||
{
|
||||
|
|
|
@ -89,6 +89,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
var found = new List<string>();
|
||||
var required = new[]
|
||||
{
|
||||
"mpeg2video",
|
||||
"h264_qsv",
|
||||
"hevc_qsv",
|
||||
"mpeg2_qsv",
|
||||
|
|
|
@ -36,7 +36,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
public string MimeType { get; set; }
|
||||
public bool EstimateContentLength { get; set; }
|
||||
public bool EnableMpegtsM2TsMode { get; set; }
|
||||
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
|
||||
public long? EncodingDurationTicks { get; set; }
|
||||
public string LiveStreamId { get; set; }
|
||||
|
@ -109,7 +108,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
|
||||
public string OutputFilePath { get; set; }
|
||||
public int? OutputAudioBitrate;
|
||||
|
||||
public string ActualOutputVideoCodec
|
||||
{
|
||||
|
@ -379,7 +377,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
return count;
|
||||
}
|
||||
|
||||
public void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded)
|
||||
public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
|
||||
{
|
||||
var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
|
||||
|
||||
|
@ -387,8 +385,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
if (!percentComplete.HasValue && ticks.HasValue && RunTimeTicks.HasValue)
|
||||
{
|
||||
var pct = ticks.Value/RunTimeTicks.Value;
|
||||
percentComplete = pct*100;
|
||||
var pct = ticks.Value / RunTimeTicks.Value;
|
||||
percentComplete = pct * 100;
|
||||
}
|
||||
|
||||
if (percentComplete.HasValue)
|
||||
|
|
|
@ -22,15 +22,17 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IConfigurationManager _config;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config)
|
||||
public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_config = config;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
public async Task<EncodingJob> CreateJob(EncodingJobOptions options, EncodingHelper encodingHelper, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
|
@ -61,6 +63,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SubtitleCodec))
|
||||
{
|
||||
state.SupportedSubtitleCodecs = request.SubtitleCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
|
||||
request.SubtitleCodec = state.SupportedSubtitleCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToSubtitleCodec(i))
|
||||
?? state.SupportedSubtitleCodecs.FirstOrDefault();
|
||||
}
|
||||
|
||||
var item = _libraryManager.GetItemById(request.ItemId);
|
||||
state.ItemType = item.GetType().Name;
|
||||
|
||||
|
|
|
@ -647,9 +647,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
var videoStream = mediaInfo.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
|
||||
|
||||
if (videoStream != null)
|
||||
if (videoStream != null && !videoStream.IsInterlaced)
|
||||
{
|
||||
var isInterlaced = await DetectInterlaced(mediaInfo, videoStream, inputPath, probeSizeArgument).ConfigureAwait(false);
|
||||
var isInterlaced = DetectInterlaced(mediaInfo, videoStream);
|
||||
|
||||
if (isInterlaced)
|
||||
{
|
||||
|
@ -672,7 +672,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DetectInterlaced(MediaSourceInfo video, MediaStream videoStream, string inputPath, string probeSizeArgument)
|
||||
private bool DetectInterlaced(MediaSourceInfo video, MediaStream videoStream)
|
||||
{
|
||||
var formats = (video.Container ?? string.Empty).Split(',').ToList();
|
||||
var enableInterlacedDection = formats.Contains("vob", StringComparer.OrdinalIgnoreCase) ||
|
||||
|
@ -698,165 +698,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
}
|
||||
|
||||
if (video.Protocol != MediaProtocol.File)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var args = "{0} -i {1} -map 0:v:{2} -an -filter:v idet -frames:v 500 -an -f null /dev/null";
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardError = true,
|
||||
FileName = FFMpegPath,
|
||||
Arguments = string.Format(args, probeSizeArgument, inputPath, videoStream.Index.ToString(CultureInfo.InvariantCulture)).Trim(),
|
||||
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
|
||||
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
var idetFoundInterlaced = false;
|
||||
|
||||
using (var processWrapper = new ProcessWrapper(process, this, _logger))
|
||||
{
|
||||
try
|
||||
{
|
||||
StartProcess(processWrapper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error starting ffprobe", ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//process.BeginOutputReadLine();
|
||||
|
||||
using (var reader = new StreamReader(process.StandardError.BaseStream))
|
||||
{
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
if (line.StartsWith("[Parsed_idet", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var idetResult = AnalyzeIdetResult(line);
|
||||
|
||||
if (idetResult.HasValue)
|
||||
{
|
||||
if (!idetResult.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
idetFoundInterlaced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
StopProcess(processWrapper, 100);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return idetFoundInterlaced;
|
||||
}
|
||||
|
||||
private bool? AnalyzeIdetResult(string line)
|
||||
{
|
||||
// As you can see, the filter only guessed one frame as progressive.
|
||||
// Results like this are pretty typical. So if less than 30% of the detections are in the "Undetermined" category, then I only consider the video to be interlaced if at least 65% of the identified frames are in either the TFF or BFF category.
|
||||
// In this case (310 + 311)/(622) = 99.8% which is well over the 65% metric. I may refine that number with more testing but I honestly do not believe I will need to.
|
||||
// http://awel.domblogger.net/videoTranscode/interlace.html
|
||||
var index = line.IndexOf("detection:", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
line = line.Substring(index).Trim();
|
||||
var parts = line.Split(' ').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => i.Trim()).ToList();
|
||||
|
||||
if (parts.Count < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
double tff = 0;
|
||||
double bff = 0;
|
||||
double progressive = 0;
|
||||
double undetermined = 0;
|
||||
double total = 0;
|
||||
|
||||
for (var i = 0; i < parts.Count - 1; i++)
|
||||
{
|
||||
var part = parts[i];
|
||||
|
||||
if (string.Equals(part, "tff:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tff = GetNextPart(parts, i);
|
||||
total += tff;
|
||||
}
|
||||
else if (string.Equals(part, "bff:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bff = GetNextPart(parts, i);
|
||||
total += tff;
|
||||
}
|
||||
else if (string.Equals(part, "progressive:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
progressive = GetNextPart(parts, i);
|
||||
total += progressive;
|
||||
}
|
||||
else if (string.Equals(part, "undetermined:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
undetermined = GetNextPart(parts, i);
|
||||
total += undetermined;
|
||||
}
|
||||
}
|
||||
|
||||
if (total == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((undetermined / total) >= .3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (((tff + bff) / total) >= .4)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private int GetNextPart(List<string> parts, int index)
|
||||
{
|
||||
var next = parts[index + 1];
|
||||
|
||||
int value;
|
||||
if (int.TryParse(next, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The us culture
|
||||
/// </summary>
|
||||
|
|
|
@ -18,143 +18,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
}
|
||||
|
||||
protected override async Task<string> GetCommandLineArguments(EncodingJob state)
|
||||
protected override string GetCommandLineArguments(EncodingJob state)
|
||||
{
|
||||
// Get the output codec name
|
||||
var encodingOptions = GetEncodingOptions();
|
||||
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
|
||||
var format = string.Empty;
|
||||
var keyFrame = string.Empty;
|
||||
|
||||
if (string.Equals(Path.GetExtension(state.OutputFilePath), ".mp4", StringComparison.OrdinalIgnoreCase) &&
|
||||
state.Options.Context == EncodingContext.Streaming)
|
||||
{
|
||||
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
||||
format = " -f mp4 -movflags frag_keyframe+empty_moov";
|
||||
}
|
||||
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, string.Equals(videoCodec, "libvpx", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
|
||||
|
||||
var videoArguments = await GetVideoArguments(state, videoCodec).ConfigureAwait(false);
|
||||
|
||||
return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -threads {5} {6}{7} -y \"{8}\"",
|
||||
inputModifier,
|
||||
EncodingHelper.GetInputArgument(state, encodingOptions),
|
||||
keyFrame,
|
||||
EncodingHelper.GetMapArgs(state),
|
||||
videoArguments,
|
||||
threads,
|
||||
GetAudioArguments(state),
|
||||
format,
|
||||
state.OutputFilePath
|
||||
).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets video arguments to pass to ffmpeg
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="videoCodec">The video codec.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private async Task<string> GetVideoArguments(EncodingJob state, string videoCodec)
|
||||
{
|
||||
var args = "-codec:v:0 " + videoCodec;
|
||||
|
||||
if (state.EnableMpegtsM2TsMode)
|
||||
{
|
||||
args += " -mpegts_m2ts_mode 1";
|
||||
}
|
||||
|
||||
var isOutputMkv = string.Equals(state.Options.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (state.RunTimeTicks.HasValue)
|
||||
{
|
||||
//args += " -copyts -avoid_negative_ts disabled -start_at_zero";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && string.Equals(state.Options.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -bsf:v h264_mp4toannexb";
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
|
||||
5.ToString(UsCulture));
|
||||
|
||||
args += keyFrameArg;
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.Options.SubtitleMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
{
|
||||
args += EncodingHelper.GetOutputSizeParam(state, videoCodec);
|
||||
}
|
||||
|
||||
var qualityParam = EncodingHelper.GetVideoQualityParam(state, videoCodec, GetEncodingOptions(), "superfast");
|
||||
|
||||
if (!string.IsNullOrEmpty(qualityParam))
|
||||
{
|
||||
args += " " + qualityParam.Trim();
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += EncodingHelper.GetGraphicalSubtitleParam(state, videoCodec);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets audio arguments to pass to ffmpeg
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetAudioArguments(EncodingJob state)
|
||||
{
|
||||
// If the video doesn't have an audio stream, return a default.
|
||||
if (state.AudioStream == null && state.VideoStream != null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Get the output codec name
|
||||
var codec = EncodingHelper.GetAudioEncoder(state);
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
|
||||
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
// Add the number of audio channels
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
if (channels.HasValue)
|
||||
{
|
||||
args += " -ac " + channels.Value;
|
||||
}
|
||||
|
||||
var bitrate = state.OutputAudioBitrate;
|
||||
|
||||
if (bitrate.HasValue)
|
||||
{
|
||||
args += " -ab " + bitrate.Value.ToString(UsCulture);
|
||||
}
|
||||
|
||||
args += " " + EncodingHelper.GetAudioFilterParam(state, GetEncodingOptions(), false);
|
||||
|
||||
return args;
|
||||
return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, state.OutputFilePath, "superfast");
|
||||
}
|
||||
|
||||
protected override string GetOutputFileExtension(EncodingJob state)
|
||||
|
|
|
@ -55,7 +55,6 @@
|
|||
<Compile Include="Encoder\EncodingUtils.cs" />
|
||||
<Compile Include="Encoder\EncoderValidator.cs" />
|
||||
<Compile Include="Encoder\FontConfigLoader.cs" />
|
||||
<Compile Include="Encoder\JobLogger.cs" />
|
||||
<Compile Include="Encoder\MediaEncoder.cs" />
|
||||
<Compile Include="Encoder\VideoEncoder.cs" />
|
||||
<Compile Include="Probing\FFProbeHelpers.cs" />
|
||||
|
|
|
@ -264,6 +264,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
/// <value>The loro_surmixlev.</value>
|
||||
public string loro_surmixlev { get; set; }
|
||||
|
||||
public string field_order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
|
|
|
@ -508,6 +508,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
stream.IsAVC = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsInterlaced = true;
|
||||
}
|
||||
|
||||
// Filter out junk
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
|
|
|
@ -734,16 +734,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
}
|
||||
}
|
||||
|
||||
var charsetFromLanguage = string.IsNullOrWhiteSpace(language)
|
||||
? null
|
||||
: GetSubtitleFileCharacterSetFromLanguage(language);
|
||||
|
||||
// This assumption should only be made for external subtitles
|
||||
if (!string.IsNullOrWhiteSpace(charsetFromLanguage) && !string.Equals(charsetFromLanguage, "windows-1252", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return charsetFromLanguage;
|
||||
}
|
||||
|
||||
var charset = await DetectCharset(path, language, protocol, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(charset))
|
||||
|
@ -756,7 +746,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
return charset;
|
||||
}
|
||||
|
||||
return charsetFromLanguage;
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
return GetSubtitleFileCharacterSetFromLanguage(language);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetSubtitleFileCharacterSetFromLanguage(string language)
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
public bool EnableAutomaticSeriesGrouping { get; set; }
|
||||
public bool EnableEmbeddedTitles { get; set; }
|
||||
|
||||
public int AutomaticRefreshIntervalDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the preferred metadata language.
|
||||
/// </summary>
|
||||
|
|
|
@ -48,6 +48,7 @@ namespace MediaBrowser.Model.Configuration
|
|||
public bool EnableHttps { get; set; }
|
||||
public bool EnableSeriesPresentationUniqueKey { get; set; }
|
||||
public bool EnableLocalizedGuids { get; set; }
|
||||
public bool EnableNormalizedItemByNameIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value pointing to the file system where the ssl certiifcate is located..
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
NumVideoStreams = 17,
|
||||
IsSecondaryAudio = 18,
|
||||
VideoCodecTag = 19,
|
||||
IsAvc = 20
|
||||
IsAvc = 20,
|
||||
IsInterlaced = 21
|
||||
}
|
||||
}
|
|
@ -231,6 +231,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
{
|
||||
playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
|
||||
}
|
||||
|
||||
playlistItem.SubProtocol = transcodingProfile.Protocol;
|
||||
|
||||
List<CodecProfile> audioCodecProfiles = new List<CodecProfile>();
|
||||
|
@ -323,7 +324,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
if (directPlayProfile != null)
|
||||
{
|
||||
// While options takes the network and other factors into account. Only applies to direct stream
|
||||
if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true)) && options.EnableDirectStream)
|
||||
if (item.SupportsDirectStream && IsAudioEligibleForDirectPlay(item, options.GetMaxBitrate(true), PlayMethod.DirectStream) && options.EnableDirectStream)
|
||||
{
|
||||
playMethods.Add(PlayMethod.DirectStream);
|
||||
}
|
||||
|
@ -331,7 +332,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
// The profile describes what the device supports
|
||||
// If device requirements are satisfied then allow both direct stream and direct play
|
||||
if (item.SupportsDirectPlay &&
|
||||
IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true)) && options.EnableDirectPlay)
|
||||
IsAudioEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true), PlayMethod.DirectPlay) && options.EnableDirectPlay)
|
||||
{
|
||||
playMethods.Add(PlayMethod.DirectPlay);
|
||||
}
|
||||
|
@ -479,10 +480,19 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
playlistItem.AudioCodecs = transcodingProfile.AudioCodec.Split(',');
|
||||
|
||||
playlistItem.VideoCodec = transcodingProfile.VideoCodec;
|
||||
playlistItem.VideoCodecs = transcodingProfile.VideoCodec.Split(',');
|
||||
playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
|
||||
playlistItem.EnableSubtitlesInManifest = transcodingProfile.EnableSubtitlesInManifest;
|
||||
|
||||
if (transcodingProfile.MinSegments > 0)
|
||||
{
|
||||
playlistItem.MinSegments = transcodingProfile.MinSegments;
|
||||
}
|
||||
if (transcodingProfile.SegmentLength > 0)
|
||||
{
|
||||
playlistItem.SegmentLength = transcodingProfile.SegmentLength;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(transcodingProfile.MaxAudioChannels))
|
||||
{
|
||||
int transcodingMaxAudioChannels;
|
||||
|
@ -895,7 +905,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
}
|
||||
}
|
||||
|
||||
return IsAudioEligibleForDirectPlay(item, maxBitrate);
|
||||
return IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
|
||||
}
|
||||
|
||||
public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, string transcodingSubProtocol, string transcodingContainer)
|
||||
|
@ -1025,23 +1035,29 @@ namespace MediaBrowser.Model.Dlna
|
|||
return null;
|
||||
}
|
||||
|
||||
private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long? maxBitrate)
|
||||
private bool IsAudioEligibleForDirectPlay(MediaSourceInfo item, long? maxBitrate, PlayMethod playMethod)
|
||||
{
|
||||
// Don't restrict by bitrate if coming from an external domain
|
||||
if (item.IsRemote)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!maxBitrate.HasValue)
|
||||
{
|
||||
_logger.Info("Cannot direct play due to unknown supported bitrate");
|
||||
_logger.Info("Cannot "+ playMethod + " due to unknown supported bitrate");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!item.Bitrate.HasValue)
|
||||
{
|
||||
_logger.Info("Cannot direct play due to unknown content bitrate");
|
||||
_logger.Info("Cannot " + playMethod + " due to unknown content bitrate");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.Bitrate.Value > maxBitrate.Value)
|
||||
{
|
||||
_logger.Info("Bitrate exceeds DirectPlay limit: media bitrate: {0}, max bitrate: {1}", item.Bitrate.Value.ToString(CultureInfo.InvariantCulture), maxBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
_logger.Info("Bitrate exceeds " + playMethod + " limit: media bitrate: {0}, max bitrate: {1}", item.Bitrate.Value.ToString(CultureInfo.InvariantCulture), maxBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1137,6 +1153,37 @@ namespace MediaBrowser.Model.Dlna
|
|||
break;
|
||||
}
|
||||
case ProfileConditionValue.IsAnamorphic:
|
||||
{
|
||||
bool isAnamorphic;
|
||||
if (bool.TryParse(value, out isAnamorphic))
|
||||
{
|
||||
if (isAnamorphic && condition.Condition == ProfileConditionType.Equals)
|
||||
{
|
||||
item.RequireNonAnamorphic = true;
|
||||
}
|
||||
else if (!isAnamorphic && condition.Condition == ProfileConditionType.NotEquals)
|
||||
{
|
||||
item.RequireNonAnamorphic = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ProfileConditionValue.IsInterlaced:
|
||||
{
|
||||
bool isInterlaced;
|
||||
if (bool.TryParse(value, out isInterlaced))
|
||||
{
|
||||
if (isInterlaced && condition.Condition == ProfileConditionType.Equals)
|
||||
{
|
||||
item.DeInterlace = true;
|
||||
}
|
||||
else if (!isInterlaced && condition.Condition == ProfileConditionType.NotEquals)
|
||||
{
|
||||
item.DeInterlace = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ProfileConditionValue.AudioProfile:
|
||||
case ProfileConditionValue.Has64BitOffsets:
|
||||
case ProfileConditionValue.PacketLength:
|
||||
|
|
|
@ -6,6 +6,7 @@ using MediaBrowser.Model.MediaInfo;
|
|||
using MediaBrowser.Model.Session;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
|
@ -18,6 +19,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
public StreamInfo()
|
||||
{
|
||||
AudioCodecs = new string[] { };
|
||||
VideoCodecs = new string[] { };
|
||||
SubtitleCodecs = new string[] { };
|
||||
}
|
||||
|
||||
|
@ -34,13 +36,18 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
public long StartPositionTicks { get; set; }
|
||||
|
||||
public string VideoCodec { get; set; }
|
||||
public string VideoProfile { get; set; }
|
||||
|
||||
public int? SegmentLength { get; set; }
|
||||
public int? MinSegments { get; set; }
|
||||
|
||||
public bool RequireAvc { get; set; }
|
||||
public bool DeInterlace { get; set; }
|
||||
public bool RequireNonAnamorphic { get; set; }
|
||||
public bool CopyTimestamps { get; set; }
|
||||
public bool EnableSubtitlesInManifest { get; set; }
|
||||
public string[] AudioCodecs { get; set; }
|
||||
public string[] VideoCodecs { get; set; }
|
||||
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
|
@ -204,11 +211,15 @@ namespace MediaBrowser.Model.Dlna
|
|||
string.Empty :
|
||||
string.Join(",", item.AudioCodecs);
|
||||
|
||||
string videoCodecs = item.VideoCodecs.Length == 0 ?
|
||||
string.Empty :
|
||||
string.Join(",", item.VideoCodecs);
|
||||
|
||||
list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
|
||||
list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
|
||||
list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
|
||||
list.Add(new NameValuePair("Static", item.IsDirectStream.ToString().ToLower()));
|
||||
list.Add(new NameValuePair("VideoCodec", item.VideoCodec ?? string.Empty));
|
||||
list.Add(new NameValuePair("VideoCodec", videoCodecs));
|
||||
list.Add(new NameValuePair("AudioCodec", audioCodecs));
|
||||
list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? StringHelper.ToStringCultureInvariant(item.AudioStreamIndex.Value) : string.Empty));
|
||||
list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? StringHelper.ToStringCultureInvariant(item.SubtitleStreamIndex.Value) : string.Empty));
|
||||
|
@ -232,7 +243,9 @@ namespace MediaBrowser.Model.Dlna
|
|||
// }
|
||||
//}
|
||||
|
||||
if (StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls") && !forceStartPosition)
|
||||
var isHls = StringHelper.EqualsIgnoreCase(item.SubProtocol, "hls");
|
||||
|
||||
if (isHls && !forceStartPosition)
|
||||
{
|
||||
list.Add(new NameValuePair("StartTimeTicks", string.Empty));
|
||||
}
|
||||
|
@ -276,6 +289,24 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
|
||||
|
||||
list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString().ToLower()));
|
||||
list.Add(new NameValuePair("DeInterlace", item.DeInterlace.ToString().ToLower()));
|
||||
|
||||
if (!isDlna && isHls)
|
||||
{
|
||||
list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
|
||||
|
||||
if (item.SegmentLength.HasValue)
|
||||
{
|
||||
list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
if (item.MinSegments.HasValue)
|
||||
{
|
||||
list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
@ -609,6 +640,31 @@ namespace MediaBrowser.Model.Dlna
|
|||
}
|
||||
}
|
||||
|
||||
public string TargetVideoCodec
|
||||
{
|
||||
get
|
||||
{
|
||||
MediaStream stream = TargetVideoStream;
|
||||
|
||||
string inputCodec = stream == null ? null : stream.Codec;
|
||||
|
||||
if (IsDirectStream)
|
||||
{
|
||||
return inputCodec;
|
||||
}
|
||||
|
||||
foreach (string codec in VideoCodecs)
|
||||
{
|
||||
if (StringHelper.EqualsIgnoreCase(codec, inputCodec))
|
||||
{
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
|
||||
return VideoCodecs.Length == 0 ? null : VideoCodecs[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicts the audio channels that will be in the output stream
|
||||
/// </summary>
|
||||
|
|
|
@ -42,6 +42,12 @@ namespace MediaBrowser.Model.Dlna
|
|||
[XmlAttribute("maxAudioChannels")]
|
||||
public string MaxAudioChannels { get; set; }
|
||||
|
||||
[XmlAttribute("minSegments")]
|
||||
public int MinSegments { get; set; }
|
||||
|
||||
[XmlAttribute("segmentLength")]
|
||||
public int SegmentLength { get; set; }
|
||||
|
||||
public List<string> GetAudioCodecs()
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
|
|
|
@ -10,6 +10,8 @@ namespace MediaBrowser.Model.IO
|
|||
/// </summary>
|
||||
public interface IFileSystem
|
||||
{
|
||||
void AddShortcutHandler(IShortcutHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified filename is shortcut.
|
||||
/// </summary>
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace MediaBrowser.Model.LiveTv
|
|||
TunerHosts = new List<TunerHostInfo>();
|
||||
ListingProviders = new List<ListingsProviderInfo>();
|
||||
MediaLocationsCreated = new string[] { };
|
||||
RecordingEncodingFormat = "mp4";
|
||||
RecordingEncodingFormat = "mkv";
|
||||
RecordingPostProcessorArguments = "\"{path}\"";
|
||||
EnableRecordingEncoding = true;
|
||||
}
|
||||
|
@ -46,14 +46,13 @@ namespace MediaBrowser.Model.LiveTv
|
|||
public string Url { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string DeviceId { get; set; }
|
||||
public string FriendlyName { get; set; }
|
||||
public bool ImportFavoritesOnly { get; set; }
|
||||
public bool AllowHWTranscoding { get; set; }
|
||||
public bool IsEnabled { get; set; }
|
||||
public bool EnableTvgId { get; set; }
|
||||
|
||||
public TunerHostInfo()
|
||||
{
|
||||
IsEnabled = true;
|
||||
AllowHWTranscoding = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,11 @@ namespace MediaBrowser.Model.MediaInfo
|
|||
public bool EnableDirectPlay { get; set; }
|
||||
public bool EnableDirectStream { get; set; }
|
||||
public bool EnableTranscoding { get; set; }
|
||||
public bool ForceDirectPlayRemoteMediaSource { get; set; }
|
||||
|
||||
public PlaybackInfoRequest()
|
||||
{
|
||||
ForceDirectPlayRemoteMediaSource = true;
|
||||
EnableDirectPlay = true;
|
||||
EnableDirectStream = true;
|
||||
EnableTranscoding = true;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Model.Net
|
||||
{
|
||||
|
@ -13,6 +15,7 @@ namespace MediaBrowser.Model.Net
|
|||
void Bind(IpEndPointInfo endpoint);
|
||||
void Connect(IpEndPointInfo endPoint);
|
||||
void StartAccept(Action<IAcceptSocket> onAccept, Func<bool> isClosed);
|
||||
Task SendFile(string path, byte[] preBuffer, byte[] postBuffer, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class SocketCreateException : Exception
|
||||
|
|
|
@ -24,5 +24,6 @@ namespace MediaBrowser.Model.Net
|
|||
/// Sends a UDP message to a particular end point (uni or multicast).
|
||||
/// </summary>
|
||||
Task SendAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken);
|
||||
Task SendWithLockAsync(byte[] buffer, int bytes, IpEndPointInfo endPoint, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,8 @@ namespace MediaBrowser.Model.Net
|
|||
/// <returns>A <see cref="ISocket"/> implementation.</returns>
|
||||
ISocket CreateUdpSocket(int localPort);
|
||||
|
||||
ISocket CreateUdpBroadcastSocket(int localPort);
|
||||
|
||||
ISocket CreateTcpSocket(IpAddressInfo remoteAddress, int remotePort);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -55,9 +55,12 @@ namespace MediaBrowser.Model.Querying
|
|||
/// <value>The enable image types.</value>
|
||||
public ImageType[] EnableImageTypes { get; set; }
|
||||
|
||||
public bool EnableTotalRecordCount { get; set; }
|
||||
|
||||
public NextUpQuery()
|
||||
{
|
||||
EnableImageTypes = new ImageType[] {};
|
||||
EnableTotalRecordCount = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user