Reworking the transcoder and creating a file manager

This commit is contained in:
Zoe Roux 2021-03-22 02:52:18 +01:00
parent fdfd42c7c1
commit 57e49b7e83
14 changed files with 235 additions and 180 deletions

View File

@ -0,0 +1,16 @@
using System.IO;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc;
namespace Kyoo.Controllers
{
public interface IFileManager
{
public IActionResult FileResult([NotNull] string path, bool rangeSupport = false);
public StreamReader GetReader([NotNull] string path);
// TODO implement a List for directorys, a Exist to check existance and all.
// TODO replace every use of System.IO with this to allow custom paths (like uptobox://path)
// TODO find a way to handle Transmux/Transcode with this system.
}
}

View File

@ -1,12 +1,11 @@
using Kyoo.Models;
using Kyoo.Models.Watch;
using System.Threading.Tasks;
namespace Kyoo.Controllers
{
public interface ITranscoder
{
Task<Track[]> ExtractInfos(string path);
Task<Track[]> ExtractInfos(string path, bool reextract);
Task<string> Transmux(Episode episode);
Task<string> Transcode(Episode episode);
}

View File

@ -22,6 +22,7 @@
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0-beta-20204-02" PrivateAssets="All" />
</ItemGroup>

View File

@ -12,7 +12,7 @@ namespace Kyoo.Models
Video = 1,
Audio = 2,
Subtitle = 3,
Font = 4
Attachment = 4
}
namespace Watch
@ -100,7 +100,7 @@ namespace Kyoo.Models
StreamType.Subtitle => "",
StreamType.Video => "video.",
StreamType.Audio => "audio.",
StreamType.Font => "font.",
StreamType.Attachment => "font.",
_ => ""
};
string index = TrackIndex != 0 ? $"-{TrackIndex}" : string.Empty;

View File

@ -0,0 +1,41 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Controllers
{
public class FileManager : ControllerBase, IFileManager
{
private FileExtensionContentTypeProvider _provider;
private string _GetContentType(string path)
{
if (_provider == null)
{
_provider = new FileExtensionContentTypeProvider();
_provider.Mappings[".mkv"] = "video/x-matroska";
}
if (_provider.TryGetContentType(path, out string contentType))
return contentType;
return "video/mp4";
}
public IActionResult FileResult(string path, bool range)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
if (!System.IO.File.Exists(path))
return NotFound();
return PhysicalFile(path, _GetContentType(path), range);
}
public StreamReader GetReader(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
return new StreamReader(path);
}
}
}

View File

@ -0,0 +1,134 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Kyoo.Models;
using Microsoft.Extensions.Configuration;
using Stream = Kyoo.Models.Watch.Stream;
// We use threads so tasks are not always awaited.
#pragma warning disable 4014
namespace Kyoo.Controllers
{
public class BadTranscoderException : Exception {}
public class Transcoder : ITranscoder
{
private static class TranscoderAPI
{
private const string TranscoderPath = "libtranscoder.so";
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int init();
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transmux(string path, string outpath, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transcode(string path, string outpath, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr extract_infos(string path,
string outpath,
out int length,
out int trackCount,
bool reextracct);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern void free(IntPtr ptr);
public static Track[] ExtractInfos(string path, string outPath, bool reextract)
{
int size = Marshal.SizeOf<Stream>();
IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount, reextract);
IntPtr streamsPtr = ptr;
Track[] tracks;
if (trackCount > 0 && ptr != IntPtr.Zero)
{
tracks = new Track[trackCount];
int j = 0;
for (int i = 0; i < arrayLength; i++)
{
Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr);
if (stream!.Type != StreamType.Unknown)
{
tracks[j] = new Track(stream);
j++;
}
streamsPtr += size;
}
}
else
tracks = Array.Empty<Track>();
if (ptr != IntPtr.Zero)
free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers.
return tracks;
}
}
private readonly string _transmuxPath;
private readonly string _transcodePath;
public Transcoder(IConfiguration config)
{
_transmuxPath = Path.GetFullPath(config.GetValue<string>("transmuxTempPath"));
_transcodePath = Path.GetFullPath(config.GetValue<string>("transcodeTempPath"));
if (TranscoderAPI.init() != Marshal.SizeOf<Stream>())
throw new BadTranscoderException();
}
public Task<Track[]> ExtractInfos(string path, bool reextract)
{
string dir = Path.GetDirectoryName(path);
if (dir == null)
throw new ArgumentException("Invalid path.");
dir = Path.Combine(dir, "Extra");
return Task.Factory.StartNew(
() => TranscoderAPI.ExtractInfos(path, dir, reextract),
TaskCreationOptions.LongRunning);
}
public async Task<string> Transmux(Episode episode)
{
if (!File.Exists(episode.Path))
throw new ArgumentException("Path does not exists. Can't transcode.");
string folder = Path.Combine(_transmuxPath, episode.Slug);
string manifest = Path.Combine(folder, episode.Slug + ".m3u8");
float playableDuration = 0;
bool transmuxFailed = false;
try
{
Directory.CreateDirectory(folder);
if (File.Exists(manifest))
return manifest;
}
catch (UnauthorizedAccessException)
{
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config.");
return null;
}
Task.Factory.StartNew(() =>
{
string cleanManifest = manifest.Replace('\\', '/');
transmuxFailed = TranscoderAPI.transmux(episode.Path, cleanManifest, out playableDuration) != 0;
}, TaskCreationOptions.LongRunning);
while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed)
await Task.Delay(10);
return transmuxFailed ? null : manifest;
}
public Task<string> Transcode(Episode episode)
{
return Task.FromResult<string>(null); // Not implemented yet.
}
}
}

