// 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 . using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Core.Models.Options; using Kyoo.Core.Models.Watch; using Kyoo.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Kyoo.Core.Controllers { /// /// The transcoder used by the . /// public class Transcoder : ITranscoder { #pragma warning disable IDE1006 /// /// The class that interact with the transcoder written in C. /// private static class TranscoderAPI { /// /// 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. /// private const string TranscoderPath = "transcoder"; /// /// Initialize the C library, setup the logger and return the size of a . /// /// The size of a [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] private static extern int init(); /// /// Initialize the C library, setup the logger and return the size of a . /// /// The size of a public static int Init() => init(); /// /// Transmux the file at the specified path. The path must be a local one with '/' as a separator. /// /// The path of a local file with '/' as a separators. /// The path of the hls output file. /// /// The number of seconds currently playable. This is incremented as the file gets transmuxed. /// /// 0 on success, non 0 on failure. [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] private static extern int transmux(string path, string outPath, out float playableDuration); /// /// Transmux the file at the specified path. The path must be a local one. /// /// The path of a local file. /// The path of the hls output file. /// /// The number of seconds currently playable. This is incremented as the file gets transmuxed. /// /// 0 on success, non 0 on failure. public static int Transmux(string path, string outPath, out float playableDuration) { path = path.Replace('\\', '/'); outPath = outPath.Replace('\\', '/'); return transmux(path, outPath, out playableDuration); } /// /// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file. /// /// /// The path of the video file to analyse. This must be a local path with '/' as a separator. /// /// The directory that will be used to store extracted files. /// The size of the returned array. /// The number of tracks in the returned array. /// Should the cache be invalidated and information re-extracted or not? /// A pointer to an array of [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] private static extern IntPtr extract_infos(string path, string outPath, out uint length, out uint trackCount, bool reExtract); /// /// An helper method to free an array of . /// /// A pointer to the first element of the array /// The number of items in the array. [DllImport(TranscoderPath, CallingConvention = CallingConvention.Cdecl)] private static extern void free_streams(IntPtr streams, uint count); /// /// Retrieve tracks from a video file and extract subtitles, fonts and chapters to an external file. /// /// The path of the video file to analyse. This must be a local path. /// The directory that will be used to store extracted files. /// Should the cache be invalidated and information re-extracted or not? /// An array of . public static Track[] ExtractInfos(string path, string outPath, bool reExtract) { path = path.Replace('\\', '/'); outPath = outPath.Replace('\\', '/'); int size = Marshal.SizeOf(); IntPtr ptr = extract_infos(path, outPath, out uint arrayLength, out uint 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++) { FTrack stream = Marshal.PtrToStructure(streamsPtr); if (stream!.Type != FTrackType.Unknown && stream.Type != FTrackType.Attachment) { tracks[j] = stream.ToTrack(); j++; } streamsPtr += size; } } else tracks = Array.Empty(); if (ptr != IntPtr.Zero) free_streams(ptr, trackCount); return tracks; } } #pragma warning restore IDE1006 /// /// The file system used to retrieve the extra directory of shows to know where to extract information. /// private readonly IFileSystem _files; /// /// Options to know where to cache transmuxed/transcoded episodes. /// private readonly IOptions _options; /// /// The logger to use. This is also used by the wrapped C library. /// private readonly ILogger _logger; /// /// Create a new . /// /// /// The file system used to retrieve the extra directory of shows to know where to extract information. /// /// Options to know where to cache transmuxed/transcoded episodes. /// The logger to use. This is also used by the wrapped C library. public Transcoder(IFileSystem files, IOptions options, ILogger logger) { _files = files; _options = options; _logger = logger; if (TranscoderAPI.Init() != Marshal.SizeOf()) _logger.LogCritical("The transcoder library could not be initialized correctly"); } /// public async Task> ExtractInfos(Episode episode, bool reExtract) { 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 ); } /// public async Task> ListFonts(Episode episode) { string path = _files.Combine(await _files.GetExtraDirectory(episode), "Attachments"); return (await _files.ListFiles(path)) .Select(x => new Font(x)) .ToArray(); } /// public async Task GetFont(Episode episode, string slug) { string path = _files.Combine(await _files.GetExtraDirectory(episode), "Attachments"); string font = (await _files.ListFiles(path)) .FirstOrDefault(x => Utility.ToSlug(Path.GetFileNameWithoutExtension(x)) == slug); if (font == null) return null; return new Font(font); } /// public IActionResult Transmux(Episode episode) { string folder = Path.Combine(_options.Value.TransmuxPath, episode.Slug); string manifest = Path.GetFullPath(Path.Combine(folder, episode.Slug + ".m3u8")); try { Directory.CreateDirectory(folder); if (File.Exists(manifest)) return new PhysicalFileResult(manifest, "application/x-mpegurl"); } catch (UnauthorizedAccessException) { _logger.LogCritical("Access to the path {Manifest} is denied. " + "Please change your transmux path in the config", manifest); return new StatusCodeResult(500); } return new TransmuxResult(episode.Path, manifest, _logger); } /// /// 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. /// private class TransmuxResult : IActionResult { /// /// The path of the episode to transmux. It must be a local one. /// private readonly string _path; /// /// The path of the manifest file to create. It must be a local one. /// private readonly string _manifest; /// /// The logger to use in case of issue. /// private readonly ILogger _logger; /// /// Create a new . /// /// The path of the episode to transmux. It must be a local one. /// The path of the manifest file to create. It must be a local one. /// The logger to use in case of issue. 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 /// 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 } } }