├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── Uploader.Core.Test ├── .vscode │ ├── launch.json │ └── tasks.json ├── Program.cs └── Uploader.Core.Test.csproj ├── Uploader.Core ├── Log │ ├── Log4NetLogger.cs │ ├── Log4NetProvider.cs │ └── Log4netExtensions.cs ├── Managers │ ├── Common │ │ ├── BaseDaemon.cs │ │ ├── CheckManager.cs │ │ ├── LogManager.cs │ │ ├── ProcessManager.cs │ │ ├── Startup.cs │ │ ├── TempFileManager.cs │ │ └── Tools.cs │ ├── Front │ │ ├── GeneralSettings.cs │ │ ├── OverlayManager.cs │ │ ├── ProgressManager.cs │ │ ├── SubtitleManager.cs │ │ └── VideoManager.cs │ ├── Ipfs │ │ ├── IpfsAddManager.cs │ │ ├── IpfsDaemon.cs │ │ └── IpfsSettings.cs │ └── Video │ │ ├── AudioCpuEncodeDaemon.cs │ │ ├── AudioVideoCpuEncodeDaemon.cs │ │ ├── EncodeManager.cs │ │ ├── FfProbeProcessManager.cs │ │ ├── FfmpegProcessManager.cs │ │ ├── SizeHelper.cs │ │ ├── SpriteDaemon.cs │ │ ├── SpriteManager.cs │ │ ├── VideoGpuEncodeDaemon.cs │ │ ├── VideoSettings.cs │ │ └── VideoSourceManager.cs ├── Models │ ├── FileContainer.cs │ ├── FileItem.cs │ ├── ProcessItem.cs │ ├── ProcessStep.cs │ ├── TypeContainer.cs │ ├── TypeFile.cs │ └── VideoSize.cs ├── Uploader.Core.csproj └── overlay.png ├── Uploader.Web ├── .vscode │ ├── launch.json │ └── tasks.json ├── Attributes │ └── DisableFormValueModelBindingAttribute.cs ├── Controllers │ ├── ProgressController.cs │ └── UploaderController.cs ├── Helpers │ ├── FileStreamingHelper.cs │ └── MultipartRequestHelper.cs ├── Program.cs ├── Startup.cs ├── Uploader.Web.csproj ├── appsettings.Development.json ├── appsettings.json ├── appsettings.prod_cpu.json ├── appsettings.prod_gpu.json ├── log4net.config ├── overlay.png └── wwwroot │ ├── index.html │ └── scripts │ ├── fine-uploader-gallery.min.css │ ├── fine-uploader.min.js │ ├── loading.gif │ └── retry.gif └── Uploader.sln /.gitignore: -------------------------------------------------------------------------------- 1 | /**/bin/* 2 | /**/obj/* 3 | /**/logs/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/Uploader.Web/bin/Debug/netcoreapp2.0/Uploader.Web.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/Uploader.Web", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start http://localhost:5000/index.html" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Uploader.Web/Uploader.Web.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipfs-uploader 2 | 3 | Files manipulation and adds to IPFS gracefully 4 | 5 | Confirmed working on Windows 7+ and Linux. Untested on mac. 6 | 7 | Dependencies required: 8 | * either `dotnet-sdk-2.0` for dev 9 | * or `dotnet-runtime` for production 10 | * `ffmpeg` 11 | * `ffprobe` 12 | * `imagemagick` 13 | * `ipfs` (go-ipfs) with a running deamon 14 | 15 | ## Starting it in dev mode 16 | Navigate to the `Uploader.Web` directory and just do `dotnet run` 17 | 18 | ## Available Calls 19 | Once your instance is started, it should listen on localhost:5000 by default and serve all these calls. 20 | ## GET 21 | * `/getStatus` and `/getStatus?details=true` will give some information about the running instance such as queues times and usage statistics 22 | * `/getErrors` will give information about recent errors 23 | * `/index.html` allows you to easily test video, image and subtitles uploads. Only available in dev mode 24 | * `/getProgressByToken/:token` answers with a json object representing the progress of the upload 25 | * * token: is the token given after a succesful `/uploadVideo` or `/uploadImage` 26 | * `/getProgressBySourceHash/:token` same as above but takes the source video hash as input 27 | 28 | ## POST 29 | * `/uploadVideo` The main video upload call 30 | * POST file input `files`. Needs to be a video otherwise it will end up as an error. 31 | * * Query String Options 32 | * videoEncodingFormats: controls which video resolutions are requested for the encoding 33 | * sprite: true/false. Controls whether a sprite file should be generated for this video. 34 | 35 | Curl example: ``curl -F "video=@./video.mp4" http://localhost:5000/uploadVideo?videoEncodingFormats=240p,480p,720p&sprite=true`` 36 | 37 | * `/uploadImage` 38 | * POST file input `files`. Needs to be an image otherwise it will end up as an error. 39 | 40 | * `/uploadSubtitle` 41 | * POST text input `subtitle`. Needs to be a string starting with WEBVTT otherwise it will end up as an error. 42 | 43 | ## Configuration 44 | The main config file is available as `appsettings.json`. 45 | 46 | * Front : 47 | * "CORS": "https://d.tube" Cross-origin domains to authorize requests from. 48 | * General : 49 | * "MaxGetProgressCanceled": 20, The number of seconds after which an upload gets cancelled if the user doesn't request a `/getProgressByX`. 50 | * "ImageMagickPath": "", The path to imagemagick (leave blank if you use linux). 51 | * "TempFilePath": "/tmp/dtube", Path of temporary files created during the encoding and ipfs processes. 52 | * "ErrorFilePath": "/home/dtube/errors", Path where uploaded files creating errors get stored . 53 | * "FinalFilePath": "/home/dtube/success", Path where succesfully uploaded files to IPFS get stored (if OnlyHash option is true). 54 | * "Version": "0.7.5" The version of the running instance. 55 | * Encode (array) : the list of possible video qualities encoded by this instance. 56 | * "urlTag": "720p", The tag that represents a quality. 57 | * "maxRate": "2000k", The video bitrate. 58 | * "width": 1280, Video width. 59 | * "height": 720, Video height. 60 | * "MinSourceHeightForEncoding": 600, Minimum source height for allowing encoding to this quality. 61 | * "qualityOrder": 5 To order different qualities. Higher is higher quality. 62 | * Ipfs : 63 | * "IpfsTimeout": 108000, IPFS add timeout in seconds 64 | * "VideoAndSpriteTrickleDag": true, If true, uses -t (trickle dag) option for adding video and sprite files to IPFS. Recommended. 65 | * "AddVideoSource": true, If true, the source file also gets added to IPFS. 66 | * "OnlyHash": false If true, uses --only-hash option of IPFS and moves files to the FinalFilePath instead or adding to the ipfs datastore directly. 67 | * Video : 68 | * "FfProbeTimeout": 10, ffprobe timeout in seconds. 69 | * "EncodeGetImagesTimeout": 600, Timeout in seconds for generating all pictures composing the sprite. 70 | * "EncodeTimeout": 108000, Timeout in seconds for video encoding. 71 | * "MaxVideoDurationForEncoding": 1800, Maximum video duration in seconds for video encoding. 72 | * "NbSpriteImages": 100, Number of total images used in the sprite. 73 | * "HeightSpriteImages": 118, Sprite height. 74 | * "GpuEncodeMode": true, Enables GPU-encoding. 75 | * "NbSpriteDaemon": 0, Number of daemons generating sprites. 76 | * "NbAudioVideoCpuEncodeDaemon": 0, Number of daemons for CPU audio and video encoding. 77 | * "NbAudioCpuEncodeDaemon": 0, Number of daemons for CPU audio only encoding (used when doing GPU video encoding). 78 | * "NbVideoGpuEncodeDaemon": 0, Number of daemons for GPU video encoding. 79 | * "AuthorizedQuality": "240p,480p,720p", Authorized encoding qualities. Any other requested quality will be ignored. 80 | * "NVidiaCard":"QuadroP5000" GPU card model. 81 | 82 | ## Building and running it in production 83 | 84 | ### Building 85 | 86 | `dotnet publish -c Release` will create a /bin/Release/netcoreapp2.0/publish folder 87 | 88 | ### Running 89 | 90 | Navigate to the publish folder and `dotnet Uploader.Web.dll` 91 | 92 | ### Logging 93 | 94 | Everything gets logged inside the `logs` folder and each process has it's own log file (ffmpeg, ipfs, etc). If you want logs in a different folder, you need to edit the `log4net.config` file. 95 | -------------------------------------------------------------------------------- /Uploader.Core.Test/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/Uploader.Core.Test.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /Uploader.Core.Test/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Uploader.Core.Test.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Uploader.Core.Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Uploader.Core.Managers.Common; 3 | 4 | namespace Uploader.Core.Test 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | bool success1 = CheckManager.CheckFfmpeg(); 11 | bool success2 = CheckManager.CheckFfprobe(); 12 | bool success3 = CheckManager.CheckImageMagickComposite(); 13 | bool success4 = CheckManager.CheckImageMagickConvert(); 14 | bool success5 = CheckManager.CheckAndLaunchIpfsDaemon(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Uploader.Core.Test/Uploader.Core.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Exe 9 | netcoreapp2.0 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Uploader.Core/Log/Log4NetLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Xml; 4 | using log4net; 5 | using log4net.Repository; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Uploader.Core.Log 9 | { 10 | public class Log4NetLogger : ILogger 11 | { 12 | private readonly string _name; 13 | private readonly XmlElement _xmlElement; 14 | private readonly ILog _log; 15 | private ILoggerRepository _loggerRepository; 16 | public Log4NetLogger(string name, XmlElement xmlElement) 17 | { 18 | _name = name; 19 | _xmlElement = xmlElement; 20 | _loggerRepository = log4net.LogManager.CreateRepository(Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy)); 21 | _log = LogManager.GetLogger(_loggerRepository.Name, name); 22 | log4net.Config.XmlConfigurator.Configure(_loggerRepository, xmlElement); 23 | } 24 | public IDisposable BeginScope(TState state) 25 | { 26 | return null; 27 | } 28 | 29 | public bool IsEnabled(LogLevel logLevel) 30 | { 31 | switch (logLevel) 32 | { 33 | case LogLevel.Critical: 34 | return _log.IsFatalEnabled; 35 | 36 | case LogLevel.Debug: 37 | case LogLevel.Trace: 38 | return _log.IsDebugEnabled; 39 | 40 | case LogLevel.Error: 41 | return _log.IsErrorEnabled; 42 | 43 | case LogLevel.Information: 44 | return _log.IsInfoEnabled; 45 | 46 | case LogLevel.Warning: 47 | return _log.IsWarnEnabled; 48 | 49 | default: 50 | throw new ArgumentOutOfRangeException(nameof(logLevel)); 51 | } 52 | } 53 | 54 | public void Log(LogLevel logLevel, EventId eventId, TState state, 55 | Exception exception, Func formatter) 56 | { 57 | if (!IsEnabled(logLevel)) 58 | { 59 | return; 60 | } 61 | 62 | if (formatter == null) 63 | { 64 | throw new ArgumentNullException(nameof(formatter)); 65 | } 66 | 67 | string message = null; 68 | if (null != formatter) 69 | { 70 | message = formatter(state, exception); 71 | } 72 | 73 | if (!string.IsNullOrEmpty(message) || exception != null) 74 | { 75 | switch (logLevel) 76 | { 77 | case LogLevel.Critical: 78 | _log.Fatal(message); 79 | break; 80 | 81 | case LogLevel.Debug: 82 | case LogLevel.Trace: 83 | _log.Debug(message); 84 | break; 85 | 86 | case LogLevel.Error: 87 | _log.Error(message); 88 | break; 89 | 90 | case LogLevel.Information: 91 | _log.Info(message); 92 | break; 93 | 94 | case LogLevel.Warning: 95 | _log.Warn(message); 96 | break; 97 | 98 | default: 99 | _log.Warn($"Encountered unknown log level {logLevel}, writing out as Info."); 100 | _log.Info(message, exception); 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /Uploader.Core/Log/Log4NetProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.IO; 3 | using System.Xml; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Uploader.Core.Log 7 | { 8 | public class Log4NetProvider : ILoggerProvider 9 | { 10 | private readonly string _log4NetConfigFile; 11 | private readonly ConcurrentDictionary _loggers = 12 | new ConcurrentDictionary(); 13 | public Log4NetProvider(string log4NetConfigFile) 14 | { 15 | _log4NetConfigFile = log4NetConfigFile; 16 | } 17 | 18 | public ILogger CreateLogger(string categoryName) 19 | { 20 | return _loggers.GetOrAdd(categoryName, CreateLoggerImplementation); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | _loggers.Clear(); 26 | } 27 | private Log4NetLogger CreateLoggerImplementation(string name) 28 | { 29 | return new Log4NetLogger(name, Parselog4NetConfigFile(_log4NetConfigFile)); 30 | } 31 | 32 | private static XmlElement Parselog4NetConfigFile(string filename) 33 | { 34 | XmlDocument log4netConfig = new XmlDocument(); 35 | log4netConfig.Load(File.OpenRead(filename)); 36 | return log4netConfig["log4net"]; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Uploader.Core/Log/Log4netExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Uploader.Core.Log 4 | { 5 | public static class Log4netExtensions 6 | { 7 | public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string log4NetConfigFile) 8 | { 9 | factory.AddProvider(new Log4NetProvider(log4NetConfigFile)); 10 | return factory; 11 | } 12 | 13 | public static ILoggerFactory AddLog4Net(this ILoggerFactory factory) 14 | { 15 | factory.AddProvider(new Log4NetProvider("log4net.config")); 16 | return factory; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/BaseDaemon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | using Uploader.Core.Managers.Common; 9 | using Uploader.Core.Managers.Front; 10 | using Uploader.Core.Managers.Ipfs; 11 | using Uploader.Core.Managers.Video; 12 | using Uploader.Core.Models; 13 | 14 | namespace Uploader.Core.Managers.Common 15 | { 16 | internal abstract class BaseDaemon 17 | { 18 | private ConcurrentQueue queueFileItems = new ConcurrentQueue(); 19 | 20 | private List daemons = new List(); 21 | 22 | public int CurrentPositionInQueue 23 | { 24 | get; 25 | private set; 26 | } 27 | 28 | public int TotalAddToQueue 29 | { 30 | get; 31 | private set; 32 | } 33 | 34 | protected void Start(int parralelTask) 35 | { 36 | for (int i = 0; i < parralelTask; i++) 37 | { 38 | Task daemon = Task.Run(() => 39 | { 40 | while (true) 41 | { 42 | FileItem fileItem = null; 43 | try 44 | { 45 | Thread.Sleep(1000); 46 | 47 | fileItem = null; 48 | 49 | if (!queueFileItems.TryDequeue(out fileItem)) 50 | { 51 | continue; 52 | } 53 | 54 | CurrentPositionInQueue++; 55 | 56 | ProcessItem(fileItem); 57 | } 58 | catch(Exception ex) 59 | { 60 | LogException(fileItem, ex); 61 | } 62 | 63 | // Nettoyer les fichiers au besoin 64 | fileItem.FileContainer.CleanFilesIfEnd(); 65 | } 66 | }); 67 | daemons.Add(daemon); 68 | } 69 | } 70 | 71 | protected abstract void ProcessItem(FileItem fileItem); 72 | 73 | protected abstract void LogException(FileItem fileItem, Exception ex); 74 | 75 | protected void Queue(FileItem fileItem, ProcessItem processItem) 76 | { 77 | queueFileItems.Enqueue(fileItem); 78 | TotalAddToQueue++; 79 | 80 | processItem.SavePositionInQueue(TotalAddToQueue, CurrentPositionInQueue); 81 | processItem.SetProgress("Waiting in queue...", true); 82 | } 83 | 84 | public int CurrentWaitingInQueue => TotalAddToQueue - CurrentPositionInQueue; 85 | } 86 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/CheckManager.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Managers.Common 2 | { 3 | public static class CheckManager 4 | { 5 | public static bool CheckAndLaunchIpfsDaemon() 6 | { 7 | //IPFS_PATH=~/monDossierIPFS/ ipfs init 8 | //todo comment changer sous windows le chemin des fichiers ipfs 9 | 10 | var process0 = new ProcessManager("ipfs", "version", LogManager.IpfsLogger); 11 | bool success0 = process0.Launch(2); 12 | if(!success0) 13 | { 14 | return false; 15 | } 16 | if(!process0.DataOutput.ToString().Contains("ipfs version 0.4.")) 17 | { 18 | return false; 19 | } 20 | 21 | var process1 = new ProcessManager("ipfs", "stats bw", LogManager.IpfsLogger); 22 | bool success1 = process1.Launch(5); 23 | if(success1) 24 | return true; //le process ipfs est déjà lancé 25 | 26 | bool mustStart = false; 27 | if(process1.ErrorOutput.ToString().Contains("please run: 'ipfs init'")) 28 | { 29 | var process2 = new ProcessManager("ipfs", "init", LogManager.IpfsLogger); 30 | bool success2 = process2.Launch(10); 31 | if(!success2) 32 | { 33 | return false; // echec ipfs init 34 | } 35 | if(!process2.DataOutput.ToString().Contains("generating 2048-bit RSA keypair...done")) 36 | { 37 | return false; // echec ipfs init 38 | } 39 | mustStart = true; 40 | } 41 | if(mustStart || process1.ErrorOutput.ToString().Contains("Error: This command must be run in online mode. Try")) 42 | { 43 | var process3 = new ProcessManager("ipfs", "daemon", LogManager.IpfsLogger); 44 | return process3.LaunchWithoutTracking(); // echec ipfs daemon 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public static bool CheckFfmpeg() 51 | { 52 | var process = new ProcessManager("ffmpeg", "-version", LogManager.FfmpegLogger); 53 | bool success = process.Launch(1); 54 | if(!success) 55 | return false; 56 | 57 | if(process.DataOutput.ToString().StartsWith("ffmpeg version 3.")) 58 | { 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | public static bool CheckFfprobe() 66 | { 67 | var process = new ProcessManager("ffprobe", "-version", LogManager.FfmpegLogger); 68 | bool success = process.Launch(1); 69 | if(!success) 70 | return false; 71 | 72 | if(process.DataOutput.ToString().StartsWith("ffprobe version 3.")) 73 | { 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | public static bool CheckImageMagickComposite() 81 | { 82 | var process = new ProcessManager("C:\\ImageMagick\\composite", "-version", LogManager.SpriteLogger); 83 | bool success = process.Launch(1); 84 | if(!success) 85 | return false; 86 | 87 | if(process.DataOutput.ToString().StartsWith("Version: ImageMagick 7.")) 88 | { 89 | return true; 90 | } 91 | 92 | return false; 93 | } 94 | 95 | public static bool CheckImageMagickConvert() 96 | { 97 | var process = new ProcessManager("C:\\ImageMagick\\convert", "-version", LogManager.SpriteLogger); 98 | bool success = process.Launch(1); 99 | if(!success) 100 | return false; 101 | 102 | if(process.DataOutput.ToString().StartsWith("Version: ImageMagick 7.")) 103 | { 104 | return true; 105 | } 106 | 107 | return false; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/LogManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Uploader.Core.Managers.Common 8 | { 9 | public static class LogManager 10 | { 11 | public static void Init(ILoggerFactory loggerFactory) 12 | { 13 | GeneralLogger = loggerFactory.CreateLogger("general"); 14 | FfmpegLogger = loggerFactory.CreateLogger("ffmpeg"); 15 | SpriteLogger = loggerFactory.CreateLogger("sprite"); 16 | ImageLogger = loggerFactory.CreateLogger("image"); 17 | SubtitleLogger = loggerFactory.CreateLogger("subtitle"); 18 | IpfsLogger = loggerFactory.CreateLogger("ipfs"); 19 | } 20 | 21 | public static ILogger GeneralLogger { get; private set; } 22 | public static ILogger FfmpegLogger { get; private set; } 23 | public static ILogger SpriteLogger { get; private set; } 24 | public static ILogger IpfsLogger { get; private set; } 25 | public static ILogger ImageLogger { get; private set; } 26 | public static ILogger SubtitleLogger { get; private set; } 27 | 28 | public static void AddGeneralMessage(LogLevel logLevel, string message, string typeMessage, Exception exception = null) 29 | { 30 | Log(GeneralLogger, logLevel, message, typeMessage, exception); 31 | } 32 | 33 | public static void AddEncodingMessage(LogLevel logLevel, string message, string typeMessage, Exception exception = null) 34 | { 35 | Log(FfmpegLogger, logLevel, message, typeMessage, exception); 36 | } 37 | 38 | public static void AddIpfsMessage(LogLevel logLevel, string message, string typeMessage, Exception exception = null) 39 | { 40 | Log(IpfsLogger, logLevel, message, typeMessage, exception); 41 | } 42 | 43 | public static void AddSpriteMessage(LogLevel logLevel, string message, string typeMessage, Exception exception = null) 44 | { 45 | Log(SpriteLogger, logLevel, message, typeMessage, exception); 46 | } 47 | 48 | public static void AddImageMessage(LogLevel logLevel, string message, string typeMessage, Exception exception = null) 49 | { 50 | Log(ImageLogger, logLevel, message, typeMessage, exception); 51 | } 52 | 53 | public static void AddSubtitleMessage(LogLevel logLevel, string message, string typeMessage, Exception exception = null) 54 | { 55 | Log(SubtitleLogger, logLevel, message, typeMessage, exception); 56 | } 57 | 58 | public static void Log(ILogger logger, LogLevel logLevel, string message, string typeMessage, Exception exception = null) 59 | { 60 | string formatMessage = $"[{typeMessage}] {message}"; 61 | switch (logLevel) 62 | { 63 | case LogLevel.Trace: 64 | logger.LogTrace(formatMessage); 65 | Trace.WriteLine(formatMessage); 66 | break; 67 | case LogLevel.Debug: 68 | logger.LogDebug(formatMessage); 69 | Debug.WriteLine(formatMessage); 70 | break; 71 | case LogLevel.Information: 72 | logger.LogInformation(formatMessage); 73 | break; 74 | case LogLevel.Warning: 75 | logger.LogWarning(formatMessage); 76 | break; 77 | case LogLevel.Error: 78 | logger.LogError(formatMessage); 79 | break; 80 | case LogLevel.Critical: 81 | if(exception == null) 82 | logger.LogCritical(formatMessage); 83 | else 84 | logger.LogCritical(exception, formatMessage); 85 | break; 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/ProcessManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Text; 4 | 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Uploader.Core.Managers.Common 8 | { 9 | public class ProcessManager 10 | { 11 | private ProcessStartInfo _processStartInfo; 12 | 13 | private ILogger Logger { get; set; } 14 | 15 | public bool HasTimeout { get; private set; } 16 | 17 | public int ExitCode { get; private set; } 18 | 19 | public StringBuilder DataOutput { get; private set; } = new StringBuilder(); 20 | 21 | public StringBuilder ErrorOutput { get; private set; } = new StringBuilder(); 22 | 23 | public ProcessManager(string fileName, string arguments, ILogger logger) 24 | { 25 | if(string.IsNullOrWhiteSpace(fileName)) 26 | throw new ArgumentNullException(nameof(fileName)); 27 | if(string.IsNullOrWhiteSpace(arguments)) 28 | throw new ArgumentNullException(nameof(arguments)); 29 | if(logger == null) 30 | throw new ArgumentNullException(nameof(logger)); 31 | 32 | Logger = logger; 33 | 34 | _processStartInfo = new ProcessStartInfo(); 35 | _processStartInfo.FileName = fileName; 36 | _processStartInfo.Arguments = arguments; 37 | 38 | _processStartInfo.RedirectStandardOutput = true; 39 | _processStartInfo.RedirectStandardError = true; 40 | _processStartInfo.WorkingDirectory = TempFileManager.GetTempDirectory(); 41 | 42 | _processStartInfo.UseShellExecute = false; 43 | _processStartInfo.ErrorDialog = false; 44 | _processStartInfo.CreateNoWindow = true; 45 | _processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; 46 | } 47 | 48 | public bool LaunchAsync(int timeout) 49 | { 50 | LogManager.Log(Logger, LogLevel.Information, _processStartInfo.FileName + " " + _processStartInfo.Arguments, "Launch command"); 51 | 52 | try 53 | { 54 | using(Process process = Process.Start(_processStartInfo)) 55 | { 56 | process.OutputDataReceived += new DataReceivedEventHandler(OutputDataReceived); 57 | process.ErrorDataReceived += new DataReceivedEventHandler(ErrorDataReceived); 58 | 59 | process.BeginOutputReadLine(); 60 | process.BeginErrorReadLine(); 61 | 62 | bool success = process.WaitForExit(timeout * 1000); 63 | 64 | HasTimeout = !success; 65 | ExitCode = process.ExitCode; 66 | 67 | if (HasTimeout) 68 | { 69 | LogManager.Log(Logger, LogLevel.Error, $"Le process n'a pas pu être exécuté dans le temps imparti.", "Timeout"); 70 | return false; 71 | } 72 | 73 | if (ExitCode != 0) 74 | { 75 | LogManager.Log(Logger, LogLevel.Error, $"Le process n'a pas pu être exécuté correctement, erreur {process.ExitCode}.", "Error"); 76 | return false; 77 | } 78 | 79 | return true; 80 | } 81 | } 82 | catch(Exception ex) 83 | { 84 | LogManager.Log(Logger, LogLevel.Critical, $"Exception : Le process n'a pas pu être exécuté correctement : {ex}.", "Exception"); 85 | return false; 86 | } 87 | } 88 | 89 | public bool Launch(int timeout) 90 | { 91 | LogManager.Log(Logger, LogLevel.Information, _processStartInfo.FileName + " " + _processStartInfo.Arguments, "Launch command"); 92 | 93 | try 94 | { 95 | using(Process process = Process.Start(_processStartInfo)) 96 | { 97 | bool success = process.WaitForExit(timeout * 1000); 98 | 99 | DataOutput = DataOutput.Append(process.StandardOutput.ReadToEnd()); 100 | ErrorOutput = ErrorOutput.Append(process.StandardError.ReadToEnd()); 101 | 102 | LogManager.Log(Logger, LogLevel.Debug, DataOutput.ToString(), "DEBUG"); 103 | LogManager.Log(Logger, LogLevel.Debug, ErrorOutput.ToString(), "DEBUG"); 104 | 105 | HasTimeout = !success; 106 | ExitCode = process.ExitCode; 107 | 108 | if (HasTimeout) 109 | { 110 | LogManager.Log(Logger, LogLevel.Error, $"Le process n'a pas pu être exécuté dans le temps imparti.", "Timeout"); 111 | return false; 112 | } 113 | 114 | if (ExitCode != 0) 115 | { 116 | LogManager.Log(Logger, LogLevel.Error, $"Le process n'a pas pu être exécuté correctement, erreur {process.ExitCode}.", "Error"); 117 | return false; 118 | } 119 | 120 | return true; 121 | } 122 | } 123 | catch(Exception ex) 124 | { 125 | LogManager.Log(Logger, LogLevel.Critical, $"Exception : Le process n'a pas pu être exécuté correctement : {ex}.", "Exception"); 126 | return false; 127 | } 128 | } 129 | 130 | public bool LaunchWithoutTracking() 131 | { 132 | LogManager.Log(Logger, LogLevel.Information, _processStartInfo.FileName + " " + _processStartInfo.Arguments, "Launch command"); 133 | 134 | try 135 | { 136 | using(Process process = Process.Start(_processStartInfo)) 137 | { 138 | return !process.HasExited || process.ExitCode == 0; 139 | } 140 | } 141 | catch(Exception ex) 142 | { 143 | LogManager.Log(Logger, LogLevel.Critical, $"Exception : Le process n'a pas pu être exécuté correctement : {ex}.", "Exception"); 144 | return false; 145 | } 146 | } 147 | 148 | private void OutputDataReceived(object sender, DataReceivedEventArgs e) 149 | { 150 | string output = e.Data; 151 | if (string.IsNullOrWhiteSpace(output)) 152 | return; 153 | 154 | LogManager.Log(Logger, LogLevel.Debug, output, "DEBUG"); 155 | DataOutput.AppendLine(output); 156 | } 157 | 158 | private void ErrorDataReceived(object sender, DataReceivedEventArgs e) 159 | { 160 | string output = e.Data; 161 | if (string.IsNullOrWhiteSpace(output)) 162 | return; 163 | 164 | LogManager.Log(Logger, LogLevel.Debug, output, "DEBUG"); 165 | ErrorOutput.AppendLine(output); 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | using Microsoft.Extensions.Configuration; 5 | 6 | using Uploader.Core.Managers.Front; 7 | using Uploader.Core.Managers.Ipfs; 8 | using Uploader.Core.Managers.Video; 9 | using Uploader.Core.Models; 10 | 11 | namespace Uploader.Core.Managers.Common 12 | { 13 | public static class Startup 14 | { 15 | public static void InitSettings(IConfiguration Configuration) 16 | { 17 | Configuration.GetSection("General").Bind(GeneralSettings.Instance); 18 | Configuration.GetSection("Ipfs").Bind(IpfsSettings.Instance); 19 | Configuration.GetSection("Video").Bind(VideoSettings.Instance); 20 | 21 | var list = new List(); 22 | Configuration.GetSection("Encode").Bind(list); 23 | var dico = list.ToDictionary(l => l.UrlTag, l => l); 24 | VideoSizeFactory.Init(dico); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/TempFileManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System; 4 | 5 | using Uploader.Core.Managers.Front; 6 | using Uploader.Core.Managers.Ipfs; 7 | 8 | namespace Uploader.Core.Managers.Common 9 | { 10 | public static class TempFileManager 11 | { 12 | private static string _tempDirectoryPath; 13 | 14 | public static string GetTempDirectory() 15 | { 16 | if(_tempDirectoryPath == null) 17 | { 18 | if (GeneralSettings.Instance.TempFilePath.Length > 0) 19 | _tempDirectoryPath = GeneralSettings.Instance.TempFilePath; 20 | else 21 | _tempDirectoryPath = Path.GetTempPath(); 22 | } 23 | 24 | return _tempDirectoryPath; 25 | } 26 | 27 | public static string GetNewTempFilePath() 28 | { 29 | return Path.Combine(GetTempDirectory(), Path.GetRandomFileName()); 30 | } 31 | 32 | public static void SafeDeleteTempFile(string filePath, string hash = "") 33 | { 34 | if(string.IsNullOrWhiteSpace(filePath)) 35 | return; 36 | 37 | try 38 | { 39 | // suppression du fichier temporaire, ne pas jeter d'exception en cas d'erreur 40 | if (File.Exists(filePath)) 41 | { 42 | if (IpfsSettings.Instance.OnlyHash && hash.Length == 46) 43 | { 44 | File.Move(filePath, Path.Combine(GeneralSettings.Instance.FinalFilePath, hash)); 45 | } 46 | File.Delete(filePath); 47 | } 48 | } 49 | catch 50 | {} 51 | } 52 | 53 | public static void SafeDeleteTempFiles(IList filesPath, string hash = "") 54 | { 55 | if(filesPath == null) 56 | return; 57 | 58 | // suppression des images 59 | foreach (string filePath in filesPath) 60 | { 61 | SafeDeleteTempFile(filePath, hash); 62 | } 63 | } 64 | 65 | public static void SafeCopyError(string filePath, string hash) 66 | { 67 | if(string.IsNullOrWhiteSpace(filePath)) 68 | return; 69 | 70 | try 71 | { 72 | // suppression du fichier temporaire, ne pas jeter d'exception en cas d'erreur 73 | if (File.Exists(filePath)) 74 | { 75 | File.Copy(filePath, Path.Combine(GeneralSettings.Instance.ErrorFilePath, hash)); 76 | } 77 | } 78 | catch 79 | {} 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Common/Tools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Uploader.Core.Managers.Common 5 | { 6 | internal static class Tools 7 | { 8 | public static DateTime Max(params DateTime[] dates) 9 | { 10 | return dates.Max(); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Front/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Managers.Front 2 | { 3 | internal class GeneralSettings 4 | { 5 | private GeneralSettings(){} 6 | 7 | static GeneralSettings() 8 | { 9 | Instance = new GeneralSettings(); 10 | } 11 | 12 | public static GeneralSettings Instance { get; private set; } 13 | 14 | /// 15 | /// seconds 16 | /// 17 | public int MaxGetProgressCanceled { get; set; } 18 | 19 | 20 | public string ImageMagickPath { get; set; } 21 | 22 | public string TempFilePath { get; set; } 23 | 24 | public string FinalFilePath { get; set; } 25 | 26 | public string ErrorFilePath { get; set; } 27 | 28 | public string Version { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Uploader.Core/Managers/Front/OverlayManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | using Uploader.Core.Managers.Common; 7 | using Uploader.Core.Managers.Ipfs; 8 | using Uploader.Core.Models; 9 | 10 | namespace Uploader.Core.Managers.Front 11 | { 12 | public static class ImageManager 13 | { 14 | private static string _overlayImagePath = Path.Combine(Directory.GetCurrentDirectory(), "overlay.png"); 15 | 16 | private static int _finalWidthSnap = 210; 17 | private static int _finalHeightSnap = 118; 18 | 19 | private static int _finalWidthOverlay = 640; 20 | private static int _finalHeightOverlay = 360; 21 | 22 | public static Guid ComputeImage(string sourceFilePath) 23 | { 24 | FileContainer fileContainer = FileContainer.NewImageContainer(sourceFilePath); 25 | FileItem sourceFile = fileContainer.SourceFileItem; 26 | 27 | /////////////////////////////////////////////////////////////////////////////////////////////// 28 | /// resize + crop source image 29 | /////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | try 32 | { 33 | LogManager.AddImageMessage(LogLevel.Information, "SourceFileName " + Path.GetFileName(sourceFile.SourceFilePath), "Start Resize and Crop source"); 34 | string arguments = $"{Path.GetFileName(sourceFile.SourceFilePath)} -resize \"{_finalWidthOverlay}x{_finalHeightOverlay}^\" -gravity Center -crop {_finalWidthOverlay}x{_finalHeightOverlay}+0+0 {Path.GetFileName(sourceFile.TempFilePath)}"; 35 | var process = new ProcessManager(Path.Combine(GeneralSettings.Instance.ImageMagickPath, "convert"), arguments, LogManager.ImageLogger); 36 | bool success = process.Launch(5); 37 | if(!success) 38 | { 39 | LogManager.AddImageMessage(LogLevel.Error, "Erreur convert", "Erreur"); 40 | fileContainer.CancelAll("Erreur resize and crop source"); 41 | fileContainer.CleanFilesIfEnd(); 42 | return fileContainer.ProgressToken; 43 | } 44 | sourceFile.ReplaceOutputPathWithTempPath(); 45 | LogManager.AddImageMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(sourceFile.OutputFilePath), "End Resize and Crop source"); 46 | } 47 | catch(Exception ex) 48 | { 49 | LogManager.AddImageMessage(LogLevel.Critical, "Exception non gérée resize crop source", "Exception", ex); 50 | fileContainer.CancelAll("Exception non gérée"); 51 | fileContainer.CleanFilesIfEnd(); 52 | return fileContainer.ProgressToken; 53 | } 54 | 55 | // remplacement de l'image source par l'image de sortie 56 | sourceFile.SetSourceFilePath(sourceFile.OutputFilePath); 57 | 58 | /////////////////////////////////////////////////////////////////////////////////////////////// 59 | /// Resize snap image 60 | /////////////////////////////////////////////////////////////////////////////////////////////// 61 | 62 | // changement de la source de SnapFileItem 63 | fileContainer.SnapFileItem.SetSourceFilePath(sourceFile.SourceFilePath); 64 | 65 | try 66 | { 67 | LogManager.AddImageMessage(LogLevel.Information, "SourceFileName " + Path.GetFileName(fileContainer.SnapFileItem.SourceFilePath), "Start Resize Snap"); 68 | string arguments = $"{Path.GetFileName(fileContainer.SnapFileItem.SourceFilePath)} -resize \"{_finalWidthSnap}x{_finalHeightSnap}^\" {Path.GetFileName(fileContainer.SnapFileItem.TempFilePath)}"; 69 | var process = new ProcessManager(Path.Combine(GeneralSettings.Instance.ImageMagickPath, "convert"), arguments, LogManager.ImageLogger); 70 | bool success = process.Launch(5); 71 | if(!success) 72 | { 73 | LogManager.AddImageMessage(LogLevel.Error, "Erreur snap", "Erreur"); 74 | fileContainer.CancelAll("Erreur snap"); 75 | fileContainer.CleanFilesIfEnd(); 76 | return fileContainer.ProgressToken; 77 | } 78 | fileContainer.SnapFileItem.ReplaceOutputPathWithTempPath(); 79 | LogManager.AddImageMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(fileContainer.SnapFileItem.OutputFilePath), "End Resize Snap"); 80 | IpfsDaemon.Instance.Queue(fileContainer.SnapFileItem); 81 | } 82 | catch(Exception ex) 83 | { 84 | LogManager.AddImageMessage(LogLevel.Critical, "Exception non gérée snap", "Exception", ex); 85 | fileContainer.CancelAll("Exception non gérée"); 86 | fileContainer.CleanFilesIfEnd(); 87 | return fileContainer.ProgressToken; 88 | } 89 | 90 | /////////////////////////////////////////////////////////////////////////////////////////////// 91 | /// Overlay image 92 | /////////////////////////////////////////////////////////////////////////////////////////////// 93 | 94 | // changement de la source de OverlayFileItem 95 | fileContainer.OverlayFileItem.SetSourceFilePath(sourceFile.SourceFilePath); 96 | 97 | try 98 | { 99 | LogManager.AddImageMessage(LogLevel.Information, "SourceFileName " + Path.GetFileName(fileContainer.OverlayFileItem.SourceFilePath), "Start Overlay"); 100 | string arguments = $"-gravity NorthEast {_overlayImagePath} {Path.GetFileName(fileContainer.OverlayFileItem.SourceFilePath)} {Path.GetFileName(fileContainer.OverlayFileItem.TempFilePath)}"; 101 | var process = new ProcessManager(Path.Combine(GeneralSettings.Instance.ImageMagickPath, "composite"), arguments, LogManager.ImageLogger); 102 | bool success = process.Launch(5); 103 | if(!success) 104 | { 105 | LogManager.AddImageMessage(LogLevel.Error, "Erreur overlay", "Erreur"); 106 | fileContainer.CancelAll("Erreur overlay"); 107 | fileContainer.CleanFilesIfEnd(); 108 | return fileContainer.ProgressToken; 109 | } 110 | fileContainer.OverlayFileItem.ReplaceOutputPathWithTempPath(); 111 | LogManager.AddImageMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(fileContainer.OverlayFileItem.OutputFilePath), "End Overlay"); 112 | IpfsDaemon.Instance.Queue(fileContainer.OverlayFileItem); 113 | } 114 | catch(Exception ex) 115 | { 116 | LogManager.AddImageMessage(LogLevel.Critical, "Exception non gérée overlay", "Exception", ex); 117 | fileContainer.CancelAll("Exception non gérée"); 118 | fileContainer.CleanFilesIfEnd(); 119 | return fileContainer.ProgressToken; 120 | } 121 | 122 | return fileContainer.ProgressToken; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Front/ProgressManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | using Microsoft.Extensions.Logging; 10 | 11 | using Uploader.Core.Managers.Common; 12 | using Uploader.Core.Managers.Ipfs; 13 | using Uploader.Core.Managers.Video; 14 | using Uploader.Core.Models; 15 | 16 | namespace Uploader.Core.Managers.Front 17 | { 18 | public static class ProgressManager 19 | { 20 | public static string Version => GeneralSettings.Instance.Version; 21 | 22 | private static ConcurrentDictionary progresses = new ConcurrentDictionary(); 23 | 24 | internal static void RegisterProgress(FileContainer fileContainer) 25 | { 26 | progresses.TryAdd(fileContainer.ProgressToken, fileContainer); 27 | 28 | // Supprimer le suivi progress après 1j 29 | var now = DateTime.UtcNow; 30 | var purgeList = progresses.Values.Where(f => (now - f.LastActivityDateTime).TotalHours >= 24).ToList(); 31 | foreach (FileContainer toDelete in purgeList) 32 | { 33 | FileContainer thisFileContainer; 34 | progresses.TryRemove(toDelete.ProgressToken, out thisFileContainer); 35 | } 36 | } 37 | 38 | public static dynamic GetErrors() 39 | { 40 | return new 41 | { 42 | list = progresses.Values 43 | .Where(c => c.Error()) 44 | .Select(c => GetResult(c, true)) 45 | .ToList() 46 | }; 47 | } 48 | 49 | public static dynamic GetStats(bool details) 50 | { 51 | return details ? GetDetailStats() : GetLightStats(); 52 | } 53 | 54 | public static dynamic GetLightStats() 55 | { 56 | return new 57 | { 58 | version = Version, 59 | currentWaitingInQueue = GetCurrentWaitingInQueue() 60 | }; 61 | } 62 | 63 | public static dynamic GetDetailStats() 64 | { 65 | try 66 | { 67 | var list = progresses.Values.ToList(); 68 | 69 | var listVideoEncoded = new List(); 70 | var listSpriteCreated = new List(); 71 | var listIpfsAdded = new List(); 72 | 73 | listVideoEncoded.AddRange(list.Select(l => l.SourceFileItem)); 74 | listIpfsAdded.AddRange(list.Select(l => l.SourceFileItem).Where(f => f.IpfsProcess != null)); 75 | 76 | var listSpriteFiles = list.Where(l => l.SpriteVideoFileItem != null).Select(l => l.SpriteVideoFileItem).ToList(); 77 | listSpriteCreated.AddRange(listSpriteFiles); 78 | listIpfsAdded.AddRange(listSpriteFiles); 79 | 80 | var listEncodeFiles = list.Where(l => l.EncodedFileItems != null).SelectMany(l => l.EncodedFileItems).ToList(); 81 | listVideoEncoded.AddRange(listEncodeFiles); 82 | listIpfsAdded.AddRange(listEncodeFiles.Where(f => f.IpfsProcess != null)); 83 | 84 | return new 85 | { 86 | version = Version, 87 | currentWaitingInQueue = GetCurrentWaitingInQueue(), 88 | 89 | Init = GetStatByStep(ProcessStep.Init, listVideoEncoded, listSpriteCreated, listIpfsAdded), 90 | Waiting = GetStatByStep(ProcessStep.Waiting, listVideoEncoded, listSpriteCreated, listIpfsAdded), 91 | Canceled = GetStatByStep(ProcessStep.Canceled, listVideoEncoded, listSpriteCreated, listIpfsAdded), 92 | Started = GetStatByStep(ProcessStep.Started, listVideoEncoded, listSpriteCreated, listIpfsAdded), 93 | Error = GetStatByStep(ProcessStep.Error, listVideoEncoded, listSpriteCreated, listIpfsAdded), 94 | Success = GetStatByStep(ProcessStep.Success, listVideoEncoded, listSpriteCreated, listIpfsAdded) 95 | }; 96 | } 97 | catch(Exception ex) 98 | { 99 | return new 100 | { 101 | version = Version, 102 | currentWaitingInQueue = GetCurrentWaitingInQueue(), 103 | exception = ex.ToString() 104 | }; 105 | } 106 | } 107 | 108 | private static dynamic GetCurrentWaitingInQueue() 109 | { 110 | return new 111 | { 112 | audioCpuToEncode = AudioCpuEncodeDaemon.Instance.CurrentWaitingInQueue, 113 | videoGpuToEncode = VideoGpuEncodeDaemon.Instance.CurrentWaitingInQueue, 114 | audioVideoCpuToEncode = AudioVideoCpuEncodeDaemon.Instance.CurrentWaitingInQueue, 115 | spriteToCreate = SpriteDaemon.Instance.CurrentWaitingInQueue, 116 | ipfsToAdd = IpfsDaemon.Instance.CurrentWaitingInQueue 117 | }; 118 | } 119 | 120 | private static dynamic GetStatByStep(ProcessStep step, List listVideoEncoded, List listSpriteCreated, List listIpfsAdded) 121 | { 122 | try 123 | { 124 | return new 125 | { 126 | audioCpuEncodeLast24h = GetProcessStats(listVideoEncoded.FindAll(f => f.AudioCpuEncodeProcess != null && f.AudioCpuEncodeProcess.CurrentStep == step).Select(f => f.AudioCpuEncodeProcess).ToList()), 127 | videoGpuEncodeLast24h = GetProcessStats(listVideoEncoded.FindAll(f => f.VideoGpuEncodeProcess != null && f.VideoGpuEncodeProcess.CurrentStep == step).Select(f => f.VideoGpuEncodeProcess).ToList()), 128 | audioVideoCpuEncodeLast24h = GetProcessStats(listVideoEncoded.FindAll(f => f.AudioVideoCpuEncodeProcess != null && f.AudioVideoCpuEncodeProcess.CurrentStep == step).Select(f => f.AudioVideoCpuEncodeProcess).ToList()), 129 | spriteCreationLast24h = GetProcessStats(listSpriteCreated.FindAll(f => f.SpriteEncodeProcess != null && f.SpriteEncodeProcess.CurrentStep == step).Select(f => f.SpriteEncodeProcess).ToList()), 130 | ipfsAddLast24h = GetProcessStats(listIpfsAdded.FindAll(f => f.IpfsProcess != null && f.IpfsProcess.CurrentStep == step).Select(f => f.IpfsProcess).ToList()) 131 | }; 132 | } 133 | catch(Exception ex) 134 | { 135 | return new 136 | { 137 | exception = ex.ToString() 138 | }; 139 | } 140 | } 141 | 142 | private static dynamic GetProcessStats(List processItems) 143 | { 144 | if(processItems == null || !processItems.Any()) 145 | return null; 146 | 147 | return new 148 | { 149 | nb = processItems.Count, 150 | waitingInQueue = GetInfo(processItems 151 | .Where(p => p.OriginWaitingPositionInQueue > 0) 152 | .Select(p => (long)p.OriginWaitingPositionInQueue) 153 | .ToList()), 154 | fileSize = GetFileSizeInfo(processItems 155 | .Where(p => p.FileItem.FileSize.HasValue) 156 | .Select(p => p.FileItem.FileSize.Value) 157 | .ToList()), 158 | waitingTime = GetTimeInfo(processItems 159 | .Where(p => p.WaitingTime.HasValue) 160 | .Select(p => p.WaitingTime.Value) 161 | .ToList()), 162 | processTime = GetTimeInfo(processItems 163 | .Where(p => p.ProcessTime.HasValue) 164 | .Select(p => p.ProcessTime.Value) 165 | .ToList()) 166 | }; 167 | } 168 | 169 | private static dynamic GetProcessWithSourceStats(List processItems) 170 | { 171 | var stats = GetProcessStats(processItems); 172 | if(stats == null) 173 | return null; 174 | 175 | return new 176 | { 177 | nb = stats.nb, 178 | waitingInQueue = stats.waitingInQueue, 179 | sourceDuration = GetTimeInfo(processItems 180 | .Where(p => p.FileItem.FileContainer.SourceFileItem.VideoDuration.HasValue) 181 | .Select(p => (long)p.FileItem.FileContainer.SourceFileItem.VideoDuration.Value) 182 | .ToList()), 183 | sourceFileSize = GetFileSizeInfo(processItems 184 | .Where(p => p.FileItem.FileContainer.SourceFileItem.FileSize.HasValue) 185 | .Select(p => (long)p.FileItem.FileContainer.SourceFileItem.FileSize.Value) 186 | .ToList()), 187 | fileSize = stats.fileSize, 188 | waitingTime = stats.waitingTime, 189 | processTime = stats.processTime 190 | }; 191 | } 192 | 193 | private static dynamic GetInfo(List infos) 194 | { 195 | if(!infos.Any()) 196 | return null; 197 | return new { min = infos.Min(), average = Math.Round(infos.Average(), 2), max = infos.Max() }; 198 | } 199 | 200 | private static dynamic GetTimeInfo(List infos) 201 | { 202 | if(!infos.Any()) 203 | return null; 204 | return new { min = Time(infos.Min()), average = Time((long)infos.Average()), max = Time(infos.Max()) }; 205 | } 206 | 207 | private static dynamic GetFileSizeInfo(List infos) 208 | { 209 | if(!infos.Any()) 210 | return null; 211 | return new { min = Filesize(infos.Min()), average = Filesize((long)infos.Average()), max = Filesize(infos.Max()) }; 212 | } 213 | 214 | private static string Filesize(long octet) 215 | { 216 | if(octet < 1 * 1000) 217 | return octet + " o"; 218 | 219 | if(octet < 1 * 1000000) 220 | return Math.Round(octet/1000d, 2) + " Ko"; 221 | 222 | if(octet < 1 * 1000000000) 223 | return Math.Round(octet/1000000d, 2) + " Mo"; 224 | 225 | if(octet < 1 * 1000000000000) 226 | return Math.Round(octet/1000000000d, 2) + " Go"; 227 | 228 | return Math.Round(octet/1000000000000d, 2) + " To"; 229 | } 230 | 231 | private static string Time(long seconds) 232 | { 233 | if(seconds < 1 * 60) 234 | return seconds + " second(s)"; 235 | 236 | if(seconds < 1 * 3600) 237 | return Math.Round(seconds/60d, 2) + " minute(s)"; 238 | 239 | return Math.Round(seconds/3600d, 2) + " heure(s)"; 240 | } 241 | 242 | public static dynamic GetFileContainerByToken(Guid progressToken) 243 | { 244 | FileContainer fileContainer; 245 | progresses.TryGetValue(progressToken, out fileContainer); 246 | 247 | if(fileContainer != null) 248 | fileContainer.UpdateLastTimeProgressRequest(); 249 | else 250 | return null; 251 | 252 | return GetResult(fileContainer); 253 | } 254 | 255 | public static dynamic GetFileContainerBySourceHash(string sourceHash) 256 | { 257 | FileContainer fileContainer = progresses.Values 258 | .Where(s => s.SourceFileItem.IpfsHash == sourceHash) 259 | .OrderByDescending(s => s.NumInstance) 260 | .FirstOrDefault(); 261 | 262 | if(fileContainer != null) 263 | fileContainer.UpdateLastTimeProgressRequest(); 264 | else 265 | return null; 266 | 267 | return GetResult(fileContainer); 268 | } 269 | 270 | private static dynamic GetResult(FileContainer fileContainer, bool error = false) 271 | { 272 | switch (fileContainer.TypeContainer) 273 | { 274 | case TypeContainer.Video: 275 | return new 276 | { 277 | finished = fileContainer.Finished(), 278 | debugInfo = error ? DebugInfo(fileContainer) : null, 279 | sourceAudioCpuEncoding = AudioCpuEncodeResultJson(fileContainer.SourceFileItem, error), 280 | sourceVideoGpuEncoding = VideoGpuEncodeResultJson(fileContainer.SourceFileItem, error), 281 | ipfsAddSourceVideo = IpfsResultJson(fileContainer.SourceFileItem, error), 282 | sprite = fileContainer.SpriteVideoFileItem == null ? null : 283 | new 284 | { 285 | spriteCreation = SpriteResultJson(fileContainer.SpriteVideoFileItem, error), 286 | ipfsAddSprite = IpfsResultJson(fileContainer.SpriteVideoFileItem, error) 287 | }, 288 | encodedVideos = !fileContainer.EncodedFileItems.Any() ? null : 289 | fileContainer.EncodedFileItems.Select(e => 290 | new 291 | { 292 | encode = AudioVideoCpuEncodeResultJson(e, error), 293 | ipfsAddEncodeVideo = IpfsResultJson(e, error) 294 | }) 295 | .ToArray() 296 | }; 297 | 298 | case TypeContainer.Image: 299 | return new 300 | { 301 | ipfsAddSource = IpfsResultJson(fileContainer.SnapFileItem, error), 302 | ipfsAddOverlay = IpfsResultJson(fileContainer.OverlayFileItem, error) 303 | }; 304 | 305 | case TypeContainer.Subtitle: 306 | return new 307 | { 308 | ipfsAddSource = IpfsResultJson(fileContainer.SubtitleFileItem, error) 309 | }; 310 | } 311 | 312 | LogManager.AddGeneralMessage(LogLevel.Critical, "Type container non géré " + fileContainer.TypeContainer, "Error"); 313 | throw new InvalidOperationException("type container non géré"); 314 | } 315 | 316 | private static dynamic DebugInfo(FileContainer fileContainer) 317 | { 318 | string hash = fileContainer?.SourceFileItem.IpfsHash; 319 | return new 320 | { 321 | originFileName = Path.GetFileName(fileContainer.OriginFilePath), 322 | ipfsUrl = hash == null ? null : "https://ipfs.io/ipfs/" + hash, 323 | exceptionDetail = fileContainer.ExceptionDetail, 324 | sourceInfo = SourceInfo(fileContainer.SourceFileItem) 325 | }; 326 | } 327 | 328 | private static dynamic SourceInfo(FileItem sourceFileItem) 329 | { 330 | if (sourceFileItem == null || sourceFileItem.InfoSourceProcess == null) 331 | return null; 332 | 333 | return new 334 | { 335 | sourceFileItem.FileSize, 336 | sourceFileItem.VideoCodec, 337 | sourceFileItem.VideoDuration, 338 | sourceFileItem.VideoWidth, 339 | sourceFileItem.VideoHeight, 340 | sourceFileItem.VideoPixelFormat, 341 | sourceFileItem.VideoFrameRate, 342 | sourceFileItem.VideoBitRate, 343 | sourceFileItem.VideoNbFrame, 344 | sourceFileItem.VideoRotate, 345 | sourceFileItem.AudioCodec, 346 | sourceFileItem.InfoSourceProcess.ErrorMessage 347 | }; 348 | } 349 | 350 | private static dynamic IpfsResultJson(FileItem fileItem, bool error) 351 | { 352 | var result = ProcessResultJson(fileItem?.IpfsProcess, error, IpfsDaemon.Instance.CurrentPositionInQueue); 353 | if(result == null) 354 | return null; 355 | 356 | return new 357 | { 358 | progress = result.progress, 359 | encodeSize = result.encodeSize, 360 | lastTimeProgress = result.lastTimeProgress, 361 | errorMessage = result.errorMessage, 362 | step = result.step, 363 | positionInQueue = result.positionInQueue, 364 | 365 | hash = fileItem.IpfsHash, 366 | fileSize = fileItem.FileSize 367 | }; 368 | } 369 | 370 | private static dynamic SpriteResultJson(FileItem fileItem, bool error) 371 | { 372 | return ProcessResultJson(fileItem?.SpriteEncodeProcess, error, SpriteDaemon.Instance.CurrentPositionInQueue); 373 | } 374 | 375 | private static dynamic AudioCpuEncodeResultJson(FileItem fileItem, bool error) 376 | { 377 | return ProcessResultJson(fileItem?.AudioCpuEncodeProcess, error, AudioCpuEncodeDaemon.Instance.CurrentPositionInQueue); 378 | } 379 | 380 | private static dynamic AudioVideoCpuEncodeResultJson(FileItem fileItem, bool error) 381 | { 382 | return ProcessResultJson(fileItem?.AudioVideoCpuEncodeProcess, error, AudioVideoCpuEncodeDaemon.Instance.CurrentPositionInQueue); 383 | } 384 | 385 | private static dynamic VideoGpuEncodeResultJson(FileItem fileItem, bool error) 386 | { 387 | return ProcessResultJson(fileItem?.VideoGpuEncodeProcess, error, VideoGpuEncodeDaemon.Instance.CurrentPositionInQueue); 388 | } 389 | 390 | private static dynamic ProcessResultJson(ProcessItem processItem, bool error, int daemonCurrentPositionInQUeue) 391 | { 392 | if (processItem == null) 393 | return null; 394 | if(error && processItem.CurrentStep != ProcessStep.Error) 395 | return null; 396 | 397 | return new 398 | { 399 | progress = processItem.Progress, 400 | encodeSize = processItem.FileItem.VideoSize.VideoSizeString(), 401 | lastTimeProgress = processItem.LastTimeProgressChanged, 402 | errorMessage = processItem.ErrorMessage, 403 | step = processItem.CurrentStep.ToString(), 404 | positionInQueue = Position(processItem, daemonCurrentPositionInQUeue) 405 | }; 406 | } 407 | 408 | private static string VideoSizeString(this VideoSize videoSize) 409 | { 410 | return videoSize == null ? "source" : videoSize.Height.ToString() + "p"; 411 | } 412 | 413 | private static int? Position(ProcessItem processItem, int daemonCurrentPositionInQueue) 414 | { 415 | if (processItem.CurrentStep != ProcessStep.Waiting) 416 | return null; 417 | 418 | return processItem.PositionInQueue - daemonCurrentPositionInQueue; 419 | } 420 | } 421 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Front/SubtitleManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using Uploader.Core.Managers.Common; 7 | using Uploader.Core.Managers.Ipfs; 8 | using Uploader.Core.Models; 9 | 10 | namespace Uploader.Core.Managers.Front 11 | { 12 | public static class SubtitleManager 13 | { 14 | public static async Task ComputeSubtitle(string text) 15 | { 16 | FileContainer fileContainer = FileContainer.NewSubtitleContainer(); 17 | 18 | if (!IsValidVTT(text)) 19 | { 20 | fileContainer.SubtitleFileItem.IpfsProcess.SetErrorMessage("Not a valid WEBVTT file", "Not a valid WEBVTT file"); 21 | return fileContainer.ProgressToken; 22 | } 23 | 24 | try 25 | { 26 | await File.WriteAllTextAsync(fileContainer.SubtitleFileItem.TempFilePath, text); 27 | fileContainer.SubtitleFileItem.ReplaceOutputPathWithTempPath(); 28 | IpfsDaemon.Instance.Queue(fileContainer.SubtitleFileItem); 29 | } 30 | catch(Exception ex) 31 | { 32 | LogManager.AddSubtitleMessage(LogLevel.Critical, "Exception non gérée", "Exception", ex); 33 | fileContainer.CancelAll("Exception non gérée"); 34 | fileContainer.CleanFilesIfEnd(); 35 | return fileContainer.ProgressToken; 36 | } 37 | 38 | return fileContainer.ProgressToken; 39 | } 40 | 41 | private static bool IsValidVTT(string text) 42 | { 43 | LogManager.AddSubtitleMessage(LogLevel.Debug, text, "DEBUG"); 44 | if (!text.StartsWith("WEBVTT")) 45 | return false; 46 | 47 | // eventuellement rajouter plus de verifs 48 | // mais peu d'interet 49 | 50 | return true; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Front/VideoManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Uploader.Core.Managers.Ipfs; 6 | using Uploader.Core.Managers.Video; 7 | using Uploader.Core.Models; 8 | 9 | namespace Uploader.Core.Managers.Front 10 | { 11 | public static class VideoManager 12 | { 13 | public static Guid ComputeVideo(string originFilePath, string videoEncodingFormats, bool? sprite) 14 | { 15 | FileContainer fileContainer = FileContainer.NewVideoContainer(originFilePath); 16 | FileItem sourceFile = fileContainer.SourceFileItem; 17 | 18 | // Récupérer la durée totale de la vidéo et sa résolution, autorisation encoding 19 | bool successGetSourceInfo = VideoSourceManager.SuccessAnalyseSource(sourceFile, sourceFile.InfoSourceProcess); 20 | if(!successGetSourceInfo) 21 | return fileContainer.ProgressToken; 22 | 23 | // si ipfs add source demandé ou dépassement de la durée max 24 | if(IpfsSettings.Instance.AddVideoSource || sourceFile.HasReachMaxVideoDurationForEncoding()) 25 | { 26 | sourceFile.AddIpfsProcess(sourceFile.SourceFilePath); 27 | IpfsDaemon.Instance.Queue(sourceFile); 28 | } 29 | 30 | if(!sourceFile.HasReachMaxVideoDurationForEncoding()) 31 | { 32 | VideoSize[] requestFormats = GetVideoSizes(videoEncodingFormats); 33 | VideoSize[] authorizedFormats = GetVideoSizes(VideoSettings.Instance.AuthorizedQuality); 34 | IList formats = requestFormats 35 | .Intersect(authorizedFormats) 36 | .OrderBy(v => v.QualityOrder) 37 | .ToList(); 38 | 39 | // suppression des formats à encoder avec une qualité/bitrate/nbframe/resolution... supérieure 40 | foreach (VideoSize videoSize in formats.ToList()) 41 | { 42 | if(sourceFile.VideoHeight <= videoSize.MinSourceHeightForEncoding) 43 | formats.Remove(videoSize); 44 | } 45 | 46 | // si sprite demandé 47 | if (sprite??false) 48 | { 49 | fileContainer.AddSprite(); 50 | 51 | // si pas d'encoding à faire, il faut lancer le sprite de suite car sinon il ne sera pas lancé à la fin de l'encoding 52 | if(!formats.Any()) 53 | SpriteDaemon.Instance.Queue(fileContainer.SpriteVideoFileItem, "Waiting sprite creation..."); 54 | } 55 | 56 | // s'il y a de l'encoding à faire 57 | if(formats.Any()) 58 | { 59 | // ajouter les formats à encoder 60 | fileContainer.AddEncodedVideo(formats); 61 | 62 | if (VideoSettings.Instance.GpuEncodeMode) 63 | { 64 | sourceFile.AddGpuEncodeProcess(); 65 | // encoding audio de la source puis ça sera encoding videos Gpu 66 | AudioCpuEncodeDaemon.Instance.Queue(sourceFile, "waiting audio encoding..."); 67 | } 68 | else 69 | { 70 | // si encoding est demandé, et gpuMode -> encodingAudio 71 | foreach (FileItem file in fileContainer.EncodedFileItems) 72 | { 73 | file.AddCpuEncodeProcess(); 74 | AudioVideoCpuEncodeDaemon.Instance.Queue(file, "Waiting encode..."); 75 | } 76 | } 77 | } 78 | } 79 | 80 | return fileContainer.ProgressToken; 81 | } 82 | 83 | private static VideoSize[] GetVideoSizes(string videoEncodingFormats) 84 | { 85 | if (string.IsNullOrWhiteSpace(videoEncodingFormats)) 86 | return new VideoSize[0]; 87 | 88 | return videoEncodingFormats 89 | .Split(',') 90 | .Select(v => VideoSizeFactory.GetSize(v)) 91 | .ToArray(); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Ipfs/IpfsAddManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using Microsoft.Extensions.Logging; 5 | using Uploader.Core.Managers.Common; 6 | using Uploader.Core.Models; 7 | 8 | namespace Uploader.Core.Managers.Ipfs 9 | { 10 | internal static class IpfsAddManager 11 | { 12 | private static FileItem currentFileItem; 13 | 14 | public static void Add(FileItem fileItem) 15 | { 16 | try 17 | { 18 | currentFileItem = fileItem; 19 | 20 | LogManager.AddIpfsMessage(LogLevel.Information, "FileName " + Path.GetFileName(currentFileItem.OutputFilePath), "Start"); 21 | 22 | currentFileItem.IpfsHash = null; 23 | currentFileItem.IpfsProcess.StartProcessDateTime(); 24 | 25 | // Send to ipfs and return hash from ipfs 26 | var processStartInfo = new ProcessStartInfo(); 27 | processStartInfo.FileName = "ipfs"; 28 | if (IpfsSettings.Instance.OnlyHash) 29 | processStartInfo.Arguments = $"add --only-hash {Path.GetFileName(currentFileItem.OutputFilePath)}"; 30 | else 31 | processStartInfo.Arguments = $"add {Path.GetFileName(currentFileItem.OutputFilePath)}"; 32 | 33 | if(IpfsSettings.Instance.VideoAndSpriteTrickleDag) 34 | if(currentFileItem.TypeFile == TypeFile.SourceVideo || 35 | currentFileItem.TypeFile == TypeFile.EncodedVideo || 36 | currentFileItem.TypeFile == TypeFile.SpriteVideo) 37 | processStartInfo.Arguments = $"add -t {Path.GetFileName(currentFileItem.OutputFilePath)}"; 38 | 39 | processStartInfo.RedirectStandardOutput = true; 40 | processStartInfo.RedirectStandardError = true; 41 | processStartInfo.WorkingDirectory = TempFileManager.GetTempDirectory(); 42 | 43 | processStartInfo.UseShellExecute = false; 44 | processStartInfo.ErrorDialog = false; 45 | processStartInfo.CreateNoWindow = true; 46 | processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; 47 | 48 | LogManager.AddIpfsMessage(LogLevel.Information, processStartInfo.FileName + " " + processStartInfo.Arguments, "Launch command"); 49 | using(Process process = Process.Start(processStartInfo)) 50 | { 51 | process.OutputDataReceived += new DataReceivedEventHandler(OutputDataReceived); 52 | process.ErrorDataReceived += new DataReceivedEventHandler(ErrorDataReceived); 53 | 54 | process.BeginOutputReadLine(); 55 | process.BeginErrorReadLine(); 56 | 57 | bool success = process.WaitForExit(IpfsSettings.Instance.IpfsTimeout * 1000); 58 | if (!success) 59 | { 60 | throw new InvalidOperationException("Timeout : Le fichier n'a pas pu être envoyé à ipfs dans le temps imparti."); 61 | } 62 | 63 | if (process.ExitCode != 0) 64 | { 65 | throw new InvalidOperationException($"Le fichier n'a pas pu être envoyé à ipfs, erreur {process.ExitCode}."); 66 | } 67 | } 68 | 69 | currentFileItem.IpfsProcess.EndProcessDateTime(); 70 | LogManager.AddIpfsMessage(LogLevel.Information, "Hash " + currentFileItem.IpfsHash + " / FileSize " + currentFileItem.FileSize, "End"); 71 | } 72 | catch (Exception ex) 73 | { 74 | string message = "FileSize " + currentFileItem.FileSize + " / Progress " + currentFileItem.IpfsProcess.Progress; 75 | currentFileItem.IpfsProcess.SetErrorMessage("Exception non gérée", message, ex); 76 | } 77 | } 78 | 79 | private static void ErrorDataReceived(object sender, DataReceivedEventArgs e) 80 | { 81 | string output = e.Data; 82 | if (string.IsNullOrWhiteSpace(output)) 83 | return; 84 | 85 | // Récupérer la progression toutes les 1s 86 | if (currentFileItem.IpfsProcess.LastTimeProgressChanged.HasValue && (DateTime.UtcNow - currentFileItem.IpfsProcess.LastTimeProgressChanged.Value).TotalMilliseconds < 1000) 87 | return; 88 | 89 | LogManager.AddIpfsMessage(LogLevel.Debug, Path.GetFileName(currentFileItem.OutputFilePath) + " : " + output, "DEBUG"); 90 | 91 | // Récupérer la progression d'envoi, ex : 98.45% 92 | int startIndex = output.IndexOf('%') - 6; 93 | if(startIndex >= 0 && output.Length >= startIndex + 7) 94 | { 95 | string newProgress = output.Substring(startIndex, 7).Trim(); 96 | currentFileItem.IpfsProcess.SetProgress(newProgress); 97 | } 98 | } 99 | 100 | private static void OutputDataReceived(object sender, DataReceivedEventArgs e) 101 | { 102 | string output = e.Data; 103 | if (string.IsNullOrWhiteSpace(output)) 104 | return; 105 | 106 | LogManager.AddIpfsMessage(LogLevel.Debug, Path.GetFileName(currentFileItem.OutputFilePath) + " : " + output, "DEBUG"); 107 | 108 | if (output.StartsWith("added ")) 109 | { 110 | currentFileItem.IpfsHash = output.Split(' ')[1]; 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Ipfs/IpfsDaemon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using Uploader.Core.Managers.Common; 9 | using Uploader.Core.Managers.Front; 10 | using Uploader.Core.Models; 11 | 12 | namespace Uploader.Core.Managers.Ipfs 13 | { 14 | internal class IpfsDaemon : BaseDaemon 15 | { 16 | public static IpfsDaemon Instance { get; private set; } 17 | 18 | static IpfsDaemon() 19 | { 20 | Instance = new IpfsDaemon(); 21 | Instance.Start(1); 22 | } 23 | 24 | protected override void ProcessItem(FileItem fileItem) 25 | { 26 | // Si le client a pas demandé le progress depuis moins de 20s, annuler l'opération 27 | if (!fileItem.IpfsProcess.CanProcess()) 28 | { 29 | string message = "FileName " + Path.GetFileName(fileItem.OutputFilePath) + " car le client est déconnecté"; 30 | fileItem.IpfsProcess.Cancel("Le client est déconnecté.", message); 31 | return; 32 | } 33 | 34 | // Ipfs add file 35 | IpfsAddManager.Add(fileItem); 36 | } 37 | 38 | protected override void LogException(FileItem fileItem, Exception ex) 39 | { 40 | fileItem.IpfsProcess.SetErrorMessage("Exception non gérée", "Exception non gérée", ex); 41 | } 42 | 43 | public void Queue(FileItem fileItem) 44 | { 45 | base.Queue(fileItem, fileItem.IpfsProcess); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Ipfs/IpfsSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Managers.Ipfs 2 | { 3 | internal class IpfsSettings 4 | { 5 | private IpfsSettings(){} 6 | 7 | static IpfsSettings() 8 | { 9 | Instance = new IpfsSettings(); 10 | } 11 | 12 | public static IpfsSettings Instance { get; private set; } 13 | 14 | /// 15 | /// seconds 16 | /// 30 * 60 * 60 17 | /// 30h max pour envoyer un document 18 | /// 19 | public int IpfsTimeout { get; set; } 20 | 21 | public bool VideoAndSpriteTrickleDag { get; set; } 22 | 23 | /// 24 | /// si false, la video source ne sera pas envoyé à ipfs 25 | /// sauf si pas d'encoding video si la vidéo source dépasse 26 | /// la durée max fixé dans VideoSettings.MaxVideoDurationForEncoding 27 | /// 28 | public bool AddVideoSource { get; set; } 29 | 30 | /// 31 | /// si true, on utilise l'option --only-hash de ipfs 32 | /// ET on ne supprime pas le fichier normal du disque 33 | /// 34 | public bool OnlyHash {get; set;} 35 | } 36 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/AudioCpuEncodeDaemon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Uploader.Core.Managers.Common; 10 | using Uploader.Core.Managers.Front; 11 | using Uploader.Core.Managers.Ipfs; 12 | using Uploader.Core.Models; 13 | 14 | namespace Uploader.Core.Managers.Video 15 | { 16 | internal class AudioCpuEncodeDaemon : BaseDaemon 17 | { 18 | public static AudioCpuEncodeDaemon Instance { get; private set; } 19 | 20 | static AudioCpuEncodeDaemon() 21 | { 22 | Instance = new AudioCpuEncodeDaemon(); 23 | Instance.Start(VideoSettings.Instance.NbAudioCpuEncodeDaemon); 24 | } 25 | 26 | protected override void ProcessItem(FileItem fileItem) 27 | { 28 | // si le client a pas demandé le progress depuis plus de 20s, annuler l'opération 29 | if (!fileItem.AudioCpuEncodeProcess.CanProcess()) 30 | { 31 | string message = "FileName " + Path.GetFileName(fileItem.OutputFilePath) + " car le client est déconnecté"; 32 | fileItem.AudioCpuEncodeProcess.CancelCascade("Le client est déconnecté.", message); 33 | return; 34 | } 35 | 36 | if (EncodeManager.AudioCpuEncoding(fileItem)) 37 | { 38 | VideoGpuEncodeDaemon.Instance.Queue(fileItem, "waiting video encoding..."); 39 | } 40 | } 41 | 42 | protected override void LogException(FileItem fileItem, Exception ex) 43 | { 44 | fileItem.AudioCpuEncodeProcess.SetErrorMessage("Exception non gérée", "Exception AudioCpuEncoding non gérée", ex); 45 | } 46 | 47 | public void Queue(FileItem fileItem, string message) 48 | { 49 | base.Queue(fileItem, fileItem.AudioCpuEncodeProcess); 50 | 51 | fileItem.VideoGpuEncodeProcess.SetProgress(message, true); 52 | foreach (FileItem item in fileItem.FileContainer.EncodedFileItems) 53 | { 54 | item.IpfsProcess.SetProgress(message, true); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/AudioVideoCpuEncodeDaemon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Uploader.Core.Managers.Common; 10 | using Uploader.Core.Managers.Front; 11 | using Uploader.Core.Managers.Ipfs; 12 | using Uploader.Core.Models; 13 | 14 | namespace Uploader.Core.Managers.Video 15 | { 16 | internal class AudioVideoCpuEncodeDaemon : BaseDaemon 17 | { 18 | public static AudioVideoCpuEncodeDaemon Instance { get; private set; } 19 | 20 | static AudioVideoCpuEncodeDaemon() 21 | { 22 | Instance = new AudioVideoCpuEncodeDaemon(); 23 | Instance.Start(VideoSettings.Instance.NbAudioVideoCpuEncodeDaemon); 24 | } 25 | 26 | protected override void ProcessItem(FileItem fileItem) 27 | { 28 | // si le client a pas demandé le progress depuis plus de 20s, annuler l'opération 29 | if (!fileItem.AudioVideoCpuEncodeProcess.CanProcess()) 30 | { 31 | string message = "FileName " + Path.GetFileName(fileItem.OutputFilePath) + " car le client est déconnecté"; 32 | fileItem.AudioVideoCpuEncodeProcess.CancelCascade("Le client est déconnecté.", message); 33 | return; 34 | } 35 | 36 | if (EncodeManager.AudioVideoCpuEncoding(fileItem)) 37 | { 38 | // rechercher si c'est la video la plus petite pour le sprite 39 | if(fileItem.FileContainer.SpriteVideoFileItem != null 40 | && fileItem.VideoSize.QualityOrder == fileItem.FileContainer.EncodedFileItems.Min(e => e.VideoSize.QualityOrder)) 41 | { 42 | fileItem.FileContainer.SpriteVideoFileItem.SetSourceFilePath(fileItem.OutputFilePath); 43 | SpriteDaemon.Instance.Queue(fileItem.FileContainer.SpriteVideoFileItem, "Waiting sprite creation..."); 44 | } 45 | 46 | IpfsDaemon.Instance.Queue(fileItem); 47 | } 48 | } 49 | 50 | protected override void LogException(FileItem fileItem, Exception ex) 51 | { 52 | fileItem.AudioVideoCpuEncodeProcess.SetErrorMessage("Exception non gérée", "Exception AudioVideoCpuEncoding non gérée", ex); 53 | } 54 | 55 | public void Queue(FileItem fileItem, string messageIpfs) 56 | { 57 | base.Queue(fileItem, fileItem.AudioVideoCpuEncodeProcess); 58 | 59 | fileItem.IpfsProcess.SetProgress(messageIpfs, true); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/EncodeManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Microsoft.Extensions.Logging; 5 | using Uploader.Core.Managers.Common; 6 | using Uploader.Core.Managers.Ipfs; 7 | using Uploader.Core.Models; 8 | 9 | namespace Uploader.Core.Managers.Video 10 | { 11 | internal static class EncodeManager 12 | { 13 | public static bool AudioVideoCpuEncoding(FileItem fileItem) 14 | { 15 | try 16 | { 17 | FileItem sourceFile = fileItem.FileContainer.SourceFileItem; 18 | LogManager.AddEncodingMessage(LogLevel.Information, "SourceFilePath " + Path.GetFileName(sourceFile.SourceFilePath) + " -> " + fileItem.VideoSize, "Start AudioVideoCpuEncoding"); 19 | fileItem.AudioVideoCpuEncodeProcess.StartProcessDateTime(); 20 | 21 | string size = GetSize(fileItem.VideoSize, sourceFile.VideoWidth.Value, sourceFile.VideoHeight.Value); 22 | string arguments = $"-y -i {Path.GetFileName(sourceFile.SourceFilePath)}"; 23 | if(sourceFile.VideoPixelFormat != "yuv420p") 24 | arguments += " -pixel_format yuv420p"; 25 | 26 | // si rotation 90 ou 270, inverser la largeur et la hauteur de la video 27 | if(sourceFile.VideoRotate.HasValue && (sourceFile.VideoRotate.Value == 90 || sourceFile.VideoRotate.Value == 270)) 28 | { 29 | string[] sizes = size.Split(':'); 30 | size = $"{sizes[1]}:{sizes[0]}"; 31 | } 32 | 33 | arguments += $" -vf scale={size}"; 34 | 35 | if(sourceFile.VideoCodec != "h264") 36 | arguments += " -vcodec libx264"; 37 | 38 | if(sourceFile.AudioCodec != "aac") 39 | arguments += " -acodec aac -strict -2"; //-strict -2 pour forcer aac sur ubuntu 40 | else 41 | arguments += " -acodec copy"; 42 | 43 | arguments += $" {Path.GetFileName(fileItem.TempFilePath)}"; 44 | 45 | var ffmpegProcessManager = new FfmpegProcessManager(fileItem, fileItem.AudioVideoCpuEncodeProcess); 46 | ffmpegProcessManager.StartProcess(arguments, VideoSettings.Instance.EncodeTimeout); 47 | 48 | fileItem.ReplaceOutputPathWithTempPath(); 49 | LogManager.AddEncodingMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(fileItem.OutputFilePath) + " / FileSize " + fileItem.FileSize + " / Format " + fileItem.VideoSize, "End AudioVideoCpuEncoding"); 50 | fileItem.AudioVideoCpuEncodeProcess.EndProcessDateTime(); 51 | 52 | return true; 53 | } 54 | catch (Exception ex) 55 | { 56 | string message = "Exception AudioVideoCpuEncoding : Video Duration " + fileItem.VideoDuration + " / FileSize " + fileItem.FileSize + " / Progress " + fileItem.AudioVideoCpuEncodeProcess.Progress; 57 | fileItem.AudioVideoCpuEncodeProcess.SetErrorMessage("Exception non gérée", message, ex); 58 | return false; 59 | } 60 | } 61 | 62 | public static bool AudioCpuEncoding(FileItem fileItem) 63 | { 64 | try 65 | { 66 | LogManager.AddEncodingMessage(LogLevel.Information, "SourceFilePath " + Path.GetFileName(fileItem.SourceFilePath), "Start AudioCpuEncoding"); 67 | fileItem.AudioCpuEncodeProcess.StartProcessDateTime(); 68 | 69 | if(fileItem.FileContainer.SourceFileItem.AudioCodec == "aac") 70 | { 71 | fileItem.AudioCpuEncodeProcess.StartProcessDateTime(); 72 | fileItem.SetTempFilePath(fileItem.SourceFilePath); 73 | } 74 | else 75 | { 76 | if(fileItem.VideoCodec.ToLower() == "vp8" || fileItem.VideoCodec.ToLower() == "vp9") 77 | fileItem.SetTempFilePath(fileItem.TempFilePath.Replace(".mp4", ".mkv")); 78 | 79 | // encoding audio de la source 80 | string arguments = $"-y -i {Path.GetFileName(fileItem.SourceFilePath)} -vcodec copy -acodec aac -strict -2 {Path.GetFileName(fileItem.TempFilePath)}"; 81 | var ffmpegProcessManager = new FfmpegProcessManager(fileItem, fileItem.AudioCpuEncodeProcess); 82 | ffmpegProcessManager.StartProcess(arguments, VideoSettings.Instance.EncodeTimeout); 83 | } 84 | 85 | fileItem.SetVideoAacTempFilePath(fileItem.TempFilePath); 86 | LogManager.AddEncodingMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(fileItem.VideoAacTempFilePath) + " / FileSize " + fileItem.FileSize + " / Format " + fileItem.VideoSize, "End AudioCpuEncoding"); 87 | fileItem.AudioCpuEncodeProcess.EndProcessDateTime(); 88 | 89 | return true; 90 | } 91 | catch (Exception ex) 92 | { 93 | string message = "Exception AudioCpuEncoding : Video Duration " + fileItem.VideoDuration + " / FileSize " + fileItem.FileSize + " / Progress " + fileItem.AudioCpuEncodeProcess.Progress; 94 | fileItem.AudioCpuEncodeProcess.SetErrorMessage("Exception non gérée", message, ex); 95 | return false; 96 | } 97 | } 98 | 99 | public static bool VideoGpuEncoding(FileItem fileItem) 100 | { 101 | try 102 | { 103 | LogManager.AddEncodingMessage(LogLevel.Information, "SourceFilePath " + Path.GetFileName(fileItem.VideoAacTempFilePath) + " -> 1:N formats", "Start VideoGpuEncoding"); 104 | fileItem.VideoGpuEncodeProcess.StartProcessDateTime(); 105 | 106 | // encoding video 1:N formats 107 | string arguments = $"-y -hwaccel cuvid -vcodec h264_cuvid -vsync 0 -i {Path.GetFileName(fileItem.VideoAacTempFilePath)}"; 108 | if(VideoSettings.Instance.NVidiaCard != "QuadroP5000") 109 | arguments = arguments.Replace(" -hwaccel cuvid -vcodec h264_cuvid -vsync 0 ", " "); 110 | 111 | FileItem sourceFile = fileItem.FileContainer.SourceFileItem; 112 | foreach (FileItem item in fileItem.FileContainer.EncodedFileItems) 113 | { 114 | string size = GetSize(item.VideoSize, fileItem.VideoWidth.Value, fileItem.VideoHeight.Value); 115 | string maxRate = item.VideoSize.MaxRate; 116 | 117 | if(sourceFile.VideoPixelFormat != "yuv420p") 118 | arguments += " -pixel_format yuv420p"; 119 | 120 | // si rotation 90 ou 270, inverser la largeur et la hauteur de la video 121 | if(sourceFile.VideoRotate.HasValue && (sourceFile.VideoRotate.Value == 90 || sourceFile.VideoRotate.Value == 270)) 122 | { 123 | string[] sizes = size.Split(':'); 124 | size = $"{sizes[1]}:{sizes[0]}"; 125 | } 126 | 127 | arguments += $" -vf scale_npp={size} -b:v {maxRate} -maxrate {maxRate} -bufsize {maxRate} -vcodec h264_nvenc -acodec copy {Path.GetFileName(item.TempFilePath)}"; 128 | if(VideoSettings.Instance.NVidiaCard != "QuadroP5000") 129 | arguments = arguments.Replace("scale_npp=", "scale="); 130 | } 131 | 132 | var ffmpegProcessManager = new FfmpegProcessManager(fileItem, fileItem.VideoGpuEncodeProcess); 133 | ffmpegProcessManager.StartProcess(arguments, VideoSettings.Instance.EncodeTimeout); 134 | 135 | foreach (var item in fileItem.FileContainer.EncodedFileItems) 136 | { 137 | item.ReplaceOutputPathWithTempPath(); 138 | LogManager.AddEncodingMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(item.OutputFilePath) + " / FileSize " + item.FileSize + " / Format " + item.VideoSize, "End VideoGpuEncoding"); 139 | } 140 | fileItem.VideoGpuEncodeProcess.EndProcessDateTime(); 141 | 142 | return true; 143 | } 144 | catch (Exception ex) 145 | { 146 | string message = "Exception VideoGpuEncoding : Video Duration " + fileItem.VideoDuration + " / FileSize " + fileItem.FileSize + " / Progress " + fileItem.VideoGpuEncodeProcess.Progress; 147 | fileItem.VideoGpuEncodeProcess.SetErrorMessage("Exception non gérée", message, ex); 148 | return false; 149 | } 150 | } 151 | 152 | private static string GetSize(VideoSize videoSize, int width, int height) 153 | { 154 | Tuple finalSize = SizeHelper.GetSize(width, height, videoSize.Width, videoSize.Height); 155 | return $"{finalSize.Item1}:{finalSize.Item2}"; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/FfProbeProcessManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.Logging; 4 | using Uploader.Core.Managers.Common; 5 | using Uploader.Core.Models; 6 | 7 | namespace Uploader.Core.Managers.Video 8 | { 9 | internal class FfProbeProcessManager 10 | { 11 | private FileItem _fileItem; 12 | 13 | public FfProbeProcessManager(FileItem fileItem) 14 | { 15 | if(fileItem == null) 16 | throw new ArgumentNullException(nameof(fileItem)); 17 | 18 | _fileItem = fileItem; 19 | } 20 | 21 | public void FillInfo(int timeout) 22 | { 23 | // https://trac.ffmpeg.org/wiki/FFprobeTips 24 | string arguments = $"-v error -of default=nw=1 -show_entries stream_tags=rotate:format=size,duration:stream=index,codec_name,pix_fmt,height,width,duration,nb_frames,avg_frame_rate,bit_rate {Path.GetFileName(_fileItem.SourceFilePath)}"; 25 | 26 | var process = new ProcessManager("ffprobe", arguments, LogManager.FfmpegLogger); 27 | process.Launch(timeout); 28 | 29 | foreach (string output in process.DataOutput.ToString().Split(Environment.NewLine)) 30 | { 31 | try 32 | { 33 | Fill(output); 34 | } 35 | catch{} 36 | } 37 | } 38 | 39 | private void Fill(string output) 40 | { 41 | if (string.IsNullOrWhiteSpace(output) || output.EndsWith("=N/A")) 42 | return; 43 | 44 | if(!_fileItem.VideoDuration.HasValue && output.StartsWith("duration=")) 45 | { 46 | _fileItem.VideoDuration = Convert.ToInt32(output.Split('=')[1].Split('.')[0]); 47 | } 48 | else if(!_fileItem.VideoWidth.HasValue && output.StartsWith("width=")) 49 | { 50 | _fileItem.VideoWidth = Convert.ToInt32(output.Split('=')[1]); 51 | } 52 | else if(!_fileItem.VideoHeight.HasValue && output.StartsWith("height=")) 53 | { 54 | _fileItem.VideoHeight = Convert.ToInt32(output.Split('=')[1]); 55 | } 56 | else if(_fileItem.VideoCodec == null && output.StartsWith("codec_name=")) 57 | { 58 | _fileItem.VideoCodec = output.Split('=')[1]; 59 | } 60 | else if(_fileItem.VideoPixelFormat == null && output.StartsWith("pix_fmt=")) 61 | { 62 | _fileItem.VideoPixelFormat = output.Split('=')[1]; 63 | } 64 | else if(_fileItem.AudioCodec == null && output.StartsWith("codec_name=")) 65 | { 66 | _fileItem.AudioCodec = output.Split('=')[1]; 67 | } 68 | else if(!_fileItem.VideoBitRate.HasValue && output.StartsWith("bit_rate=")) 69 | { 70 | _fileItem.VideoBitRate = Convert.ToInt32(output.Split('=')[1]); 71 | } 72 | else if(_fileItem.VideoFrameRate == null && output.StartsWith("avg_frame_rate=")) 73 | { 74 | _fileItem.VideoFrameRate = output.Split('=')[1]; 75 | } 76 | else if(!_fileItem.VideoNbFrame.HasValue && output.StartsWith("nb_frames=")) 77 | { 78 | _fileItem.VideoNbFrame = Convert.ToInt32(output.Split('=')[1]); 79 | } 80 | else if(_fileItem.VideoRotate == null && output.StartsWith("TAG:rotate=")) 81 | { 82 | _fileItem.VideoRotate = Convert.ToInt32(output.Split('=')[1]); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/FfmpegProcessManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using Microsoft.Extensions.Logging; 6 | using Uploader.Core.Managers.Common; 7 | using Uploader.Core.Models; 8 | 9 | namespace Uploader.Core.Managers.Video 10 | { 11 | internal class FfmpegProcessManager 12 | { 13 | private FileItem _fileItem; 14 | private ProcessItem _processItem; 15 | 16 | public FfmpegProcessManager(FileItem fileItem, ProcessItem processItem) 17 | { 18 | if(fileItem == null) 19 | throw new ArgumentNullException(nameof(fileItem)); 20 | if(processItem == null) 21 | throw new ArgumentNullException(nameof(processItem)); 22 | 23 | _fileItem = fileItem; 24 | _processItem = processItem; 25 | } 26 | 27 | public void StartProcess(string arguments, int timeout) 28 | { 29 | var processStartInfo = new ProcessStartInfo(); 30 | processStartInfo.FileName = "ffmpeg"; 31 | 32 | processStartInfo.RedirectStandardError = true; 33 | processStartInfo.WorkingDirectory = TempFileManager.GetTempDirectory(); 34 | 35 | processStartInfo.UseShellExecute = false; 36 | processStartInfo.ErrorDialog = false; 37 | processStartInfo.CreateNoWindow = true; 38 | processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; 39 | 40 | processStartInfo.Arguments = arguments; 41 | 42 | if(_fileItem.TypeFile == TypeFile.SpriteVideo) 43 | LogManager.AddSpriteMessage(LogLevel.Information, processStartInfo.FileName + " " + processStartInfo.Arguments, "Launch command"); 44 | else 45 | LogManager.AddEncodingMessage(LogLevel.Information, processStartInfo.FileName + " " + processStartInfo.Arguments, "Launch command"); 46 | 47 | using(Process process = Process.Start(processStartInfo)) 48 | { 49 | process.ErrorDataReceived += new DataReceivedEventHandler(ErrorDataReceived); 50 | 51 | process.BeginErrorReadLine(); 52 | 53 | bool success = process.WaitForExit(timeout * 1000); 54 | if (!success) 55 | { 56 | throw new InvalidOperationException("Timeout : Le fichier n'a pas pu être encodé dans le temps imparti."); 57 | } 58 | 59 | if (process.ExitCode != 0) 60 | { 61 | throw new InvalidOperationException($"Le fichier n'a pas pu être encodé, erreur {process.ExitCode}."); 62 | } 63 | } 64 | } 65 | 66 | private void ErrorDataReceived(object sender, DataReceivedEventArgs e) 67 | { 68 | string output = e.Data; 69 | if (string.IsNullOrWhiteSpace(output)) 70 | return; 71 | 72 | LogManager.AddEncodingMessage(LogLevel.Debug, output, "DEBUG"); 73 | 74 | const string progressMarkup = " time="; // " time=00:01:42.08" 75 | 76 | // Récupérer la progression toutes les 1s 77 | if (_processItem.LastTimeProgressChanged.HasValue && (DateTime.UtcNow - _processItem.LastTimeProgressChanged.Value).TotalMilliseconds < 1000) 78 | return; 79 | 80 | if (!output.Contains(progressMarkup) || output.Length < (output.IndexOf(progressMarkup) + progressMarkup.Length + 8)) 81 | return; 82 | 83 | LogManager.AddEncodingMessage(LogLevel.Debug, Path.GetFileName(_fileItem.SourceFilePath) + " : " + output, "DEBUG"); 84 | 85 | // Récupérer la progression d'encodage avec la durée d'encodage traitée 86 | int durationDone = GetDurationInSeconds(output.Substring(output.IndexOf(progressMarkup) + progressMarkup.Length, 8))??0; 87 | _processItem.SetProgress(string.Format("{0:N2}%", (durationDone * 100.00 / (double) _fileItem.FileContainer.SourceFileItem.VideoDuration.Value)).Replace(',', '.')); 88 | } 89 | 90 | private static int? GetDurationInSeconds(string durationStr) 91 | { 92 | try 93 | { 94 | int[] durationTab = durationStr.Split(':').Select(v => Convert.ToInt32(v)).ToArray(); 95 | return durationTab[0] * 3600 + durationTab[1] * 60 + durationTab[2]; 96 | } 97 | catch 98 | { 99 | return null; 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/SizeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Uploader.Core.Managers.Video 4 | { 5 | internal static class SizeHelper 6 | { 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// largeur, hauteur 15 | public static Tuple GetSize(double width, double height, double finalWidth, double finalHeight) 16 | { 17 | //video verticale, garder hauteur finale, réduire largeur finale 18 | if(width / height < finalWidth / finalHeight) 19 | return new Tuple(GetWidth(width, height, finalHeight), (int)finalHeight); 20 | 21 | // sinon garder largeur finale, réduire hauteur finale 22 | return new Tuple((int)finalWidth, GetHeight(width, height, finalWidth)); 23 | } 24 | 25 | public static int GetWidth(double width, double height, double finalHeight) 26 | { 27 | return GetPair((int)(finalHeight * width / height)); 28 | } 29 | 30 | public static int GetHeight(double width, double height, double finalWidth) 31 | { 32 | return GetPair((int)(finalWidth * height / width)); 33 | } 34 | 35 | public static int GetPair(int number) 36 | { 37 | return number % 2 == 0 ? number : number + 1; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/SpriteDaemon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using Uploader.Core.Managers.Common; 9 | using Uploader.Core.Managers.Front; 10 | using Uploader.Core.Managers.Ipfs; 11 | using Uploader.Core.Models; 12 | 13 | namespace Uploader.Core.Managers.Video 14 | { 15 | internal class SpriteDaemon : BaseDaemon 16 | { 17 | public static SpriteDaemon Instance { get; private set; } 18 | 19 | static SpriteDaemon() 20 | { 21 | Instance = new SpriteDaemon(); 22 | Instance.Start(VideoSettings.Instance.NbSpriteDaemon); 23 | } 24 | 25 | protected override void ProcessItem(FileItem fileItem) 26 | { 27 | // si le client a pas demandé le progress depuis plus de 20s, annuler l'opération 28 | if (!fileItem.SpriteEncodeProcess.CanProcess()) 29 | { 30 | string message = "FileName " + Path.GetFileName(fileItem.OutputFilePath) + " car le client est déconnecté"; 31 | fileItem.SpriteEncodeProcess.CancelCascade("Le client est déconnecté.", message); 32 | return; 33 | } 34 | 35 | // sprite creation video 36 | if (SpriteManager.Encode(fileItem)) 37 | IpfsDaemon.Instance.Queue(fileItem); 38 | } 39 | 40 | protected override void LogException(FileItem fileItem, Exception ex) 41 | { 42 | fileItem.SpriteEncodeProcess.SetErrorMessage("Exception non gérée", "Exception non gérée", ex); 43 | } 44 | 45 | public void Queue(FileItem fileItem, string messageIpfs) 46 | { 47 | base.Queue(fileItem, fileItem.SpriteEncodeProcess); 48 | 49 | fileItem.IpfsProcess.SetProgress(messageIpfs, true); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/SpriteManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using Microsoft.Extensions.Logging; 7 | 8 | using Uploader.Core.Managers.Common; 9 | using Uploader.Core.Managers.Front; 10 | using Uploader.Core.Models; 11 | 12 | namespace Uploader.Core.Managers.Video 13 | { 14 | internal class SpriteManager 15 | { 16 | public static bool Encode(FileItem fileItem) 17 | { 18 | FileItem sourceFile = fileItem.FileContainer.SourceFileItem; 19 | try 20 | { 21 | fileItem.SpriteEncodeProcess.StartProcessDateTime(); 22 | 23 | LogManager.AddSpriteMessage(LogLevel.Information, "SourceFilePath " + Path.GetFileName(fileItem.SourceFilePath), "Start Sprite"); 24 | 25 | int nbImages = VideoSettings.Instance.NbSpriteImages; 26 | int heightSprite = VideoSettings.Instance.HeightSpriteImages; 27 | 28 | // Calculer nb image/s 29 | // si < 100s de vidéo -> 1 image/s 30 | // sinon (nb secondes de la vidéo / 100) image/s 31 | string frameRate = "1"; 32 | int duration = sourceFile.VideoDuration.Value; 33 | if (duration > nbImages) 34 | { 35 | frameRate = $"{nbImages}/{duration}"; //frameRate = inverse de image/s 36 | } 37 | 38 | int spriteWidth = SizeHelper.GetWidth(sourceFile.VideoWidth.Value, sourceFile.VideoHeight.Value, heightSprite); 39 | string sizeImageMax = $"scale={spriteWidth}:{heightSprite}"; 40 | 41 | // Extract frameRate image/s de la video 42 | string arguments = $"-y -i {Path.GetFileName(fileItem.SourceFilePath)} -r {frameRate} -vf {sizeImageMax} -f image2 {GetPattern(fileItem.TempFilePath)}"; 43 | var ffmpegProcessManager = new FfmpegProcessManager(fileItem, fileItem.SpriteEncodeProcess); 44 | ffmpegProcessManager.StartProcess(arguments, VideoSettings.Instance.EncodeGetImagesTimeout); 45 | IList files = GetListImageFrom(fileItem.TempFilePath); // récupération des images 46 | 47 | LogManager.AddSpriteMessage(LogLevel.Information, (files.Count - 1) + " images", "Start Combine images"); 48 | 49 | 50 | // garder que les 100 dernières images pour éliminer les premières (1 ou 2 en réalité) 51 | int skip = files.Count > VideoSettings.Instance.NbSpriteImages 52 | ? files.Count - VideoSettings.Instance.NbSpriteImages 53 | : 0; 54 | var list = new StringBuilder(); 55 | foreach (string imagePath in files.Skip(skip)) 56 | { 57 | if(list.Length > 0) 58 | list.Append(" "); 59 | 60 | list.Append(Path.GetFileName(imagePath)); 61 | } 62 | 63 | arguments = $"-mode concatenate -tile 1x {list} {Path.GetFileName(fileItem.TempFilePath)}"; 64 | var process = new ProcessManager(Path.Combine(GeneralSettings.Instance.ImageMagickPath, "montage"), arguments, LogManager.SpriteLogger); 65 | bool successSprite = process.Launch(5); 66 | TempFileManager.SafeDeleteTempFiles(files); // suppression des images 67 | if (!successSprite) 68 | { 69 | fileItem.SpriteEncodeProcess.SetErrorMessage("Error while combine images", "Error creation sprite while combine images"); 70 | return false; 71 | } 72 | 73 | fileItem.ReplaceOutputPathWithTempPath(); 74 | LogManager.AddSpriteMessage(LogLevel.Information, "OutputFileName " + Path.GetFileName(fileItem.OutputFilePath) + " / FileSize " + fileItem.FileSize, "End Sprite"); 75 | fileItem.SpriteEncodeProcess.EndProcessDateTime(); 76 | return true; 77 | } 78 | catch (Exception ex) 79 | { 80 | string message = "Video Duration " + sourceFile.VideoDuration + " / FileSize " + fileItem.FileSize + " / Progress " + fileItem.SpriteEncodeProcess.Progress; 81 | fileItem.SpriteEncodeProcess.SetErrorMessage("Exception non gérée", message, ex); 82 | IList files = GetListImageFrom(fileItem.TempFilePath); // récupération des images 83 | TempFileManager.SafeDeleteTempFiles(files); // suppression des images 84 | return false; 85 | } 86 | } 87 | 88 | private static string GetPattern(string filePath) 89 | { 90 | return Path.GetFileNameWithoutExtension(filePath) + "-%03d.jpeg"; 91 | } 92 | 93 | private static IList GetListImageFrom(string filePath) 94 | { 95 | if(string.IsNullOrWhiteSpace(filePath)) 96 | return new List(); 97 | 98 | string directoryName = Path.GetDirectoryName(filePath); 99 | if(directoryName == null) 100 | return new List(); 101 | 102 | return Directory.EnumerateFiles(directoryName, Path.GetFileNameWithoutExtension(filePath) + "-*.jpeg").OrderBy(s => s).ToList(); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/VideoGpuEncodeDaemon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Logging; 9 | using Uploader.Core.Managers.Common; 10 | using Uploader.Core.Managers.Front; 11 | using Uploader.Core.Managers.Ipfs; 12 | using Uploader.Core.Models; 13 | 14 | namespace Uploader.Core.Managers.Video 15 | { 16 | internal class VideoGpuEncodeDaemon : BaseDaemon 17 | { 18 | public static VideoGpuEncodeDaemon Instance { get; private set; } 19 | 20 | static VideoGpuEncodeDaemon() 21 | { 22 | Instance = new VideoGpuEncodeDaemon(); 23 | Instance.Start(VideoSettings.Instance.NbVideoGpuEncodeDaemon); 24 | } 25 | 26 | protected override void ProcessItem(FileItem fileItem) 27 | { 28 | // si le client a pas demandé le progress depuis plus de 20s, annuler l'opération 29 | if (!fileItem.VideoGpuEncodeProcess.CanProcess()) 30 | { 31 | string message = "FileName " + Path.GetFileName(fileItem.OutputFilePath) + " car le client est déconnecté"; 32 | fileItem.VideoGpuEncodeProcess.CancelCascade("Le client est déconnecté.", message); 33 | return; 34 | } 35 | 36 | // encoding videos par GPU 37 | if (EncodeManager.VideoGpuEncoding(fileItem)) 38 | { 39 | // rechercher la video la plus petite pour le sprite 40 | FileItem videoLight = fileItem.FileContainer.EncodedFileItems.OrderBy(e => e.VideoSize.QualityOrder).FirstOrDefault(); 41 | if(videoLight != null && fileItem.FileContainer.SpriteVideoFileItem != null) 42 | { 43 | fileItem.FileContainer.SpriteVideoFileItem.SetSourceFilePath(videoLight.OutputFilePath); 44 | SpriteDaemon.Instance.Queue(fileItem.FileContainer.SpriteVideoFileItem, "Waiting sprite creation..."); 45 | } 46 | 47 | foreach (var item in fileItem.FileContainer.EncodedFileItems) 48 | { 49 | IpfsDaemon.Instance.Queue(item); 50 | } 51 | } 52 | } 53 | 54 | protected override void LogException(FileItem fileItem, Exception ex) 55 | { 56 | fileItem.VideoGpuEncodeProcess.SetErrorMessage("Exception non gérée", "Exception VideoGpuEncoding non gérée", ex); 57 | } 58 | 59 | public void Queue(FileItem fileItem, string messageIpfs) 60 | { 61 | base.Queue(fileItem, fileItem.VideoGpuEncodeProcess); 62 | 63 | foreach (FileItem item in fileItem.FileContainer.EncodedFileItems) 64 | { 65 | item.IpfsProcess.SetProgress(messageIpfs, true); 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/VideoSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Managers.Video 2 | { 3 | internal class VideoSettings 4 | { 5 | private VideoSettings(){} 6 | 7 | static VideoSettings() 8 | { 9 | Instance = new VideoSettings(); 10 | } 11 | 12 | public static VideoSettings Instance { get; private set; } 13 | 14 | /// 15 | /// seconds 16 | /// 17 | public int FfProbeTimeout { get; set; } // 10s max pour extraire les informations de la vidéo 18 | 19 | /// 20 | /// seconds 21 | /// 22 | public int EncodeGetImagesTimeout { get; set; } // 10min max pour extraire les images du sprite de la vidéo 23 | 24 | /// 25 | /// seconds 26 | /// 27 | public int EncodeTimeout { get; set; } // 30h max pour encoder la vidéo 28 | 29 | /// 30 | /// seconds 31 | /// 32 | public int MaxVideoDurationForEncoding { get; set; } // 30 minutes max pour encoder une vidéo 33 | 34 | public int NbSpriteImages { get; set; } 35 | 36 | /// 37 | /// pixels 38 | /// 39 | public int HeightSpriteImages { get; set; } 40 | 41 | /// 42 | /// encoding audio puis encoding video 1:N formats 43 | /// 44 | public bool GpuEncodeMode { get; set; } 45 | 46 | /// 47 | /// 48 | /// 49 | public int NbSpriteDaemon { get; set; } 50 | 51 | /// 52 | /// 53 | /// 54 | public int NbAudioCpuEncodeDaemon { get; set; } 55 | 56 | /// 57 | /// 58 | /// 59 | public int NbVideoGpuEncodeDaemon { get; set; } 60 | 61 | /// 62 | /// 63 | /// 64 | public int NbAudioVideoCpuEncodeDaemon { get; set; } 65 | 66 | /// 67 | /// 68 | /// 69 | public string AuthorizedQuality { get; set; } 70 | 71 | /// 72 | /// 73 | /// 74 | public string NVidiaCard { get; set; } 75 | } 76 | } -------------------------------------------------------------------------------- /Uploader.Core/Managers/Video/VideoSourceManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.Logging; 4 | using Uploader.Core.Managers.Common; 5 | using Uploader.Core.Managers.Ipfs; 6 | using Uploader.Core.Models; 7 | 8 | namespace Uploader.Core.Managers.Video 9 | { 10 | internal static class VideoSourceManager 11 | { 12 | public static bool SuccessAnalyseSource(FileItem sourceFile, ProcessItem processItem) 13 | { 14 | if(sourceFile == null) 15 | throw new ArgumentNullException(nameof(sourceFile)); 16 | if(sourceFile == null) 17 | throw new ArgumentNullException(nameof(sourceFile)); 18 | if(!sourceFile.IsSource) 19 | throw new ArgumentException("Doit être le fichier source", nameof(sourceFile)); 20 | 21 | // Récupérer la durée totale de la vidéo et sa résolution 22 | try 23 | { 24 | var ffProbeProcessManager = new FfProbeProcessManager(sourceFile); 25 | ffProbeProcessManager.FillInfo(VideoSettings.Instance.FfProbeTimeout); 26 | } 27 | catch(Exception ex) 28 | { 29 | LogManager.AddEncodingMessage(LogLevel.Critical, "Exception non gérée", "Exception source info", ex); 30 | } 31 | 32 | // Si durée totale de vidéo, largeur hauteur non récupéré, on ne peut pas continuer 33 | if (!sourceFile.SuccessGetSourceInfo()) 34 | { 35 | string message = "Error while source video information."; 36 | string longMessage = message + " FileName : " + Path.GetFileName(sourceFile.SourceFilePath); 37 | processItem.SetErrorMessage(message, longMessage); 38 | return false; 39 | } 40 | 41 | LogManager.AddEncodingMessage(LogLevel.Information, "SourceVideoDuration " + sourceFile.VideoDuration.Value + " / SourceVideoFileSize " + sourceFile.FileSize, "Info source"); 42 | 43 | return true; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/FileContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | 6 | using Uploader.Core.Managers.Common; 7 | using Uploader.Core.Managers.Front; 8 | using Uploader.Core.Managers.Ipfs; 9 | 10 | namespace Uploader.Core.Models 11 | { 12 | internal class FileContainer 13 | { 14 | private static long nbInstance; 15 | 16 | public long NumInstance 17 | { 18 | get; 19 | } 20 | 21 | public DateTime CreationDate 22 | { 23 | get; 24 | } 25 | 26 | public string OriginFilePath { get; } 27 | 28 | public string ExceptionDetail { get; set; } 29 | 30 | public static FileContainer NewVideoContainer(string originFilePath) 31 | { 32 | FileContainer fileContainer = new FileContainer(TypeContainer.Video, originFilePath); 33 | 34 | fileContainer.SourceFileItem = FileItem.NewSourceVideoFileItem(fileContainer); 35 | 36 | ProgressManager.RegisterProgress(fileContainer); 37 | return fileContainer; 38 | } 39 | 40 | public void AddSprite() 41 | { 42 | SpriteVideoFileItem = FileItem.NewSpriteVideoFileItem(this); 43 | } 44 | 45 | public void AddEncodedVideo(IList videoSizes) 46 | { 47 | var list = new List(); 48 | foreach (VideoSize videoSize in videoSizes) 49 | { 50 | list.Add(FileItem.NewEncodedVideoFileItem(this, videoSize)); 51 | } 52 | EncodedFileItems = list; 53 | } 54 | 55 | public static FileContainer NewImageContainer(string originFilePath) 56 | { 57 | FileContainer fileContainer = new FileContainer(TypeContainer.Image, originFilePath); 58 | 59 | fileContainer.SourceFileItem = FileItem.NewSourceImageFileItem(fileContainer); 60 | fileContainer.SnapFileItem = FileItem.NewSnapImageFileItem(fileContainer); 61 | fileContainer.OverlayFileItem = FileItem.NewOverlayImageFileItem(fileContainer); 62 | 63 | ProgressManager.RegisterProgress(fileContainer); 64 | return fileContainer; 65 | } 66 | 67 | public static FileContainer NewSubtitleContainer() 68 | { 69 | FileContainer fileContainer = new FileContainer(TypeContainer.Subtitle, null); 70 | fileContainer.SubtitleFileItem = FileItem.NewSubtitleFileItem(fileContainer); 71 | 72 | ProgressManager.RegisterProgress(fileContainer); 73 | return fileContainer; 74 | } 75 | 76 | private FileContainer(TypeContainer typeContainer, string originFilePath) 77 | { 78 | nbInstance++; 79 | NumInstance = nbInstance; 80 | CreationDate = DateTime.UtcNow; 81 | 82 | TypeContainer = typeContainer; 83 | OriginFilePath = originFilePath; 84 | 85 | EncodedFileItems = new List(); 86 | 87 | ProgressToken = Guid.NewGuid(); 88 | 89 | UpdateLastTimeProgressRequest(); 90 | } 91 | 92 | public Guid ProgressToken 93 | { 94 | get; 95 | } 96 | 97 | public DateTime LastTimeProgressRequested 98 | { 99 | get; 100 | private set; 101 | } 102 | 103 | public void UpdateLastTimeProgressRequest() 104 | { 105 | LastTimeProgressRequested = DateTime.UtcNow; 106 | } 107 | 108 | public TypeContainer TypeContainer 109 | { 110 | get; 111 | } 112 | 113 | public FileItem SourceFileItem 114 | { 115 | get; 116 | private set; 117 | } 118 | 119 | public FileItem SpriteVideoFileItem 120 | { 121 | get; 122 | private set; 123 | } 124 | 125 | public IReadOnlyList EncodedFileItems 126 | { 127 | get; 128 | private set; 129 | } 130 | 131 | public FileItem SnapFileItem 132 | { 133 | get; 134 | private set; 135 | } 136 | 137 | public FileItem OverlayFileItem 138 | { 139 | get; 140 | private set; 141 | } 142 | 143 | public FileItem SubtitleFileItem 144 | { 145 | get; 146 | private set; 147 | } 148 | 149 | public bool MustAbort() 150 | { 151 | return (DateTime.UtcNow - LastTimeProgressRequested).TotalSeconds > GeneralSettings.Instance.MaxGetProgressCanceled; 152 | } 153 | 154 | private IEnumerable GetAllFile() 155 | { 156 | if(SourceFileItem != null) 157 | yield return SourceFileItem; 158 | 159 | if(SpriteVideoFileItem != null) 160 | yield return SpriteVideoFileItem; 161 | 162 | if(EncodedFileItems != null) 163 | foreach (FileItem fileItem in EncodedFileItems) 164 | yield return fileItem; 165 | 166 | if(SnapFileItem != null) 167 | yield return SnapFileItem; 168 | 169 | if(OverlayFileItem != null) 170 | yield return OverlayFileItem; 171 | 172 | if(SubtitleFileItem != null) 173 | yield return SubtitleFileItem; 174 | } 175 | 176 | public void CancelAll(string message) 177 | { 178 | foreach (var item in GetAllFile()) 179 | { 180 | item.Cancel(message); 181 | } 182 | } 183 | 184 | public void CleanFilesIfEnd() 185 | { 186 | if(!Finished()) 187 | return; 188 | 189 | foreach (FileItem item in GetAllFile()) 190 | { 191 | TempFileManager.SafeDeleteTempFiles(item.FilesToDelete, item.IpfsHash); 192 | } 193 | 194 | if (Error()) 195 | TempFileManager.SafeCopyError(OriginFilePath, SourceFileItem.IpfsHash); 196 | 197 | TempFileManager.SafeDeleteTempFile(OriginFilePath, SourceFileItem.IpfsHash); 198 | } 199 | 200 | public bool Finished() 201 | { 202 | return GetAllFile().All(f => f.Finished()); 203 | } 204 | 205 | public bool Error() 206 | { 207 | return GetAllFile().Any(f => f.Error()); 208 | } 209 | 210 | public DateTime LastActivityDateTime => Tools.Max(CreationDate, GetAllFile().Max(f => f.LastActivityDateTime)); 211 | } 212 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/FileItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | using Uploader.Core.Managers.Common; 7 | using Uploader.Core.Managers.Ipfs; 8 | using Uploader.Core.Managers.Video; 9 | 10 | namespace Uploader.Core.Models 11 | { 12 | internal class FileItem 13 | { 14 | public static FileItem NewSourceVideoFileItem(FileContainer fileContainer) 15 | { 16 | FileItem fileItem = new FileItem(fileContainer, fileContainer.OriginFilePath, TypeFile.SourceVideo); 17 | fileItem.VideoSize = null; 18 | fileItem.InfoSourceProcess = new ProcessItem(fileItem, LogManager.FfmpegLogger); 19 | return fileItem; 20 | } 21 | 22 | public static FileItem NewEncodedVideoFileItem(FileContainer fileContainer, VideoSize videoSize) 23 | { 24 | if (videoSize == null) 25 | throw new ArgumentNullException(nameof(videoSize)); 26 | 27 | FileItem fileItem = new FileItem(fileContainer, fileContainer.SourceFileItem.SourceFilePath, TypeFile.EncodedVideo); 28 | fileItem.VideoSize = videoSize; 29 | fileItem.IpfsProcess = new ProcessItem(fileItem, LogManager.IpfsLogger); 30 | return fileItem; 31 | } 32 | 33 | public static FileItem NewSpriteVideoFileItem(FileContainer fileContainer) 34 | { 35 | FileItem fileItem = new FileItem(fileContainer, fileContainer.OriginFilePath, TypeFile.SpriteVideo); 36 | fileItem.VideoSize = null; 37 | fileItem.SpriteEncodeProcess = new ProcessItem(fileItem, LogManager.SpriteLogger); 38 | fileItem.IpfsProcess = new ProcessItem(fileItem, LogManager.IpfsLogger); 39 | return fileItem; 40 | } 41 | 42 | public static FileItem NewSourceImageFileItem(FileContainer fileContainer) 43 | { 44 | FileItem fileItem = new FileItem(fileContainer, fileContainer.OriginFilePath, TypeFile.SourceImage); 45 | return fileItem; 46 | } 47 | 48 | public static FileItem NewSnapImageFileItem(FileContainer fileContainer) 49 | { 50 | FileItem fileItem = new FileItem(fileContainer, fileContainer.SourceFileItem.SourceFilePath, TypeFile.SnapImage); 51 | fileItem.IpfsProcess = new ProcessItem(fileItem, LogManager.ImageLogger); 52 | return fileItem; 53 | } 54 | 55 | public static FileItem NewOverlayImageFileItem(FileContainer fileContainer) 56 | { 57 | FileItem fileItem = new FileItem(fileContainer, fileContainer.SourceFileItem.SourceFilePath, TypeFile.OverlayImage); 58 | fileItem.IpfsProcess = new ProcessItem(fileItem, LogManager.ImageLogger); 59 | return fileItem; 60 | } 61 | 62 | public static FileItem NewSubtitleFileItem(FileContainer fileContainer) 63 | { 64 | FileItem fileItem = new FileItem(fileContainer, null, TypeFile.SubtitleText); 65 | fileItem.IpfsProcess = new ProcessItem(fileItem, LogManager.SubtitleLogger); 66 | return fileItem; 67 | } 68 | 69 | private FileItem(FileContainer fileContainer, string sourceFilePath, TypeFile typeFile) 70 | { 71 | FileContainer = fileContainer; 72 | FilesToDelete = new List(); 73 | CreationDate = DateTime.UtcNow; 74 | SetSourceFilePath(sourceFilePath); 75 | TypeFile = typeFile; 76 | switch(typeFile){ 77 | case TypeFile.EncodedVideo: 78 | case TypeFile.SourceVideo: 79 | SetTempFilePath(Path.ChangeExtension(TempFileManager.GetNewTempFilePath(), ".mp4")); 80 | break; 81 | 82 | case TypeFile.SpriteVideo: 83 | SetTempFilePath(Path.ChangeExtension(TempFileManager.GetNewTempFilePath(), ".jpeg")); 84 | break; 85 | 86 | case TypeFile.SourceImage: 87 | case TypeFile.SnapImage: 88 | case TypeFile.OverlayImage: 89 | SetTempFilePath(Path.ChangeExtension(TempFileManager.GetNewTempFilePath(), ".png")); 90 | break; 91 | 92 | case TypeFile.SubtitleText: 93 | SetTempFilePath(Path.ChangeExtension(TempFileManager.GetNewTempFilePath(), ".vtt")); 94 | break; 95 | } 96 | } 97 | 98 | public void AddGpuEncodeProcess() 99 | { 100 | AudioCpuEncodeProcess = new ProcessItem(this, LogManager.FfmpegLogger); 101 | VideoGpuEncodeProcess = new ProcessItem(this, LogManager.FfmpegLogger); 102 | } 103 | 104 | public void AddCpuEncodeProcess() 105 | { 106 | AudioVideoCpuEncodeProcess = new ProcessItem(this, LogManager.FfmpegLogger); 107 | } 108 | 109 | public DateTime CreationDate 110 | { 111 | get; 112 | } 113 | 114 | public TypeFile TypeFile 115 | { 116 | get; 117 | } 118 | 119 | public bool IsSource => TypeFile == TypeFile.SourceVideo || TypeFile == TypeFile.SourceImage; 120 | 121 | public DateTime LastActivityDateTime => Tools.Max(CreationDate, GetAllProcess().Any() ? GetAllProcess().Max(p => p.LastActivityDateTime) : DateTime.MinValue); 122 | 123 | public long? FileSize { get; private set; } 124 | 125 | public string OutputFilePath { get { return _outputFilePath; } } 126 | private string _outputFilePath; 127 | 128 | public void SetOutputFilePath(string path) 129 | { 130 | _outputFilePath = path; 131 | SafeAddFileToDelete(path); 132 | 133 | if (OutputFilePath != null && File.Exists(OutputFilePath)) 134 | FileSize = new FileInfo(OutputFilePath).Length; 135 | } 136 | 137 | private void SafeAddFileToDelete(string path) 138 | { 139 | if(string.IsNullOrWhiteSpace(path)) 140 | return; 141 | 142 | if(!FilesToDelete.Contains(path) && path != FileContainer.OriginFilePath) 143 | FilesToDelete.Add(path); 144 | } 145 | 146 | public void ReplaceOutputPathWithTempPath() 147 | { 148 | SetOutputFilePath(TempFilePath); 149 | } 150 | 151 | public string SourceFilePath { get { return _sourceFilePath; } } 152 | private string _sourceFilePath; 153 | 154 | public void SetSourceFilePath(string path) 155 | { 156 | _sourceFilePath = path; 157 | SafeAddFileToDelete(path); 158 | } 159 | 160 | public string TempFilePath { get { return _tempFilePath; } } 161 | private string _tempFilePath; 162 | 163 | public void SetTempFilePath(string path) 164 | { 165 | _tempFilePath = path; 166 | SafeAddFileToDelete(path); 167 | } 168 | 169 | public string VideoAacTempFilePath { get { return _videoAacTempFilePath; } } 170 | private string _videoAacTempFilePath; 171 | 172 | public void SetVideoAacTempFilePath(string path) 173 | { 174 | _videoAacTempFilePath = path; 175 | SafeAddFileToDelete(path); 176 | } 177 | 178 | public FileContainer FileContainer 179 | { 180 | get; 181 | } 182 | 183 | public VideoSize VideoSize 184 | { 185 | get; 186 | private set; 187 | } 188 | 189 | public string VideoFrameRate 190 | { 191 | get; 192 | set; 193 | } 194 | 195 | public int? VideoBitRate 196 | { 197 | get; 198 | set; 199 | } 200 | 201 | public int? VideoRotate 202 | { 203 | get; 204 | set; 205 | } 206 | 207 | public int? VideoNbFrame 208 | { 209 | get; 210 | set; 211 | } 212 | 213 | public string VideoPixelFormat 214 | { 215 | get; 216 | set; 217 | } 218 | 219 | public string AudioCodec 220 | { 221 | get; 222 | set; 223 | } 224 | 225 | public string VideoCodec 226 | { 227 | get; 228 | set; 229 | } 230 | 231 | /// 232 | /// in seconds 233 | /// 234 | /// 235 | public int? VideoDuration 236 | { 237 | get; 238 | set; 239 | } 240 | 241 | public int? VideoHeight 242 | { 243 | get; 244 | set; 245 | } 246 | 247 | public int? VideoWidth 248 | { 249 | get; 250 | set; 251 | } 252 | 253 | /// 254 | /// Récupération durée et résolution de la vidéo source 255 | /// 256 | /// 257 | public ProcessItem InfoSourceProcess 258 | { 259 | get; 260 | private set; 261 | } 262 | 263 | /// 264 | /// Encode audio de la source 265 | /// 266 | /// 267 | public ProcessItem AudioCpuEncodeProcess 268 | { 269 | get; 270 | private set; 271 | } 272 | 273 | /// 274 | /// Encode video et audi des formats demandés par CPU 275 | /// 276 | /// 277 | public ProcessItem AudioVideoCpuEncodeProcess 278 | { 279 | get; 280 | private set; 281 | } 282 | 283 | /// 284 | /// Encode video des formats demandés sans l'audio par GPU 285 | /// 286 | /// 287 | public ProcessItem VideoGpuEncodeProcess 288 | { 289 | get; 290 | private set; 291 | } 292 | 293 | /// 294 | /// Sprite création d'une video 295 | /// 296 | /// 297 | public ProcessItem SpriteEncodeProcess 298 | { 299 | get; 300 | private set; 301 | } 302 | 303 | public ProcessItem IpfsProcess 304 | { 305 | get; 306 | private set; 307 | } 308 | 309 | public string IpfsHash 310 | { 311 | get; 312 | set; 313 | } 314 | 315 | public IList FilesToDelete { get; private set; } 316 | 317 | public bool SuccessGetSourceInfo() 318 | { 319 | return (VideoDuration??0) > 0 320 | && (VideoWidth??0) > 0 321 | && (VideoHeight??0) > 0 322 | //&& (VideoBitRate??0) > 0 323 | //&& (VideoNbFrame??0) > 0 324 | //&& !string.IsNullOrWhiteSpace(VideoFrameRate) 325 | && !string.IsNullOrWhiteSpace(VideoCodec) 326 | && !string.IsNullOrWhiteSpace(VideoPixelFormat) 327 | && !string.IsNullOrWhiteSpace(AudioCodec); 328 | } 329 | 330 | public bool HasReachMaxVideoDurationForEncoding() 331 | { 332 | return VideoDuration.HasValue ? VideoDuration.Value > VideoSettings.Instance.MaxVideoDurationForEncoding : false; 333 | } 334 | 335 | public void AddIpfsProcess(string sourceFilePath) 336 | { 337 | if(IpfsProcess == null) 338 | { 339 | IpfsProcess = new ProcessItem(this, LogManager.IpfsLogger); 340 | IpfsProcess.CantCascadeCancel = true; 341 | SetOutputFilePath(sourceFilePath); 342 | } 343 | } 344 | 345 | private IEnumerable GetAllProcess() 346 | { 347 | if(IpfsProcess != null) 348 | yield return IpfsProcess; 349 | 350 | if(AudioCpuEncodeProcess != null) 351 | yield return AudioCpuEncodeProcess; 352 | 353 | if(AudioVideoCpuEncodeProcess != null) 354 | yield return AudioVideoCpuEncodeProcess; 355 | 356 | if(VideoGpuEncodeProcess != null) 357 | yield return VideoGpuEncodeProcess; 358 | 359 | if(SpriteEncodeProcess != null) 360 | yield return SpriteEncodeProcess; 361 | } 362 | 363 | public void Cancel(string message) 364 | { 365 | if(!GetAllProcess().Any()) 366 | return; 367 | 368 | foreach (ProcessItem item in GetAllProcess().Where(p => p.Unstarted() && !p.CantCascadeCancel)) 369 | { 370 | item.CancelUnstarted(message); 371 | } 372 | } 373 | 374 | public bool Finished() 375 | { 376 | if(!GetAllProcess().Any()) 377 | return true; 378 | 379 | return GetAllProcess().All(p => p.Finished()); 380 | } 381 | 382 | public bool Error() 383 | { 384 | if(!GetAllProcess().Any()) 385 | return false; 386 | 387 | return GetAllProcess().Any(p => p.CurrentStep == ProcessStep.Error); 388 | } 389 | } 390 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/ProcessItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | using Uploader.Core.Managers.Common; 5 | 6 | namespace Uploader.Core.Models 7 | { 8 | internal class ProcessItem 9 | { 10 | public ProcessItem(FileItem fileItem, ILogger logger) 11 | { 12 | CurrentStep = ProcessStep.Init; 13 | FileItem = fileItem; 14 | Logger = logger; 15 | CreationDate = DateTime.UtcNow; 16 | } 17 | 18 | public FileItem FileItem 19 | { 20 | get; 21 | private set; 22 | } 23 | 24 | public ILogger Logger 25 | { 26 | get; 27 | private set; 28 | } 29 | 30 | public bool CantCascadeCancel 31 | { 32 | get; 33 | set; 34 | } 35 | 36 | public DateTime CreationDate 37 | { 38 | get; 39 | } 40 | 41 | /// 42 | /// Date d'inscription dans la queue à son enregistrement 43 | /// 44 | /// 45 | public DateTime DateInQueue 46 | { 47 | get; 48 | private set; 49 | } 50 | 51 | /// 52 | /// Numéro attribué dans la queue à son enregistrement 53 | /// 54 | /// 55 | public int PositionInQueue 56 | { 57 | get; 58 | private set; 59 | } 60 | 61 | /// 62 | /// Position d'attente dans la queue à son enregistrement 63 | /// 64 | /// 65 | public int OriginWaitingPositionInQueue 66 | { 67 | get; 68 | private set; 69 | } 70 | 71 | public long? WaitingTime 72 | { 73 | get 74 | { 75 | if(CurrentStep == ProcessStep.Init || CurrentStep == ProcessStep.Canceled) 76 | return null; 77 | 78 | if(CurrentStep == ProcessStep.Waiting) 79 | return (long)(DateTime.UtcNow - DateInQueue).TotalSeconds; 80 | 81 | return (long)(StartProcess - DateInQueue).TotalSeconds; 82 | } 83 | } 84 | 85 | public long? ProcessTime 86 | { 87 | get 88 | { 89 | if(CurrentStep < ProcessStep.Started) 90 | return null; 91 | 92 | if(CurrentStep == ProcessStep.Started) 93 | return (long)(DateTime.UtcNow - StartProcess).TotalSeconds; 94 | 95 | return (long)(EndProcess - StartProcess).TotalSeconds; 96 | } 97 | } 98 | 99 | public DateTime LastActivityDateTime => Tools.Max(CreationDate, DateInQueue, StartProcess, EndProcess); 100 | 101 | public DateTime StartProcess 102 | { 103 | get; 104 | private set; 105 | } 106 | 107 | public DateTime EndProcess 108 | { 109 | get; 110 | private set; 111 | } 112 | 113 | public string Progress 114 | { 115 | get; 116 | private set; 117 | } 118 | 119 | public DateTime? LastTimeProgressChanged 120 | { 121 | get; 122 | private set; 123 | } 124 | 125 | public ProcessStep CurrentStep 126 | { 127 | get; 128 | private set; 129 | } 130 | 131 | public string ErrorMessage 132 | { 133 | get; 134 | private set; 135 | } 136 | 137 | public void SavePositionInQueue(int totalAddToQueue, int currentPositionInQueue) 138 | { 139 | PositionInQueue = totalAddToQueue; 140 | OriginWaitingPositionInQueue = totalAddToQueue - currentPositionInQueue; 141 | 142 | DateInQueue = DateTime.UtcNow; 143 | CurrentStep = ProcessStep.Waiting; 144 | } 145 | 146 | public bool Unstarted() 147 | { 148 | return CurrentStep == ProcessStep.Init || CurrentStep == ProcessStep.Waiting; 149 | } 150 | 151 | public bool Finished() 152 | { 153 | return CurrentStep == ProcessStep.Success || CurrentStep == ProcessStep.Error || CurrentStep == ProcessStep.Canceled; 154 | } 155 | 156 | public bool CanProcess() 157 | { 158 | return !FileItem.FileContainer.MustAbort() && CurrentStep == ProcessStep.Waiting; 159 | } 160 | 161 | public void CancelCascade(string shortMessage, string longMessage) 162 | { 163 | Cancel(shortMessage, longMessage); 164 | FileItem.FileContainer.CancelAll(shortMessage); 165 | } 166 | 167 | public void Cancel(string shortMessage, string longMessage) 168 | { 169 | LogManager.Log(Logger, LogLevel.Warning, longMessage, "Annulation"); 170 | Cancel(shortMessage); 171 | } 172 | 173 | public void CancelUnstarted(string message) 174 | { 175 | if(!Unstarted()) 176 | return; 177 | 178 | Cancel(message); 179 | } 180 | 181 | private void Cancel(string message) 182 | { 183 | Progress = null; 184 | LastTimeProgressChanged = null; 185 | ErrorMessage = message; 186 | CurrentStep = ProcessStep.Canceled; 187 | } 188 | 189 | public void StartProcessDateTime() 190 | { 191 | SetProgress("0.00%"); 192 | 193 | StartProcess = DateTime.UtcNow; 194 | CurrentStep = ProcessStep.Started; 195 | } 196 | 197 | public void SetProgress(string progress, bool initMessage = false) 198 | { 199 | Progress = progress; 200 | 201 | if(!initMessage) 202 | LastTimeProgressChanged = DateTime.UtcNow; 203 | } 204 | 205 | public void SetErrorMessage(string shortMessage, string longMessage) 206 | { 207 | LogManager.Log(Logger, LogLevel.Error, longMessage, "Error"); 208 | 209 | ErrorMessage = shortMessage; 210 | 211 | EndProcess = DateTime.UtcNow; 212 | CurrentStep = ProcessStep.Error; 213 | 214 | FileItem.FileContainer.CancelAll(shortMessage); 215 | } 216 | 217 | public void SetErrorMessage(string shortMessage, string longMessage, Exception exception) 218 | { 219 | LogManager.Log(Logger, LogLevel.Critical, longMessage, "Exception", exception); 220 | 221 | ErrorMessage = shortMessage; 222 | 223 | EndProcess = DateTime.UtcNow; 224 | CurrentStep = ProcessStep.Error; 225 | 226 | FileItem.FileContainer.ExceptionDetail = exception.ToString(); 227 | FileItem.FileContainer.CancelAll(shortMessage); 228 | } 229 | 230 | public void EndProcessDateTime() 231 | { 232 | SetProgress("100.00%"); 233 | 234 | EndProcess = DateTime.UtcNow; 235 | CurrentStep = ProcessStep.Success; 236 | } 237 | } 238 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/ProcessStep.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Models 2 | { 3 | internal enum ProcessStep 4 | { 5 | Init, 6 | Waiting, 7 | Canceled, 8 | Started, 9 | Error, 10 | Success 11 | } 12 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/TypeContainer.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Models 2 | { 3 | internal enum TypeContainer 4 | { 5 | Undefined, 6 | Video, 7 | Image, 8 | Subtitle 9 | } 10 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/TypeFile.cs: -------------------------------------------------------------------------------- 1 | namespace Uploader.Core.Models 2 | { 3 | internal enum TypeFile 4 | { 5 | Undefined, 6 | 7 | SourceVideo, 8 | EncodedVideo, 9 | SpriteVideo, 10 | 11 | SourceImage, 12 | SnapImage, 13 | OverlayImage, 14 | 15 | SubtitleText 16 | } 17 | } -------------------------------------------------------------------------------- /Uploader.Core/Models/VideoSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Uploader.Core.Models 6 | { 7 | internal class VideoSize 8 | { 9 | public string UrlTag { get; set; } 10 | public int Width { get; set; } 11 | public int Height { get; set; } 12 | public int MinSourceHeightForEncoding { get; set; } 13 | public string MaxRate { get; set; } 14 | public int QualityOrder { get; set; } 15 | } 16 | 17 | internal static class VideoSizeFactory 18 | { 19 | private static Dictionary _dico = new Dictionary(); 20 | 21 | public static void Init(Dictionary dico) 22 | { 23 | _dico = dico; 24 | } 25 | 26 | public static VideoSize GetSize(string urlTag) 27 | { 28 | if(!_dico.ContainsKey(urlTag)) 29 | throw new InvalidOperationException("Format non reconnu."); 30 | 31 | return _dico[urlTag]; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Uploader.Core/Uploader.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Uploader.Core/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtube/ipfs-uploader/7766a5fffb9cf8cdc021ebfbcd27982d35680f97/Uploader.Core/overlay.png -------------------------------------------------------------------------------- /Uploader.Web/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/Uploader.Web.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "${auto-detect-url}", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start ${auto-detect-url}" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | }, 36 | { 37 | "name": ".NET Core Attach", 38 | "type": "coreclr", 39 | "request": "attach", 40 | "processId": "${command:pickProcess}" 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /Uploader.Web/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Uploader.Web.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /Uploader.Web/Attributes/DisableFormValueModelBindingAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using Microsoft.AspNetCore.Mvc.ModelBinding; 5 | 6 | namespace Uploader.Web.Attributes 7 | { 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter 10 | { 11 | public void OnResourceExecuting(ResourceExecutingContext context) 12 | { 13 | var formValueProviderFactory = context.ValueProviderFactories 14 | .OfType() 15 | .FirstOrDefault(); 16 | if (formValueProviderFactory != null) 17 | { 18 | context.ValueProviderFactories.Remove(formValueProviderFactory); 19 | } 20 | 21 | var jqueryFormValueProviderFactory = context.ValueProviderFactories 22 | .OfType() 23 | .FirstOrDefault(); 24 | if (jqueryFormValueProviderFactory != null) 25 | { 26 | context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory); 27 | } 28 | } 29 | 30 | public void OnResourceExecuted(ResourceExecutedContext context) 31 | { 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Uploader.Web/Controllers/ProgressController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.ModelBinding; 8 | 9 | using Uploader.Web.Attributes; 10 | using Uploader.Web.Helper; 11 | 12 | using Uploader.Core.Managers.Front; 13 | 14 | namespace Uploader.Web.Controllers 15 | { 16 | [Route("progress")] 17 | public class ProgressController : Controller 18 | { 19 | [HttpGet] 20 | [Route("/getStatus")] 21 | public IActionResult GetStatus(bool details = false) 22 | { 23 | return Json(ProgressManager.GetStats(details)); 24 | } 25 | 26 | [HttpGet] 27 | [Route("/getErrors")] 28 | public IActionResult GetErrors() 29 | { 30 | return Json(ProgressManager.GetErrors()); 31 | } 32 | 33 | [HttpGet] 34 | [Route("/getProgressByToken/{token}")] 35 | public IActionResult GetProgressByToken(Guid token) 36 | { 37 | dynamic result = ProgressManager.GetFileContainerByToken(token); 38 | if (result == null) 39 | { 40 | return BadRequest(new 41 | { 42 | errorMessage = "token not exist" 43 | }); 44 | } 45 | return Json(result); 46 | } 47 | 48 | [HttpGet] 49 | [Route("/getProgressBySourceHash/{sourceHash}")] 50 | public IActionResult GetProgressBySourceHash(string sourceHash) 51 | { 52 | dynamic result = ProgressManager.GetFileContainerBySourceHash(sourceHash); 53 | if (result == null) 54 | { 55 | return BadRequest(new 56 | { 57 | errorMessage = "hash not exist" 58 | }); 59 | } 60 | return Json(result); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Uploader.Web/Controllers/UploaderController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.ModelBinding; 6 | using Microsoft.Extensions.Logging; 7 | 8 | using Uploader.Web.Attributes; 9 | using Uploader.Web.Helper; 10 | 11 | using Uploader.Core.Managers.Common; 12 | using Uploader.Core.Managers.Front; 13 | 14 | namespace Uploader.Web.Controllers 15 | { 16 | [Route("uploader")] 17 | public class UploaderController : Controller 18 | { 19 | [HttpPost] 20 | [DisableFormValueModelBinding] 21 | [DisableRequestSizeLimit] 22 | [Route("/uploadVideo")] 23 | public async Task UploadVideo(string videoEncodingFormats = null, bool? sprite = null) 24 | { 25 | try 26 | { 27 | return Ok(new 28 | { 29 | success = true, token = VideoManager.ComputeVideo(await GetFileToTemp(), videoEncodingFormats, sprite) 30 | }); 31 | } 32 | catch (Exception ex) 33 | { 34 | LogManager.AddEncodingMessage(LogLevel.Critical, "Exception non gérée", "Exception", ex); 35 | return BadRequest(new 36 | { 37 | errorMessage = ex.Message 38 | }); 39 | } 40 | } 41 | 42 | [HttpPost] 43 | [DisableFormValueModelBinding] 44 | [DisableRequestSizeLimit] 45 | [Route("/uploadImage")] 46 | public async Task UploadImage() 47 | { 48 | try 49 | { 50 | return Ok(new 51 | { 52 | success = true, token = ImageManager.ComputeImage(await GetFileToTemp()) 53 | }); 54 | } 55 | catch (Exception ex) 56 | { 57 | LogManager.AddImageMessage(LogLevel.Critical, "Exception non gérée", "Exception", ex); 58 | return BadRequest(new 59 | { 60 | errorMessage = ex.Message 61 | }); 62 | } 63 | } 64 | 65 | [HttpPost] 66 | [Route("/uploadSubtitle")] 67 | public async Task UploadSubtitle(string subtitle) 68 | { 69 | try 70 | { 71 | return Ok(new 72 | { 73 | success = true, token = await SubtitleManager.ComputeSubtitle(subtitle) 74 | }); 75 | } 76 | catch (Exception ex) 77 | { 78 | LogManager.AddSubtitleMessage(LogLevel.Critical, $"Exception ConvertSubtitle : {ex}", "Exception"); 79 | return BadRequest(new 80 | { 81 | errorMessage = ex.Message 82 | }); 83 | } 84 | } 85 | 86 | private async Task GetFileToTemp() 87 | { 88 | // Copy file to temp location 89 | string sourceFilePath = TempFileManager.GetNewTempFilePath(); 90 | 91 | FormValueProvider formModel; 92 | 93 | try 94 | { 95 | // Récupération du fichier 96 | using(System.IO.FileStream stream = System.IO.File.Create(sourceFilePath)) 97 | { 98 | formModel = await Request.StreamFile(stream); 99 | } 100 | } 101 | catch(Exception ex) 102 | { 103 | LogManager.AddGeneralMessage(LogLevel.Critical, $"Exception Download File : {ex}", "Exception"); 104 | TempFileManager.SafeDeleteTempFile(sourceFilePath); 105 | throw; 106 | } 107 | 108 | try 109 | { 110 | ValueProviderResult fileName = formModel.GetValue("qqFileName"); 111 | if(fileName.Length == 1) 112 | { 113 | var extension = System.IO.Path.GetExtension(fileName.FirstValue); 114 | if(!string.IsNullOrWhiteSpace(extension)) 115 | { 116 | string newFilePath = System.IO.Path.ChangeExtension(sourceFilePath, extension); 117 | System.IO.File.Move(sourceFilePath, newFilePath); 118 | sourceFilePath = newFilePath; 119 | } 120 | } 121 | } 122 | catch {} 123 | 124 | return sourceFilePath; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Uploader.Web/Helpers/FileStreamingHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Http.Features; 9 | using Microsoft.AspNetCore.Mvc.ModelBinding; 10 | using Microsoft.AspNetCore.WebUtilities; 11 | 12 | using Microsoft.Net.Http.Headers; 13 | 14 | namespace Uploader.Web.Helper 15 | { 16 | public static class FileStreamingHelper 17 | { 18 | private static readonly FormOptions _defaultFormOptions = new FormOptions(); 19 | 20 | public static async Task StreamFile(this HttpRequest request, Stream targetStream) 21 | { 22 | if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType)) 23 | { 24 | throw new Exception($"Expected a multipart request, but got {request.ContentType}"); 25 | } 26 | 27 | // Used to accumulate all the form url encoded key value pairs in the 28 | // request. 29 | var formAccumulator = new KeyValueAccumulator(); 30 | string boundary = MultipartRequestHelper.GetBoundary( 31 | MediaTypeHeaderValue.Parse(request.ContentType), 32 | _defaultFormOptions.MultipartBoundaryLengthLimit); 33 | var reader = new MultipartReader(boundary, request.Body, 81920); 34 | 35 | var section = await reader.ReadNextSectionAsync(); 36 | while (section != null) 37 | { 38 | ContentDispositionHeaderValue contentDisposition; 39 | bool hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition); 40 | 41 | if (hasContentDispositionHeader) 42 | { 43 | if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) 44 | { 45 | await section.Body.CopyToAsync(targetStream); 46 | } 47 | else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition)) 48 | { 49 | // Content-Disposition: form-data; name="key" 50 | // 51 | // value 52 | 53 | // Do not limit the key name length here because the 54 | // multipart headers length limit is already in effect. 55 | string key = HeaderUtilities.RemoveQuotes(contentDisposition.Name).Value; 56 | Encoding encoding = GetEncoding(section); 57 | using(var streamReader = new StreamReader( 58 | section.Body, 59 | encoding, 60 | detectEncodingFromByteOrderMarks : true, 61 | bufferSize : 1024, 62 | leaveOpen : true)) 63 | { 64 | // The value length limit is enforced by MultipartBodyLengthLimit 65 | string value = await streamReader.ReadToEndAsync(); 66 | if (string.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase)) 67 | { 68 | value = string.Empty; 69 | } 70 | formAccumulator.Append(key, value); 71 | 72 | if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit) 73 | { 74 | throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded."); 75 | } 76 | } 77 | } 78 | } 79 | 80 | // Drains any remaining section body that has not been consumed and 81 | // reads the headers for the next section. 82 | section = await reader.ReadNextSectionAsync(); 83 | } 84 | 85 | // Bind form data to a model 86 | var formValueProvider = new FormValueProvider( 87 | BindingSource.Form, 88 | new FormCollection(formAccumulator.GetResults()), 89 | CultureInfo.CurrentCulture); 90 | 91 | return formValueProvider; 92 | } 93 | 94 | private static Encoding GetEncoding(MultipartSection section) 95 | { 96 | MediaTypeHeaderValue mediaType; 97 | bool hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType); 98 | // UTF-7 is insecure and should not be honored. UTF-8 will succeed in 99 | // most cases. 100 | if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding)) 101 | { 102 | return Encoding.UTF8; 103 | } 104 | return mediaType.Encoding; 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /Uploader.Web/Helpers/MultipartRequestHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | using Microsoft.Net.Http.Headers; 5 | 6 | namespace Uploader.Web.Helper 7 | { 8 | public static class MultipartRequestHelper 9 | { 10 | // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" 11 | // The spec says 70 characters is a reasonable limit. 12 | public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) 13 | { 14 | string boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; 15 | if (string.IsNullOrWhiteSpace(boundary)) 16 | { 17 | throw new InvalidDataException("Missing content-type boundary."); 18 | } 19 | 20 | if (boundary.Length > lengthLimit) 21 | { 22 | throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); 23 | } 24 | 25 | return boundary; 26 | } 27 | 28 | public static bool IsMultipartContentType(string contentType) 29 | { 30 | return !string.IsNullOrEmpty(contentType) && 31 | contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; 32 | } 33 | 34 | public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) 35 | { 36 | // Content-Disposition: form-data; name="key"; 37 | return contentDisposition != null && 38 | contentDisposition.DispositionType.Equals("form-data") && 39 | string.IsNullOrEmpty(contentDisposition.FileName.Value) && 40 | string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); 41 | } 42 | 43 | public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) 44 | { 45 | // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" 46 | return contentDisposition != null && 47 | contentDisposition.DispositionType.Equals("form-data") && 48 | (!string.IsNullOrEmpty(contentDisposition.FileName.Value) || 49 | !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /Uploader.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Uploader.Web 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseSetting(WebHostDefaults.DetailedErrorsKey, "true") 16 | .UseUrls("http://0.0.0.0:5000") 17 | .UseStartup() 18 | .Build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Uploader.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Cors; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Http.Features; 11 | using Microsoft.AspNetCore.HttpOverrides; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Logging; 15 | 16 | using Uploader.Core.Log; 17 | 18 | namespace Uploader.Web 19 | { 20 | public class Startup 21 | { 22 | public Startup(IConfiguration configuration) 23 | { 24 | Configuration = configuration; 25 | } 26 | 27 | public IConfiguration Configuration 28 | { 29 | get; 30 | } 31 | 32 | // This method gets called by the runtime. Use this method to add services to the container. 33 | public void ConfigureServices(IServiceCollection services) 34 | { 35 | services.AddCors(); 36 | services.AddMvc(); 37 | } 38 | 39 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 40 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 41 | { 42 | loggerFactory.AddLog4Net(); 43 | Uploader.Core.Managers.Common.LogManager.Init(loggerFactory); 44 | 45 | Uploader.Core.Managers.Common.Startup.InitSettings(Configuration); 46 | 47 | if (env.IsDevelopment()) 48 | { 49 | app.UseDeveloperExceptionPage(); 50 | } 51 | 52 | app.UseForwardedHeaders(new ForwardedHeadersOptions{ ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); 53 | 54 | // A déclarer avant UseMvc 55 | string origins = Configuration.GetValue("Front:CORS"); 56 | Console.WriteLine("CORS Settings: " + origins); 57 | app.UseCors( 58 | options => options.WithOrigins(origins).AllowAnyMethod() 59 | ); 60 | 61 | app.UseMvc(); 62 | app.UseStaticFiles(); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /Uploader.Web/Uploader.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Uploader.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "Front": { 11 | "CORS": "http://localhost:3000" 12 | }, 13 | "General": { 14 | "MaxGetProgressCanceled": 120, 15 | "ImageMagickPath": "", 16 | "TempFilePath": "/home/dr/testfolder", 17 | "FinalFilePath": "/home/dr/testfolder2" 18 | }, 19 | "Ipfs": { 20 | "AddVideoSource": true, 21 | "OnlyHash": true 22 | }, 23 | "Video": { 24 | "GpuEncodeMode": true, 25 | "NbSpriteDaemon": 1, 26 | "NbAudioVideoCpuEncodeDaemon": 0, 27 | "NbAudioCpuEncodeDaemon": 1, 28 | "NbVideoGpuEncodeDaemon": 1, 29 | "AuthorizedQuality": "240p", 30 | "NVidiaCard": "1080GTX" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Uploader.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | }, 15 | 16 | "Front": { 17 | "CORS": "https://d.tube" 18 | }, 19 | 20 | "General": { 21 | "MaxGetProgressCanceled": 20, 22 | "ImageMagickPath": "", 23 | "TempFilePath": "/home/dr/testfolder", 24 | "ErrorFilePath": "/home/dr/testfolder3", 25 | "FinalFilePath": "/home/dr/testfolder2", 26 | "Version": "0.7.5" 27 | }, 28 | 29 | "Encode": [ 30 | { 31 | "urlTag": "144p", 32 | "maxRate": "100k", 33 | "width": 256, 34 | "height": 144, 35 | "MinSourceHeightForEncoding": 80, 36 | "qualityOrder": 1 37 | }, 38 | { 39 | "urlTag": "240p", 40 | "maxRate": "200k", 41 | "width": 426, 42 | "height": 240, 43 | "MinSourceHeightForEncoding": 120, 44 | "qualityOrder": 2 45 | }, 46 | { 47 | "urlTag": "360p", 48 | "maxRate": "400k", 49 | "width": 640, 50 | "height": 360, 51 | "MinSourceHeightForEncoding": 300, 52 | "qualityOrder": 3 53 | }, 54 | { 55 | "urlTag": "480p", 56 | "maxRate": "1000k", 57 | "width": 854, 58 | "height": 480, 59 | "MinSourceHeightForEncoding": 360, 60 | "qualityOrder": 4 61 | }, 62 | { 63 | "urlTag": "720p", 64 | "maxRate": "2000k", 65 | "width": 1280, 66 | "height": 720, 67 | "MinSourceHeightForEncoding": 600, 68 | "qualityOrder": 5 69 | }, 70 | { 71 | "urlTag": "1080p", 72 | "maxRate": "3200k", 73 | "width": 1920, 74 | "height": 1080, 75 | "MinSourceHeightForEncoding": 900, 76 | "qualityOrder": 6 77 | } 78 | ], 79 | 80 | "Ipfs": { 81 | "IpfsTimeout": 108000, 82 | "VideoAndSpriteTrickleDag": true, 83 | "AddVideoSource": true, 84 | "OnlyHash": false 85 | }, 86 | 87 | "Video": { 88 | "FfProbeTimeout": 10, 89 | "EncodeGetImagesTimeout": 600, 90 | "EncodeTimeout": 108000, 91 | "MaxVideoDurationForEncoding": 1800, 92 | "NbSpriteImages": 100, 93 | "HeightSpriteImages": 118, 94 | "GpuEncodeMode": true, 95 | "NbSpriteDaemon": 0, 96 | "NbAudioVideoCpuEncodeDaemon": 0, 97 | "NbAudioCpuEncodeDaemon": 0, 98 | "NbVideoGpuEncodeDaemon": 0, 99 | "AuthorizedQuality": "240p", 100 | "NVidiaCard":"QuadroP5000" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Uploader.Web/appsettings.prod_cpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "Ipfs": { 3 | "AddVideoSource": true 4 | }, 5 | "Video": { 6 | "GpuEncodeMode": false, 7 | "NbSpriteDaemon": 1, 8 | "NbAudioVideoCpuEncodeDaemon": 1, 9 | "AuthorizedQuality": "480p" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Uploader.Web/appsettings.prod_gpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "Ipfs": { 3 | "AddVideoSource": true 4 | }, 5 | "Video": { 6 | "GpuEncodeMode": true, 7 | "NbSpriteDaemon": 1, 8 | "NbAudioCpuEncodeDaemon": 2, 9 | "NbVideoGpuEncodeDaemon": 4, 10 | "AuthorizedQuality": "240p,480p,720p" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Uploader.Web/log4net.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /Uploader.Web/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtube/ipfs-uploader/7766a5fffb9cf8cdc021ebfbcd27982d35680f97/Uploader.Web/overlay.png -------------------------------------------------------------------------------- /Uploader.Web/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 81 | 82 | 83 | 96 | 97 |