View File

@ -1,70 +0,0 @@
using System;
using Kyoo.Models;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Kyoo.Controllers.TranscoderLink;
#pragma warning disable 4014
namespace Kyoo.Controllers
{
public class BadTranscoderException : Exception {}
public class Transcoder : ITranscoder
{
private readonly string _transmuxPath;
private readonly string _transcodePath;
public Transcoder(IConfiguration config)
{
_transmuxPath = Path.GetFullPath(config.GetValue<string>("transmuxTempPath"));
_transcodePath = Path.GetFullPath(config.GetValue<string>("transcodeTempPath"));
if (TranscoderAPI.init() != Marshal.SizeOf<Models.Watch.Stream>())
throw new BadTranscoderException();
}
public Task<Track[]> ExtractInfos(string path)
{
string dir = Path.GetDirectoryName(path);
if (dir == null)
throw new ArgumentException("Invalid path.");
return Task.Factory.StartNew(() => TranscoderAPI.ExtractInfos(path, dir), TaskCreationOptions.LongRunning);
}
public async Task<string> Transmux(Episode episode)
{
string folder = Path.Combine(_transmuxPath, episode.Slug);
string manifest = Path.Combine(folder, episode.Slug + ".m3u8");
float playableDuration = 0;
bool transmuxFailed = false;
try
{
Directory.CreateDirectory(folder);
if (File.Exists(manifest))
return manifest;
}
catch (UnauthorizedAccessException)
{
await Console.Error.WriteLineAsync($"Access to the path {manifest} is denied. Please change your transmux path in the config.");
return null;
}
Task.Factory.StartNew(() =>
{
transmuxFailed = TranscoderAPI.transmux(episode.Path, manifest.Replace('\\', '/'), out playableDuration) != 0;
}, TaskCreationOptions.LongRunning);
while (playableDuration < 10 || !File.Exists(manifest) && !transmuxFailed)
await Task.Delay(10);
return transmuxFailed ? null : manifest;
}
public Task<string> Transcode(Episode episode)
{
return Task.FromResult<string>(null); // Not implemented yet.
}
}
}

View File

