mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Transcoder: Including transcode methods in the filesystem interface
This commit is contained in:
parent
40e32a1689
commit
cd5b953e0f
18
Kyoo.sln
18
Kyoo.sln
@ -25,6 +25,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Host.Console", "src\Ky
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Swagger", "src\Kyoo.Swagger\Kyoo.Swagger.csproj", "{7D1A7596-73F6-4D35-842E-A5AD9C620596}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{FEAE1B0E-D797-470F-9030-0EF743575ECC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Databases", "Databases", "{865461CA-EC06-4B42-91CF-8723B0A9BB67}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{C569FF25-7E01-484C-9F72-5B99845AD94B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -88,4 +96,14 @@ Global
|
||||
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0C8AA7EA-E723-4532-852F-35AA4E8AFED5} = {FEAE1B0E-D797-470F-9030-0EF743575ECC}
|
||||
{BAB270D4-E0EA-4329-BA65-512FDAB01001} = {8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}
|
||||
{D06BF829-23F5-40F3-A62D-627D9F4B4D6C} = {8D28F5EF-0CD7-4697-A2A7-24EC31A48F21}
|
||||
{6F91B645-F785-46BB-9C4F-1EFC83E489B6} = {865461CA-EC06-4B42-91CF-8723B0A9BB67}
|
||||
{3213C96D-0BF3-460B-A8B5-B9977229408A} = {865461CA-EC06-4B42-91CF-8723B0A9BB67}
|
||||
{6515380E-1E57-42DA-B6E3-E1C8A848818A} = {865461CA-EC06-4B42-91CF-8723B0A9BB67}
|
||||
{D8658BEA-8949-45AC-BEBB-A4FFC4F800F5} = {C569FF25-7E01-484C-9F72-5B99845AD94B}
|
||||
{98851001-40DD-46A6-94B3-2F8D90722076} = {C569FF25-7E01-484C-9F72-5B99845AD94B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
@ -31,8 +31,6 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// </summary>
|
||||
public interface IFileSystem
|
||||
{
|
||||
// TODO find a way to handle Transmux/Transcode with this system.
|
||||
|
||||
/// <summary>
|
||||
/// Used for http queries returning a file. This should be used to return local files
|
||||
/// or proxy them from a distant server.
|
||||
@ -51,7 +49,7 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// If the type is not specified, it will be deduced automatically (from the extension or by sniffing the file).
|
||||
/// </param>
|
||||
/// <returns>An <see cref="IActionResult"/> representing the file returned.</returns>
|
||||
public IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null);
|
||||
IActionResult FileResult([CanBeNull] string path, bool rangeSupport = false, string type = null);
|
||||
|
||||
/// <summary>
|
||||
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
|
||||
@ -60,7 +58,7 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// <param name="path">The path of the file</param>
|
||||
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
|
||||
/// <returns>A reader to read the file.</returns>
|
||||
public Task<Stream> GetReader([NotNull] string path);
|
||||
Task<Stream> GetReader([NotNull] string path);
|
||||
|
||||
/// <summary>
|
||||
/// Read a file present at <paramref name="path"/>. The reader can be used in an arbitrary context.
|
||||
@ -70,28 +68,28 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// <param name="mime">The mime type of the opened file.</param>
|
||||
/// <exception cref="FileNotFoundException">If the file could not be found.</exception>
|
||||
/// <returns>A reader to read the file.</returns>
|
||||
public Task<Stream> GetReader([NotNull] string path, AsyncRef<string> mime);
|
||||
Task<Stream> GetReader([NotNull] string path, AsyncRef<string> mime);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new file at <paramref name="path"></paramref>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the new file.</param>
|
||||
/// <returns>A writer to write to the new file.</returns>
|
||||
public Task<Stream> NewFile([NotNull] string path);
|
||||
Task<Stream> NewFile([NotNull] string path);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new directory at the given path
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the directory</param>
|
||||
/// <returns>The path of the newly created directory is returned.</returns>
|
||||
public Task<string> CreateDirectory([NotNull] string path);
|
||||
Task<string> CreateDirectory([NotNull] string path);
|
||||
|
||||
/// <summary>
|
||||
/// Combine multiple paths.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths to combine</param>
|
||||
/// <returns>The combined path.</returns>
|
||||
public string Combine(params string[] paths);
|
||||
string Combine(params string[] paths);
|
||||
|
||||
/// <summary>
|
||||
/// List files in a directory.
|
||||
@ -99,7 +97,7 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// <param name="path">The path of the directory</param>
|
||||
/// <param name="options">Should the search be recursive or not.</param>
|
||||
/// <returns>A list of files's path.</returns>
|
||||
public Task<ICollection<string>> ListFiles([NotNull] string path,
|
||||
Task<ICollection<string>> ListFiles([NotNull] string path,
|
||||
SearchOption options = SearchOption.TopDirectoryOnly);
|
||||
|
||||
/// <summary>
|
||||
@ -107,7 +105,7 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check</param>
|
||||
/// <returns>True if the path exists, false otherwise</returns>
|
||||
public Task<bool> Exists([NotNull] string path);
|
||||
Task<bool> Exists([NotNull] string path);
|
||||
|
||||
/// <summary>
|
||||
/// Get the extra directory of a resource <typeparamref name="T"/>.
|
||||
@ -117,6 +115,25 @@ namespace Kyoo.Abstractions.Controllers
|
||||
/// <param name="resource">The resource to proceed</param>
|
||||
/// <typeparam name="T">The type of the resource.</typeparam>
|
||||
/// <returns>The extra directory of the resource.</returns>
|
||||
public Task<string> GetExtraDirectory<T>([NotNull] T resource);
|
||||
Task<string> GetExtraDirectory<T>([NotNull] T resource);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve tracks for a specific episode.
|
||||
/// Subtitles, chapters and fonts should also be extracted and cached when calling this method.
|
||||
/// </summary>
|
||||
/// <param name="episode">The episode to retrieve tracks for.</param>
|
||||
/// <param name="reExtract">Should the cache be invalidated and subtitles and others be re-extracted?</param>
|
||||
/// <returns>The list of tracks available for this episode.</returns>
|
||||
Task<ICollection<Track>> ExtractInfos([NotNull] Episode episode, bool reExtract);
|
||||
|
||||
/// <summary>
|
||||
/// Transmux the selected episode to hls.
|
||||
/// </summary>
|
||||
/// <param name="episode">The episode to transmux.</param>
|
||||
/// <returns>The master file (m3u8) of the transmuxed hls file.</returns>
|
||||
IActionResult Transmux([NotNull] Episode episode);
|
||||
|
||||
// Maybe add options for to select the codec.
|
||||
// IActionResult Transcode(Episode episode);
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Models;
|
||||
|
||||
namespace Kyoo.Abstractions.Controllers
|
||||
{
|
||||
public interface ITranscoder
|
||||
{
|
||||
Task<Track[]> ExtractInfos(Episode episode, bool reextract);
|
||||
|
||||
Task<string> Transmux(Episode episode);
|
||||
|
||||
Task<string> Transcode(Episode episode);
|
||||
}
|
||||
}
|
@ -107,7 +107,8 @@ namespace Kyoo.Abstractions.Models.Utils
|
||||
{
|
||||
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
|
||||
return Expression.Lambda<Func<T, bool>>(equal);
|
||||
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
|
||||
return Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -124,7 +125,8 @@ namespace Kyoo.Abstractions.Models.Utils
|
||||
{
|
||||
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
|
||||
return Expression.Lambda<Func<T, bool>>(equal);
|
||||
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
|
||||
return Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -214,5 +214,19 @@ namespace Kyoo.Core.Controllers
|
||||
};
|
||||
return await CreateDirectory(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
|
||||
{
|
||||
IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _);
|
||||
return fs.ExtractInfos(episode, reExtract);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IActionResult Transmux(Episode episode)
|
||||
{
|
||||
IFileSystem fs = _GetFileSystemForPath(episode.Path, out string _);
|
||||
return fs.Transmux(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +110,18 @@ namespace Kyoo.Core.Controllers
|
||||
throw new NotSupportedException("Extras can not be stored inside an http filesystem.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
|
||||
{
|
||||
throw new NotSupportedException("Extracting infos is not supported on an http filesystem.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IActionResult Transmux(Episode episode)
|
||||
{
|
||||
throw new NotSupportedException("Transmuxing is not supported on an http filesystem.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IActionResult"/> to proxy an http request.
|
||||
/// </summary>
|
||||
|
@ -41,6 +41,11 @@ namespace Kyoo.Core.Controllers
|
||||
/// </summary>
|
||||
private readonly IContentTypeProvider _provider;
|
||||
|
||||
/// <summary>
|
||||
/// The transcoder of local files.
|
||||
/// </summary>
|
||||
private readonly ITranscoder _transcoder;
|
||||
|
||||
/// <summary>
|
||||
/// Options to check if the metadata should be kept in the show directory or in a kyoo's directory.
|
||||
/// </summary>
|
||||
@ -51,10 +56,14 @@ namespace Kyoo.Core.Controllers
|
||||
/// </summary>
|
||||
/// <param name="options">The options to use.</param>
|
||||
/// <param name="provider">An extension provider to get content types from files extensions.</param>
|
||||
public LocalFileSystem(IOptionsMonitor<BasicOptions> options, IContentTypeProvider provider)
|
||||
/// <param name="transcoder">The transcoder of local files.</param>
|
||||
public LocalFileSystem(IOptionsMonitor<BasicOptions> options,
|
||||
IContentTypeProvider provider,
|
||||
ITranscoder transcoder)
|
||||
{
|
||||
_options = options;
|
||||
_provider = provider;
|
||||
_transcoder = transcoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -155,5 +164,15 @@ namespace Kyoo.Core.Controllers
|
||||
_ => null
|
||||
});
|
||||
}
|
||||
|
||||
public Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
|
||||
{
|
||||
return _transcoder.ExtractInfos(episode, reExtract);
|
||||
}
|
||||
|
||||
public IActionResult Transmux(Episode episode)
|
||||
{
|
||||
return _transcoder.Transmux(episode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,36 +17,70 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Core.Models.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
// We use threads so tasks are not always awaited.
|
||||
#pragma warning disable 4014
|
||||
// Private items that are external can't start with an _
|
||||
#pragma warning disable IDE1006
|
||||
|
||||
namespace Kyoo.Core.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The transcoder used by the <see cref="LocalFileSystem"/>.
|
||||
/// </summary>
|
||||
public class Transcoder : ITranscoder
|
||||
{
|
||||
/// <summary>
|
||||
/// The class that interact with the transcoder written in C.
|
||||
/// </summary>
|
||||
private static class TranscoderAPI
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the library. For windows '.dll' should be appended, on linux or macos it should be prefixed
|
||||
/// by 'lib' and '.so' or '.dylib' should be appended.
|
||||
/// </summary>
|
||||
private const string TranscoderPath = "transcoder";
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the C library, setup the logger and return the size of a <see cref="Models.Watch.Stream"/>.
|
||||
/// </summary>
|
||||
/// <returns>The size of a <see cref="Models.Watch.Stream"/></returns>
|
||||
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int init();
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the C library, setup the logger and return the size of a <see cref="Models.Watch.Stream"/>.
|
||||
/// </summary>
|
||||
/// <returns>The size of a <see cref="Models.Watch.Stream"/></returns>
|
||||
public static int Init() => init();
|
||||
|
||||
/// <summary>
|
||||
/// Transmux the file at the specified path. The path must be a local one with '/' as a separator.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of a local file with '/' as a separators.</param>
|
||||
/// <param name="outPath">The path of the hls output file.</param>
|
||||
/// <param name="playableDuration">
|
||||
/// The number of seconds currently playable. This is incremented as the file gets transmuxed.
|
||||
/// </param>
|
||||
/// <returns><c>0</c> on success, non 0 on failure.</returns>
|
||||
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
|
||||
private static extern int transmux(string path, string outpath, out float playableDuration);
|
||||
private static extern int transmux(string path, string outPath, out float playableDuration);
|
||||
|
||||
/// <summary>
|
||||
/// Transmux the file at the specified path. The path must be a local one.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of a local file.</param>
|
||||
/// <param name="outPath">The path of the hls output file.</param>
|
||||
/// <param name="playableDuration">
|
||||
/// The number of seconds currently playable. This is incremented as the file gets transmuxed.
|
||||
/// </param>
|
||||
/// <returns><c>0</c> on success, non 0 on failure.</returns>
|
||||
public static int Transmux(string path, string outPath, out float playableDuration)
|
||||
{
|
||||
path = path.Replace('\\', '/');
|
||||
@ -54,24 +88,47 @@ namespace Kyoo.Core.Controllers
|
||||
return transmux(path, outPath, out playableDuration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file.
|
||||
/// </summary>
|
||||
/// <param name="path">
|
||||
/// The path of the video file to analyse. This must be a local path with '/' as a separator.
|
||||
/// </param>
|
||||
/// <param name="outPath">The directory that will be used to store extracted files.</param>
|
||||
/// <param name="length">The size of the returned array.</param>
|
||||
/// <param name="trackCount">The number of tracks in the returned array.</param>
|
||||
/// <param name="reExtract">Should the cache be invalidated and information re-extracted or not?</param>
|
||||
/// <returns>A pointer to an array of <see cref="Models.Watch.Stream"/></returns>
|
||||
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
|
||||
private static extern IntPtr extract_infos(string path,
|
||||
string outpath,
|
||||
string outPath,
|
||||
out uint length,
|
||||
out uint trackCount,
|
||||
bool reextracct);
|
||||
bool reExtract);
|
||||
|
||||
/// <summary>
|
||||
/// An helper method to free an array of <see cref="Models.Watch.Stream"/>.
|
||||
/// </summary>
|
||||
/// <param name="streams">A pointer to the first element of the array</param>
|
||||
/// <param name="count">The number of items in the array.</param>
|
||||
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void free_streams(IntPtr streams, uint count);
|
||||
|
||||
public static Track[] ExtractInfos(string path, string outPath, bool reextract)
|
||||
/// <summary>
|
||||
/// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the video file to analyse. This must be a local path.</param>
|
||||
/// <param name="outPath">The directory that will be used to store extracted files.</param>
|
||||
/// <param name="reExtract">Should the cache be invalidated and information re-extracted or not?</param>
|
||||
/// <returns>An array of <see cref="Track"/>.</returns>
|
||||
public static Track[] ExtractInfos(string path, string outPath, bool reExtract)
|
||||
{
|
||||
path = path.Replace('\\', '/');
|
||||
outPath = outPath.Replace('\\', '/');
|
||||
|
||||
int size = Marshal.SizeOf<Models.Watch.Stream>();
|
||||
IntPtr ptr = extract_infos(path, outPath, out uint arrayLength, out uint trackCount, reextract);
|
||||
IntPtr ptr = extract_infos(path, outPath, out uint arrayLength, out uint trackCount, reExtract);
|
||||
IntPtr streamsPtr = ptr;
|
||||
Track[] tracks;
|
||||
|
||||
@ -100,67 +157,161 @@ namespace Kyoo.Core.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
public class BadTranscoderException : Exception { }
|
||||
|
||||
/// <summary>
|
||||
/// The file system used to retrieve the extra directory of shows to know where to extract information.
|
||||
/// </summary>
|
||||
private readonly IFileSystem _files;
|
||||
private readonly IOptions<BasicOptions> _options;
|
||||
private readonly Lazy<ILibraryManager> _library;
|
||||
|
||||
public Transcoder(IFileSystem files, IOptions<BasicOptions> options, Lazy<ILibraryManager> library)
|
||||
/// <summary>
|
||||
/// Options to know where to cache transmuxed/transcoded episodes.
|
||||
/// </summary>
|
||||
private readonly IOptions<BasicOptions> _options;
|
||||
|
||||
/// <summary>
|
||||
/// The logger to use. This is also used by the wrapped C library.
|
||||
/// </summary>
|
||||
private readonly ILogger<Transcoder> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Transcoder"/>.
|
||||
/// </summary>
|
||||
/// <param name="files">
|
||||
/// The file system used to retrieve the extra directory of shows to know where to extract information.
|
||||
/// </param>
|
||||
/// <param name="options">Options to know where to cache transmuxed/transcoded episodes.</param>
|
||||
/// <param name="logger">The logger to use. This is also used by the wrapped C library.</param>
|
||||
public Transcoder(IFileSystem files, IOptions<BasicOptions> options, ILogger<Transcoder> logger)
|
||||
{
|
||||
_files = files;
|
||||
_options = options;
|
||||
_library = library;
|
||||
_logger = logger;
|
||||
|
||||
if (TranscoderAPI.Init() != Marshal.SizeOf<Models.Watch.Stream>())
|
||||
throw new BadTranscoderException();
|
||||
_logger.LogCritical("The transcoder library could not be initialized correctly");
|
||||
}
|
||||
|
||||
public async Task<Track[]> ExtractInfos(Episode episode, bool reextract)
|
||||
/// <inheritdoc />
|
||||
public async Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract)
|
||||
{
|
||||
await _library.Value.Load(episode, x => x.Show);
|
||||
string dir = await _files.GetExtraDirectory(episode.Show);
|
||||
string dir = await _files.GetExtraDirectory(episode);
|
||||
if (dir == null)
|
||||
throw new ArgumentException("Invalid path.");
|
||||
return await Task.Factory.StartNew(
|
||||
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reextract),
|
||||
TaskCreationOptions.LongRunning);
|
||||
() => TranscoderAPI.ExtractInfos(episode.Path, dir, reExtract),
|
||||
TaskCreationOptions.LongRunning
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<string> Transmux(Episode episode)
|
||||
/// <inheritdoc />
|
||||
public IActionResult Transmux(Episode episode)
|
||||
{
|
||||
if (!File.Exists(episode.Path))
|
||||
throw new ArgumentException("Path does not exists. Can't transcode.");
|
||||
|
||||
string folder = Path.Combine(_options.Value.TransmuxPath, episode.Slug);
|
||||
string manifest = Path.Combine(folder, episode.Slug + ".m3u8");
|
||||
float playableDuration = 0;
|
||||
bool transmuxFailed = false;
|
||||
string manifest = Path.GetFullPath(Path.Combine(folder, episode.Slug + ".m3u8"));
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
if (File.Exists(manifest))
|
||||
return manifest;
|
||||
return new PhysicalFileResult(manifest, "application/x-mpegurl");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config.");
|
||||
return null;
|
||||
_logger.LogCritical("Access to the path {Manifest} is denied. " +
|
||||
"Please change your transmux path in the config", manifest);
|
||||
return new StatusCodeResult(500);
|
||||
}
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
transmuxFailed = TranscoderAPI.Transmux(episode.Path, manifest, out playableDuration) != 0;
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
while (playableDuration < 10 || (!File.Exists(manifest) && !transmuxFailed))
|
||||
await Task.Delay(10);
|
||||
return transmuxFailed ? null : manifest;
|
||||
return new TransmuxResult(episode.Path, manifest, _logger);
|
||||
}
|
||||
|
||||
public Task<string> Transcode(Episode episode)
|
||||
/// <summary>
|
||||
/// An action result that runs the transcoder and return the created manifest file after a few seconds of
|
||||
/// the video has been proceeded. If the transcoder fails, it returns a 500 error code.
|
||||
/// </summary>
|
||||
private class TransmuxResult : IActionResult
|
||||
{
|
||||
return Task.FromResult<string>(null); // Not implemented yet.
|
||||
/// <summary>
|
||||
/// The path of the episode to transmux. It must be a local one.
|
||||
/// </summary>
|
||||
private readonly string _path;
|
||||
|
||||
/// <summary>
|
||||
/// The path of the manifest file to create. It must be a local one.
|
||||
/// </summary>
|
||||
private readonly string _manifest;
|
||||
|
||||
/// <summary>
|
||||
/// The logger to use in case of issue.
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="TransmuxResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the episode to transmux. It must be a local one.</param>
|
||||
/// <param name="manifest">The path of the manifest file to create. It must be a local one.</param>
|
||||
/// <param name="logger">The logger to use in case of issue.</param>
|
||||
public TransmuxResult(string path, string manifest, ILogger logger)
|
||||
{
|
||||
_path = path;
|
||||
_manifest = Path.GetFullPath(manifest);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// We use threads so tasks are not always awaited.
|
||||
#pragma warning disable 4014
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
float playableDuration = 0;
|
||||
bool transmuxFailed = false;
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
transmuxFailed = TranscoderAPI.Transmux(_path, _manifest, out playableDuration) != 0;
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
|
||||
while (playableDuration < 10 || (!File.Exists(_manifest) && !transmuxFailed))
|
||||
await Task.Delay(10);
|
||||
|
||||
if (!transmuxFailed)
|
||||
{
|
||||
new PhysicalFileResult(_manifest, "application/x-mpegurl")
|
||||
.ExecuteResultAsync(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogCritical("The transmuxing failed on the C library");
|
||||
new StatusCodeResult(500)
|
||||
.ExecuteResultAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore 4014
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The transcoder used by the <see cref="LocalFileSystem"/>. This is on a different interface than the file system
|
||||
/// to offset the work.
|
||||
/// </summary>
|
||||
public interface ITranscoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve tracks for a specific episode.
|
||||
/// Subtitles, chapters and fonts should also be extracted and cached when calling this method.
|
||||
/// </summary>
|
||||
/// <param name="episode">The episode to retrieve tracks for.</param>
|
||||
/// <param name="reExtract">Should the cache be invalidated and subtitles and others be re-extracted?</param>
|
||||
/// <returns>The list of tracks available for this episode.</returns>
|
||||
Task<ICollection<Track>> ExtractInfos(Episode episode, bool reExtract);
|
||||
|
||||
/// <summary>
|
||||
/// Transmux the selected episode to hls.
|
||||
/// </summary>
|
||||
/// <param name="episode">The episode to transmux.</param>
|
||||
/// <returns>The master file (m3u8) of the transmuxed hls file.</returns>
|
||||
IActionResult Transmux(Episode episode);
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ namespace Kyoo.Core.Tasks
|
||||
/// <summary>
|
||||
/// The transcoder used to extract subtitles and metadata.
|
||||
/// </summary>
|
||||
private readonly ITranscoder _transcoder;
|
||||
private readonly IFileSystem _transcoder;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="RegisterEpisode"/> task.
|
||||
@ -74,13 +74,13 @@ namespace Kyoo.Core.Tasks
|
||||
/// The thumbnail manager used to download images.
|
||||
/// </param>
|
||||
/// <param name="transcoder">
|
||||
/// The transcoder used to extract subtitles and metadata.
|
||||
/// The file manager used to retrieve episodes metadata.
|
||||
/// </param>
|
||||
public RegisterEpisode(IIdentifier identifier,
|
||||
ILibraryManager libraryManager,
|
||||
AProviderComposite metadataProvider,
|
||||
IThumbnailsManager thumbnailsManager,
|
||||
ITranscoder transcoder)
|
||||
IFileSystem transcoder)
|
||||
{
|
||||
_identifier = identifier;
|
||||
_libraryManager = libraryManager;
|
||||
|
@ -1,133 +0,0 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Exceptions;
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
using Kyoo.Core.Models.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Kyoo.Core.Api
|
||||
{
|
||||
[Route("video")]
|
||||
[ApiController]
|
||||
public class VideoApi : Controller
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ITranscoder _transcoder;
|
||||
private readonly IOptions<BasicOptions> _options;
|
||||
private readonly IFileSystem _files;
|
||||
|
||||
public VideoApi(ILibraryManager libraryManager,
|
||||
ITranscoder transcoder,
|
||||
IOptions<BasicOptions> options,
|
||||
IFileSystem files)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_transcoder = transcoder;
|
||||
_options = options;
|
||||
_files = files;
|
||||
}
|
||||
|
||||
public override void OnActionExecuted(ActionExecutedContext ctx)
|
||||
{
|
||||
base.OnActionExecuted(ctx);
|
||||
// Disabling the cache prevent an issue on firefox that skip the last 30 seconds of HLS files.
|
||||
ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
ctx.HttpContext.Response.Headers.Add("Pragma", "no-cache");
|
||||
ctx.HttpContext.Response.Headers.Add("Expires", "0");
|
||||
}
|
||||
|
||||
// TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)]
|
||||
[HttpGet("{slug}")]
|
||||
[HttpGet("direct/{slug}")]
|
||||
public async Task<IActionResult> Direct(string slug)
|
||||
{
|
||||
try
|
||||
{
|
||||
Episode episode = await _libraryManager.Get<Episode>(slug);
|
||||
return _files.FileResult(episode.Path, true);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("transmux/{slug}/master.m3u8")]
|
||||
[Permission("video", Kind.Read)]
|
||||
public async Task<IActionResult> Transmux(string slug)
|
||||
{
|
||||
try
|
||||
{
|
||||
Episode episode = await _libraryManager.Get<Episode>(slug);
|
||||
string path = await _transcoder.Transmux(episode);
|
||||
|
||||
if (path == null)
|
||||
return StatusCode(500);
|
||||
return _files.FileResult(path, true);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("transcode/{slug}/master.m3u8")]
|
||||
[Permission("video", Kind.Read)]
|
||||
public async Task<IActionResult> Transcode(string slug)
|
||||
{
|
||||
try
|
||||
{
|
||||
Episode episode = await _libraryManager.Get<Episode>(slug);
|
||||
string path = await _transcoder.Transcode(episode);
|
||||
|
||||
if (path == null)
|
||||
return StatusCode(500);
|
||||
return _files.FileResult(path, true);
|
||||
}
|
||||
catch (ItemNotFoundException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("transmux/{episodeLink}/segments/{chunk}")]
|
||||
[Permission("video", Kind.Read)]
|
||||
public IActionResult GetTransmuxedChunk(string episodeLink, string chunk)
|
||||
{
|
||||
string path = Path.GetFullPath(Path.Combine(_options.Value.TransmuxPath, episodeLink));
|
||||
path = Path.Combine(path, "segments", chunk);
|
||||
return PhysicalFile(path, "video/MP2T");
|
||||
}
|
||||
|
||||
[HttpGet("transcode/{episodeLink}/segments/{chunk}")]
|
||||
[Permission("video", Kind.Read)]
|
||||
public IActionResult GetTranscodedChunk(string episodeLink, string chunk)
|
||||
{
|
||||
string path = Path.GetFullPath(Path.Combine(_options.Value.TranscodePath, episodeLink));
|
||||
path = Path.Combine(path, "segments", chunk);
|
||||
return PhysicalFile(path, "video/MP2T");
|
||||
}
|
||||
}
|
||||
}
|
134
src/Kyoo.Core/Views/Watch/VideoApi.cs
Normal file
134
src/Kyoo.Core/Views/Watch/VideoApi.cs
Normal file
@ -0,0 +1,134 @@
|
||||
// Kyoo - A portable and vast media library solution.
|
||||
// Copyright (c) Kyoo.
|
||||
//
|
||||
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
//
|
||||
// Kyoo is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// Kyoo is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
using Kyoo.Abstractions.Models.Attributes;
|
||||
using Kyoo.Abstractions.Models.Permissions;
|
||||
using Kyoo.Abstractions.Models.Utils;
|
||||
using Kyoo.Core.Models.Options;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Options;
|
||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||
|
||||
namespace Kyoo.Core.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the video in a raw format or transcoded in the codec you want.
|
||||
/// </summary>
|
||||
[Route("videos")]
|
||||
[Route("video", Order = AlternativeRoute)]
|
||||
[ApiController]
|
||||
[ApiDefinition("Videos", Group = WatchGroup)]
|
||||
public class VideoApi : Controller
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IFileSystem _files;
|
||||
|
||||
public VideoApi(ILibraryManager libraryManager,
|
||||
IFileSystem files)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_files = files;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Disabling the cache prevent an issue on firefox that skip the last 30 seconds of HLS files
|
||||
/// </remarks>
|
||||
public override void OnActionExecuted(ActionExecutedContext ctx)
|
||||
{
|
||||
base.OnActionExecuted(ctx);
|
||||
ctx.HttpContext.Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
ctx.HttpContext.Response.Headers.Add("Pragma", "no-cache");
|
||||
ctx.HttpContext.Response.Headers.Add("Expires", "0");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct video
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or
|
||||
/// transmuxing is done.
|
||||
/// </remarks>
|
||||
/// <param name="identifier">The identifier of the episode to retrieve.</param>
|
||||
/// <returns>The raw video stream</returns>
|
||||
/// <response code="404">No episode exists for the given identifier.</response>
|
||||
// TODO enable the following line, this is disabled since the web app can't use bearers. [Permission("video", Kind.Read)]
|
||||
[HttpGet("direct/{identifier:id}")]
|
||||
[HttpGet("{identifier:id}", Order = AlternativeRoute)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Direct(Identifier identifier)
|
||||
{
|
||||
Episode episode = await identifier.Match(
|
||||
id => _libraryManager.GetOrDefault<Episode>(id),
|
||||
slug => _libraryManager.GetOrDefault<Episode>(slug)
|
||||
);
|
||||
return _files.FileResult(episode?.Path, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transmux video
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Change the container of the video to hls but don't re-encode the video or audio. This doesn't require mutch
|
||||
/// resources from the server.
|
||||
/// </remarks>
|
||||
/// <param name="identifier">The identifier of the episode to retrieve.</param>
|
||||
/// <returns>The transmuxed video stream</returns>
|
||||
/// <response code="404">No episode exists for the given identifier.</response>
|
||||
[HttpGet("transmux/{identifier:id}/master.m3u8")]
|
||||
[Permission("video", Kind.Read)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Transmux(Identifier identifier)
|
||||
{
|
||||
Episode episode = await identifier.Match(
|
||||
id => _libraryManager.GetOrDefault<Episode>(id),
|
||||
slug => _libraryManager.GetOrDefault<Episode>(slug)
|
||||
);
|
||||
return _files.Transmux(episode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transmuxed chunk
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retrieve a chunk of a transmuxed video.
|
||||
/// </remarks>
|
||||
/// <param name="episodeLink">The identifier of the episode.</param>
|
||||
/// <param name="chunk">The identifier of the chunk to retrieve.</param>
|
||||
/// <param name="options">The options used to retrieve the path of the segments.</param>
|
||||
/// <returns>A transmuxed video chunk.</returns>
|
||||
[HttpGet("transmux/{episodeLink}/segments/{chunk}", Order = AlternativeRoute)]
|
||||
[Permission("video", Kind.Read)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IActionResult GetTransmuxedChunk(string episodeLink, string chunk,
|
||||
[FromServices] IOptions<BasicOptions> options)
|
||||
{
|
||||
string path = Path.GetFullPath(Path.Combine(options.Value.TransmuxPath, episodeLink));
|
||||
path = Path.Combine(path, "segments", chunk);
|
||||
return PhysicalFile(path, "video/MP2T");
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,6 @@ using NJsonSchema;
|
||||
using NJsonSchema.Generation.TypeMappers;
|
||||
using NSwag;
|
||||
using NSwag.Generation.AspNetCore;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||
|
||||
namespace Kyoo.Swagger
|
||||
@ -90,40 +89,35 @@ namespace Kyoo.Swagger
|
||||
x.Type = JsonObjectType.String | JsonObjectType.Integer;
|
||||
}));
|
||||
|
||||
document.AddSecurity("Kyoo", new OpenApiSecurityScheme()
|
||||
document.AddSecurity("Kyoo", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.OpenIdConnect,
|
||||
Description = "Kyoo's OpenID Authentication",
|
||||
Flow = OpenApiOAuth2Flow.AccessCode,
|
||||
Flows = new OpenApiOAuthFlows()
|
||||
{
|
||||
Implicit = new OpenApiOAuthFlow()
|
||||
{
|
||||
Scopes = new Dictionary<string, string>
|
||||
{
|
||||
{ "read", "Read access to protected resources" },
|
||||
{ "write", "Write access to protected resources" }
|
||||
},
|
||||
AuthorizationUrl = "https://localhost:44333/core/connect/authorize",
|
||||
TokenUrl = "https://localhost:44333/core/connect/token"
|
||||
}
|
||||
}
|
||||
OpenIdConnectUrl = "/.well-known/openid-configuration",
|
||||
Description = "You can login via an OIDC client, clients must be first registered in kyoo. " +
|
||||
"Documentation coming soon."
|
||||
});
|
||||
document.OperationProcessors.Add(new OperationPermissionProcessor());
|
||||
document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Overall.ToString(), new OpenApiSecurityScheme
|
||||
// This does not respect the swagger's specification but it works for swaggerUi and ReDoc so honestly this will do.
|
||||
document.AddSecurity(Group.Overall.ToString(), new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate."
|
||||
}));
|
||||
document.DocumentProcessors.Add(new SecurityDefinitionAppender(Group.Admin.ToString(), new OpenApiSecurityScheme
|
||||
ExtensionData = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "OpenID Connect or Api Key"
|
||||
},
|
||||
Description = "Kyoo's permissions work by groups. Permissions are attributed to " +
|
||||
"a specific group and if a user has a group permission, it will be the same as having every " +
|
||||
"permission in the group. For example, having overall.read gives you collections.read, " +
|
||||
"shows.read and so on."
|
||||
});
|
||||
document.AddSecurity(Group.Admin.ToString(), new OpenApiSecurityScheme
|
||||
{
|
||||
Type = OpenApiSecuritySchemeType.ApiKey,
|
||||
Name = "Authorization",
|
||||
In = OpenApiSecurityApiKeyLocation.Header,
|
||||
Description = "Type into the textbox: Bearer {your JWT token}. You can get a JWT token from /Authorization/Authenticate."
|
||||
}));
|
||||
ExtensionData = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = "OpenID Connect or Api Key"
|
||||
},
|
||||
Description = "The permission group used for administrative items like tasks, account management " +
|
||||
"and library creation."
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user