Video

98 |
99 | 111 | 112 |

Image

113 |
114 | 126 | 127 |

Subtitle

128 |
129 |
130 | 131 | 134 |
135 | 136 |
137 | 138 | -------------------------------------------------------------------------------- /Uploader.Web/wwwroot/scripts/fine-uploader-gallery.min.css: -------------------------------------------------------------------------------- 1 | .qq-gallery .qq-btn{float:right;border:none;padding:0;margin:0;box-shadow:none}.qq-gallery .qq-upload-button{display:inline;width:105px;padding:7px 10px;float:left;text-align:center;background:#00abc7;color:#fff;border-radius:2px;border:1px solid #37b7cc;box-shadow:0 1px 1px rgba(255,255,255,.37) inset,1px 0 1px rgba(255,255,255,.07) inset,0 1px 0 rgba(0,0,0,.36),0 -2px 12px rgba(0,0,0,.08) inset}.qq-gallery .qq-upload-button-hover{background:#33b6cc}.qq-gallery .qq-upload-button-focus{outline:1px dotted #000}.qq-gallery.qq-uploader{position:relative;min-height:200px;max-height:490px;overflow-y:hidden;width:inherit;border-radius:6px;border:1px dashed #ccc;background-color:#fafafa;padding:20px}.qq-gallery.qq-uploader:before{content:attr(qq-drop-area-text) " ";position:absolute;font-size:200%;left:0;width:100%;text-align:center;top:45%;opacity:.25}.qq-gallery .qq-upload-drop-area,.qq-upload-extra-drop-area{position:absolute;top:0;left:0;width:100%;height:100%;min-height:30px;z-index:2;background:#f9f9f9;border-radius:4px;text-align:center}.qq-gallery .qq-upload-drop-area span{display:block;position:absolute;top:50%;width:100%;margin-top:-8px;font-size:16px}.qq-gallery .qq-upload-extra-drop-area{position:relative;margin-top:50px;font-size:16px;padding-top:30px;height:20px;min-height:40px}.qq-gallery .qq-upload-drop-area-active{background:#fdfdfd;border-radius:4px}.qq-gallery .qq-upload-list{margin:0;padding:10px 0 0;list-style:none;max-height:450px;overflow-y:auto;clear:both;box-shadow:none}.qq-gallery .qq-upload-list li{display:inline-block;position:relative;max-width:120px;margin:0 25px 25px 0;padding:0;line-height:16px;font-size:13px;color:#424242;background-color:#fff;border-radius:2px;box-shadow:0 1px 1px 0 rgba(0,0,0,.22);vertical-align:top;height:186px}.qq-gallery .qq-upload-continue,.qq-gallery .qq-upload-delete,.qq-gallery .qq-upload-failed-text,.qq-gallery .qq-upload-pause,.qq-gallery .qq-upload-retry,.qq-gallery .qq-upload-size,.qq-gallery .qq-upload-spinner{display:inline}.qq-gallery .qq-upload-continue:hover,.qq-gallery .qq-upload-delete:hover,.qq-gallery .qq-upload-pause:hover,.qq-gallery .qq-upload-retry:hover{background-color:transparent}.qq-gallery .qq-upload-cancel,.qq-gallery .qq-upload-continue,.qq-gallery .qq-upload-delete,.qq-gallery .qq-upload-pause{cursor:pointer}.qq-gallery .qq-upload-continue,.qq-gallery .qq-upload-delete,.qq-gallery .qq-upload-pause{border:none;background:0 0;color:#00a0ba;font-size:12px;padding:0}.qq-gallery .qq-upload-status-text{color:#333;font-size:12px;padding-left:3px;padding-top:2px;width:inherit;display:none;width:108px}.qq-gallery .qq-upload-fail .qq-upload-status-text{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden;display:block}.qq-gallery .qq-upload-retrying .qq-upload-status-text{display:inline-block}.qq-gallery .qq-upload-retrying .qq-progress-bar-container{display:none}.qq-gallery .qq-upload-cancel{background-color:#525252;color:#f7f7f7;font-weight:700;font-family:Arial,Helvetica,sans-serif;border-radius:12px;border:none;height:22px;width:22px;padding:4px;position:absolute;right:-5px;top:-6px;margin:0;line-height:17px}.qq-gallery .qq-upload-cancel:hover{background-color:#525252}.qq-gallery .qq-upload-retry{cursor:pointer;position:absolute;top:30px;left:50%;margin-left:-31px;box-shadow:0 1px 1px rgba(255,255,255,.37) inset,1px 0 1px rgba(255,255,255,.07) inset,0 4px 4px rgba(0,0,0,.5),0 -2px 12px rgba(0,0,0,.08) inset;padding:3px 4px;border:1px solid #d2ddc7;border-radius:2px;color:inherit;background-color:#ebf6e0;z-index:1}.qq-gallery .qq-upload-retry:hover{background-color:#f7ffec}.qq-gallery .qq-file-info{padding:10px 6px 4px;margin-top:-3px;border-radius:0 0 2px 2px;text-align:left;overflow:hidden}.qq-gallery .qq-file-info .qq-file-name{position:relative}.qq-gallery .qq-upload-file{display:block;margin-right:0;margin-bottom:3px;width:auto;text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.qq-gallery .qq-upload-spinner{display:inline-block;background:url(loading.gif);position:absolute;left:50%;margin-left:-7px;top:53px;width:15px;height:15px;vertical-align:text-bottom}.qq-gallery .qq-drop-processing{display:block}.qq-gallery .qq-drop-processing-spinner{display:inline-block;background:url(processing.gif);width:24px;height:24px;vertical-align:text-bottom}.qq-gallery .qq-upload-failed-text{display:none;font-style:italic;font-weight:700}.qq-gallery .qq-upload-failed-icon{display:none;width:15px;height:15px;vertical-align:text-bottom}.qq-gallery .qq-upload-fail .qq-upload-failed-text{display:inline}.qq-gallery .qq-upload-retrying .qq-upload-failed-text{display:inline}.qq-gallery .qq-upload-list li.qq-upload-success{background-color:#f2f7ed}.qq-gallery .qq-upload-list li.qq-upload-fail{background-color:#f5eded;box-shadow:0 0 1px 0 red;border:0}.qq-gallery .qq-progress-bar{display:block;background:#00abc7;width:0%;height:15px;border-radius:6px;margin-bottom:3px}.qq-gallery .qq-total-progress-bar{height:25px;border-radius:9px}.qq-gallery .qq-total-progress-bar-container{margin-left:9px;display:inline;float:right;width:500px}.qq-gallery .qq-upload-size{float:left;font-size:11px;color:#929292;margin-bottom:3px;margin-right:0;display:inline-block}.qq-gallery INPUT.qq-edit-filename{position:absolute;opacity:0;z-index:-1}.qq-gallery .qq-upload-file.qq-editable{cursor:pointer;margin-right:20px}.qq-gallery .qq-edit-filename-icon.qq-editable{display:inline-block;cursor:pointer;position:absolute;right:0;top:0}.qq-gallery INPUT.qq-edit-filename.qq-editing{position:static;height:28px;width:90px;width:-moz-available;padding:0 8px;margin-bottom:3px;border:1px solid #ccc;border-radius:2px;font-size:13px;opacity:1}.qq-gallery .qq-edit-filename-icon{display:none;background:url(edit.gif);width:15px;height:15px;vertical-align:text-bottom}.qq-gallery .qq-delete-icon{background:url(trash.gif);width:15px;height:15px;vertical-align:sub;display:inline-block}.qq-gallery .qq-retry-icon{background:url(retry.gif);width:15px;height:15px;vertical-align:sub;display:inline-block;float:none}.qq-gallery .qq-continue-icon{background:url(continue.gif);width:15px;height:15px;vertical-align:sub;display:inline-block}.qq-gallery .qq-pause-icon{background:url(pause.gif);width:15px;height:15px;vertical-align:sub;display:inline-block}.qq-gallery .qq-hide{display:none}.qq-gallery .qq-in-progress .qq-thumbnail-wrapper{opacity:.5}.qq-gallery .qq-thumbnail-wrapper{overflow:hidden;position:relative;height:120px;width:120px}.qq-gallery .qq-thumbnail-selector{border-radius:2px 2px 0 0;bottom:0;top:0;margin:auto;display:block}:root *>.qq-gallery .qq-thumbnail-selector{position:relative;top:50%;transform:translateY(-50%);-moz-transform:translateY(-50%);-ms-transform:translateY(-50%);-webkit-transform:translateY(-50%)}.qq-gallery.qq-uploader DIALOG{display:none}.qq-gallery.qq-uploader DIALOG[open]{display:block}.qq-gallery.qq-uploader DIALOG{display:none}.qq-gallery.qq-uploader DIALOG[open]{display:block}.qq-gallery.qq-uploader DIALOG .qq-dialog-buttons{text-align:center;padding-top:10px}.qq-gallery.qq-uploader DIALOG .qq-dialog-buttons BUTTON{margin-left:5px;margin-right:5px}.qq-gallery.qq-uploader DIALOG .qq-dialog-message-selector{padding-bottom:10px}.qq-gallery .qq-uploader DIALOG::backdrop{background-color:rgba(0,0,0,.7)}/*# sourceMappingURL=fine-uploader-gallery.min.css.map */ -------------------------------------------------------------------------------- /Uploader.Web/wwwroot/scripts/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtube/ipfs-uploader/7766a5fffb9cf8cdc021ebfbcd27982d35680f97/Uploader.Web/wwwroot/scripts/loading.gif -------------------------------------------------------------------------------- /Uploader.Web/wwwroot/scripts/retry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtube/ipfs-uploader/7766a5fffb9cf8cdc021ebfbcd27982d35680f97/Uploader.Web/wwwroot/scripts/retry.gif -------------------------------------------------------------------------------- /Uploader.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uploader.Core", "Uploader.Core\Uploader.Core.csproj", "{0F299D35-AF53-45CB-95FC-3F624A6EF7B0}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uploader.Web", "Uploader.Web\Uploader.Web.csproj", "{4DD8B942-EE1F-40D1-93F6-C7B9D809851C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Debug|x64.ActiveCfg = Debug|x64 26 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Debug|x64.Build.0 = Debug|x64 27 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Debug|x86.ActiveCfg = Debug|x86 28 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Debug|x86.Build.0 = Debug|x86 29 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Release|x64.ActiveCfg = Release|x64 32 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Release|x64.Build.0 = Release|x64 33 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Release|x86.ActiveCfg = Release|x86 34 | {0F299D35-AF53-45CB-95FC-3F624A6EF7B0}.Release|x86.Build.0 = Release|x86 35 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Debug|x64.ActiveCfg = Debug|x64 38 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Debug|x64.Build.0 = Debug|x64 39 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Debug|x86.ActiveCfg = Debug|x86 40 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Debug|x86.Build.0 = Debug|x86 41 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Release|x64.ActiveCfg = Release|x64 44 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Release|x64.Build.0 = Release|x64 45 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Release|x86.ActiveCfg = Release|x86 46 | {4DD8B942-EE1F-40D1-93F6-C7B9D809851C}.Release|x86.Build.0 = Release|x86 47 | EndGlobalSection 48 | EndGlobal 49 | --------------------------------------------------------------------------------