@ -1,60 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Kyoo.Models;
using Kyoo.Models.Watch;
// ReSharper disable InconsistentNaming
namespace Kyoo.Controllers.TranscoderLink
{
public static class TranscoderAPI
{
private const string TranscoderPath = "libtranscoder.so";
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int init();
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transmux(string path, string out_path, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
public static extern int transcode(string path, string out_path, out float playableDuration);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr extract_infos(string path, string outpath, out int length, out int track_count);
[DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)]
private static extern void free(IntPtr stream_ptr);
public static Track[] ExtractInfos(string path, string outPath)
{
int size = Marshal.SizeOf<Stream>();
IntPtr ptr = extract_infos(path, outPath, out int arrayLength, out int trackCount);
IntPtr streamsPtr = ptr;
Track[] tracks;
if (trackCount > 0 && ptr != IntPtr.Zero)
{
tracks = new Track[trackCount];
int j = 0;
for (int i = 0; i < arrayLength; i++)
{
Stream stream = Marshal.PtrToStructure<Stream>(streamsPtr);
if (stream!.Type != StreamType.Unknown)
{
tracks[j] = new Track(stream);
j++;
}
streamsPtr += size;
}
}
else
tracks = Array.Empty<Track>();
if (ptr != IntPtr.Zero)
free(ptr); // free_streams is not necesarry since the Marshal free the unmanaged pointers.
return tracks;
}
}
}

View File

@ -158,6 +158,7 @@ namespace Kyoo
services.AddScoped<DbContext, DatabaseContext>();
services.AddScoped<ILibraryManager, LibraryManager>();
services.AddScoped<IFileManager, FileManager>();
services.AddSingleton<ITranscoder, Transcoder>();
services.AddSingleton<IThumbnailsManager, ThumbnailsManager>();
services.AddSingleton<IProviderManager, ProviderManager>();

View File

@ -359,8 +359,8 @@ namespace Kyoo.Controllers
private async Task<ICollection<Track>> GetTracks(Episode episode)
{
episode.Tracks = (await _transcoder.ExtractInfos(episode.Path))
.Where(x => x.Type != StreamType.Font)
episode.Tracks = (await _transcoder.ExtractInfos(episode.Path, false))
.Where(x => x.Type != StreamType.Attachment)
.ToArray();
return episode.Tracks;
}

View File

@ -101,8 +101,8 @@ namespace Kyoo.Tasks
if (subs)
{
await _library.Load(episode, x => x.Tracks);
episode.Tracks = (await _transcoder!.ExtractInfos(episode.Path))
.Where(x => x.Type != StreamType.Font)
episode.Tracks = (await _transcoder!.ExtractInfos(episode.Path, true))
.Where(x => x.Type != StreamType.Attachment)
.Concat(episode.Tracks.Where(x => x.IsExternal))
.ToList();
await _library.EditEpisode(episode, false);

View File

@ -14,10 +14,12 @@ namespace Kyoo.Api
public class SubtitleApi : ControllerBase
{
private readonly ILibraryManager _libraryManager;
private readonly IFileManager _files;
public SubtitleApi(ILibraryManager libraryManager)
public SubtitleApi(ILibraryManager libraryManager, IFileManager files)
{
_libraryManager = libraryManager;
_files = files;
}
@ -39,9 +41,8 @@ namespace Kyoo.Api
return NotFound();
if (subtitle.Codec == "subrip" && extension == "vtt")
return new ConvertSubripToVtt(subtitle.Path);
string mime = subtitle.Codec == "ass" ? "text/x-ssa" : "application/x-subrip";
return PhysicalFile(subtitle.Path, mime);
return new ConvertSubripToVtt(subtitle.Path, _files);
return _files.FileResult(subtitle.Path);
}
}
@ -49,27 +50,29 @@ namespace Kyoo.Api
public class ConvertSubripToVtt : IActionResult
{
private readonly string _path;
private readonly IFileManager _files;
public ConvertSubripToVtt(string subtitlePath)
public ConvertSubripToVtt(string subtitlePath, IFileManager files)
{
_path = subtitlePath;
_files = files;
}
public async Task ExecuteResultAsync(ActionContext context)
{
string line;
List<string> lines = new List<string>();
List<string> lines = new();
context.HttpContext.Response.StatusCode = 200;
context.HttpContext.Response.Headers.Add("Content-Type", "text/vtt");
await using (StreamWriter writer = new StreamWriter(context.HttpContext.Response.Body))
await using (StreamWriter writer = new(context.HttpContext.Response.Body))
{
await writer.WriteLineAsync("WEBVTT");
await writer.WriteLineAsync("");
await writer.WriteLineAsync("");
using StreamReader reader = new StreamReader(_path);
using StreamReader reader = _files.GetReader(_path);
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line == "")

View File

@ -1,12 +1,11 @@
using Kyoo.Controllers;
using System.IO;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.StaticFiles;
namespace Kyoo.Api
{
@ -16,14 +15,18 @@ namespace Kyoo.Api
{
private readonly ILibraryManager _libraryManager;
private readonly ITranscoder _transcoder;
private readonly IFileManager _files;
private readonly string _transmuxPath;
private readonly string _transcodePath;
private FileExtensionContentTypeProvider _provider;
public VideoApi(ILibraryManager libraryManager, ITranscoder transcoder, IConfiguration config)
public VideoApi(ILibraryManager libraryManager,
ITranscoder transcoder,
IConfiguration config,
IFileManager files)
{
_libraryManager = libraryManager;
_transcoder = transcoder;
_files = files;
_transmuxPath = config.GetValue<string>("transmuxTempPath");
_transcodePath = config.GetValue<string>("transcodeTempPath");
}
@ -37,19 +40,6 @@ namespace Kyoo.Api
ctx.HttpContext.Response.Headers.Add("Expires", "0");
}
private string _GetContentType(string path)
{
if (_provider == null)
{
_provider = new FileExtensionContentTypeProvider();
_provider.Mappings[".mkv"] = "video/x-matroska";
}
if (_provider.TryGetContentType(path, out string contentType))
return contentType;
return "video/mp4";
}
[HttpGet("{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
[HttpGet("direct/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}")]
@ -60,9 +50,9 @@ namespace Kyoo.Api
return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode != null && System.IO.File.Exists(episode.Path))
return PhysicalFile(episode.Path, _GetContentType(episode.Path), true);
return NotFound();
if (episode == null)
return NotFound();
return _files.FileResult(episode.Path, true);
}
[HttpGet("{movieSlug}")]
@ -72,9 +62,9 @@ namespace Kyoo.Api
{
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode != null && System.IO.File.Exists(episode.Path))
return PhysicalFile(episode.Path, _GetContentType(episode.Path), true);
return NotFound();
if (episode == null)
return NotFound();
return _files.FileResult(episode.Path, true);
}
@ -86,12 +76,12 @@ namespace Kyoo.Api
return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null || !System.IO.File.Exists(episode.Path))
if (episode == null)
return NotFound();
string path = await _transcoder.Transmux(episode);
if (path == null)
return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true);
return _files.FileResult(path, true);
}
[HttpGet("transmux/{movieSlug}/master.m3u8")]
@ -100,12 +90,12 @@ namespace Kyoo.Api
{
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null || !System.IO.File.Exists(episode.Path))
if (episode == null)
return NotFound();
string path = await _transcoder.Transmux(episode);
if (path == null)
return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true);
return _files.FileResult(path, true);
}
[HttpGet("transcode/{showSlug}-s{seasonNumber:int}e{episodeNumber:int}/master.m3u8")]
@ -116,12 +106,12 @@ namespace Kyoo.Api
return BadRequest(new {error = "Season number or episode number can not be negative."});
Episode episode = await _libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
if (episode == null || !System.IO.File.Exists(episode.Path))
if (episode == null)
return NotFound();
string path = await _transcoder.Transcode(episode);
if (path == null)
return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true);
return _files.FileResult(path, true);
}
[HttpGet("transcode/{movieSlug}/master.m3u8")]
@ -130,12 +120,12 @@ namespace Kyoo.Api
{
Episode episode = await _libraryManager.GetMovieEpisode(movieSlug);
if (episode == null || !System.IO.File.Exists(episode.Path))
if (episode == null)
return NotFound();
string path = await _transcoder.Transcode(episode);
if (path == null)
return StatusCode(500);
return PhysicalFile(path, "application/x-mpegurl", true);
return _files.FileResult(path, true);
}

@ -1 +1 @@
Subproject commit 3885dca743bbde5d83cb3816646455856fc5c316
Subproject commit 9acd635aca92ad81f1de562e34b2c7c270bade29