diff --git a/README.md b/README.md
index 6def826..0d734ad 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,11 @@
# SharpGrabber
-
-A **.NET Standard** library for grabbing information and
-downloading from top media providers such as **YouTube**, **Instagram** etc.
-## Features
-- Grabs useful information about media such as length, title, author and many more.
-- Deciphers secure *YouTube* videos optionally.
-- Extracts direct links to all available qualities.
-- Extracts images and thumbnails.
-- Supports *asynchronous* operations.
+This project consists of several connected sub-projects:
+- `SharpGrabber` is a *.NET Standard* library for crawling into top media provider websites such as **YouTube**, **Instagram** etc. in order to grab information and return direct links of the audio/video files.
+- `SharpGrabber.Converter` is a *.NET Standard* library based on `ffmpeg` to join audio and video streams. This is particularly useful when grabbing high quality *YouTube* media that might be separated into audio and video files.
+- `SharpGrabber.Desktop` A cross-platform desktop application
+which utilizes both mentioned libraries to expose their functionality for desktop end-users.
### Supported Providers
The following providers are currently supported with the option
@@ -18,13 +14,25 @@ to easily add more or even override part of grabbing algorithm with your own cod
- YouTube
- Instagram
-## Installation
-Install *SharpGrabber* automatically using NuGet package manager.
+## Features
+#### SharpGrabber Library
+- Grabs useful information about media such as length, title, author and many more.
+- Deciphers secure *YouTube* videos optionally.
+- Extracts direct links to all available qualities.
+- Extracts images and thumbnails.
+- Supports *asynchronous* operations.
+
+#### SharpGrabber.Desktop Application
+- Displays information obtained by the `SharpGrabber` library and downloads the resolved direct links.
+- Uses `SharpGrabber.Converter` to merge YouTube separated audio and video streams into complete media files.
+
+## SharpGrabber Installation
+Include *SharpGrabber* library in your own .NET projects.
### Install via NuGet
Install-Package DotNetTools.SharpGrabber -Version 1.0.0
-## Usage Example
+## SharpGrabber Usage Example
### Download specifically from a provider
@@ -38,11 +46,19 @@ Install *SharpGrabber* automatically using NuGet package manager.
var result = await grabber.GrabAsync(new Uri(""));
IList grabbedResources = result.Resources;
+## SharpGrabber.Desktop
+Requirements of the cross-platform desktop application to run and operate correctly:
+ - .NET Core 2.1 or higher (.NET Framework 4.6.1 or higher)
+ - Shared libraries of *ffmpeg* copied into `ffmpeg` directory alongside app executable files for media conversion support.
+ - On Windows, you may download the latest Zeranoe ffmpeg build.
+
+
+
## Roadmap
This project is very much in progress and the following features
are top priority:
-- Conversion support (especially useful for high quality YouTube videos)
-- .NET Core demo app
+- Support for Android
+- Accelerate downloads in the desktop app (like a download manager)
- Support for more media providers
## Support
diff --git a/SharpGrabber.sln b/SharpGrabber.sln
index 1018ec1..39c31ac 100644
--- a/SharpGrabber.sln
+++ b/SharpGrabber.sln
@@ -1,10 +1,15 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29418.71
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetTools.SharpGrabber", "src\DotNetTools.SharpGrabber\DotNetTools.SharpGrabber.csproj", "{F2CB23B0-E878-4CDE-9F29-C3B8E915125B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGrabber", "src\SharpGrabber\SharpGrabber.csproj", "{F2CB23B0-E878-4CDE-9F29-C3B8E915125B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetTools.SharpGrabber.Tests", "tests\DotNetTools.SharpGrabber.Tests\DotNetTools.SharpGrabber.Tests.csproj", "{B597E685-3509-435E-B11A-BC5A151051CE}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGrabber.Tests", "tests\DotNetTools.SharpGrabber.Tests\SharpGrabber.Tests.csproj", "{B597E685-3509-435E-B11A-BC5A151051CE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGrabber.Converter", "src\SharpGrabber.Converter\SharpGrabber.Converter.csproj", "{C0A29188-D1FA-4F00-A77A-D1B17FD992FF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpGrabber.Desktop", "src\SharpGrabber.Desktop\SharpGrabber.Desktop.csproj", "{F57085EC-1886-4671-A8B2-7EAAA4EE1871}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -20,6 +25,14 @@ Global
{B597E685-3509-435E-B11A-BC5A151051CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B597E685-3509-435E-B11A-BC5A151051CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B597E685-3509-435E-B11A-BC5A151051CE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C0A29188-D1FA-4F00-A77A-D1B17FD992FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0A29188-D1FA-4F00-A77A-D1B17FD992FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0A29188-D1FA-4F00-A77A-D1B17FD992FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0A29188-D1FA-4F00-A77A-D1B17FD992FF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F57085EC-1886-4671-A8B2-7EAAA4EE1871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F57085EC-1886-4671-A8B2-7EAAA4EE1871}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F57085EC-1886-4671-A8B2-7EAAA4EE1871}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F57085EC-1886-4671-A8B2-7EAAA4EE1871}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/assets/SharpGrabberDesktop-ScreenShot-1.png b/assets/SharpGrabberDesktop-ScreenShot-1.png
new file mode 100644
index 0000000..6191651
Binary files /dev/null and b/assets/SharpGrabberDesktop-ScreenShot-1.png differ
diff --git a/src/SharpGrabber.Converter/FFMpegHelper.cs b/src/SharpGrabber.Converter/FFMpegHelper.cs
new file mode 100644
index 0000000..b2d2060
--- /dev/null
+++ b/src/SharpGrabber.Converter/FFMpegHelper.cs
@@ -0,0 +1,27 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ internal static class FFMpegHelper
+ {
+ public static unsafe string av_strerror(int error)
+ {
+ var bufferSize = 1024;
+ var buffer = stackalloc byte[bufferSize];
+ ffmpeg.av_strerror(error, buffer, (ulong)bufferSize);
+ var message = Marshal.PtrToStringAnsi((IntPtr)buffer);
+ return message;
+ }
+
+ public static int ThrowOnError(this int result)
+ {
+ if (result < 0)
+ throw new ApplicationException(av_strerror(result));
+ return result;
+ }
+ }
+}
diff --git a/src/SharpGrabber.Converter/IOContext.cs b/src/SharpGrabber.Converter/IOContext.cs
new file mode 100644
index 0000000..17bb618
--- /dev/null
+++ b/src/SharpGrabber.Converter/IOContext.cs
@@ -0,0 +1,51 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ unsafe sealed class IOContext : IDisposable
+ {
+ #region Fields
+ private AVIOContext* _ioContext;
+ private bool usedAvioOpen;
+ #endregion
+
+ #region Properties
+ public AVIOContext* Pointer => _ioContext;
+ #endregion
+
+ #region Constructor
+ public IOContext(AVIOContext* ioContext)
+ {
+ _ioContext = ioContext;
+ }
+
+ public IOContext(string path, int flags)
+ {
+ usedAvioOpen = true;
+ AVIOContext* ioContext = null;
+ ffmpeg.avio_open2(&ioContext, path, flags, null, null).ThrowOnError();
+ _ioContext = ioContext;
+ }
+
+ public IOContext(Uri uri, int flags) : this(uri.IsFile ? uri.LocalPath : uri.ToString(), flags) { }
+ #endregion
+
+ #region Methods
+ public void Dispose()
+ {
+ if (_ioContext != null)
+ {
+ var ioContext = _ioContext;
+ if (usedAvioOpen)
+ ffmpeg.avio_close(ioContext);
+ else
+ ffmpeg.avio_context_free(&ioContext);
+ _ioContext = null;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaBuilder.cs b/src/SharpGrabber.Converter/MediaBuilder.cs
new file mode 100644
index 0000000..9e4b374
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaBuilder.cs
@@ -0,0 +1,272 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ ///
+ /// Can mux multiple media streams together into a container.
+ ///
+ public unsafe sealed class MediaBuilder
+ {
+ #region Fields
+ private readonly Dictionary _sources = new Dictionary();
+ #endregion
+
+ #region Properties
+ ///
+ /// Path to the output file
+ ///
+ public string OutputPath { get; set; }
+
+ public string OutputShortName { get; set; } = "webm";
+
+ public string OutputMimeType { get; set; } = "video/webm";
+
+ public AVHWDeviceType HardwareDevice { get; set; }
+
+ public AVCodecID? TargetAudioCodec { get; set; } = null;
+
+ public AVCodecID? TargetVideoCodec { get; set; } = null;
+ #endregion
+
+ #region Constructor
+ public MediaBuilder() { }
+
+ public MediaBuilder(string outputPath)
+ {
+ OutputPath = outputPath;
+ }
+ #endregion
+
+ #region Methods
+ public void AddStreamSource(Uri path, MediaStreamType streamType)
+ {
+ _sources.Add(streamType, new MediaStreamSource(path, streamType));
+ }
+
+ public void AddStreamSource(string path, MediaStreamType streamType) => AddStreamSource(new Uri($"file://{path.Replace('\\', '/')}"), streamType);
+
+ public void AutoSelectHardwareDevice()
+ {
+ var types = MediaHelper.GetHardwareDeviceTypes();
+ if (types.Length > 0)
+ HardwareDevice = types[0];
+ }
+ #endregion
+
+ #region Internal Methods
+ private void ValidateArguments()
+ {
+ if (string.IsNullOrEmpty(OutputPath))
+ throw new ArgumentNullException(nameof(OutputPath));
+ if (_sources.Count < 1 || _sources.Count > 2)
+ throw new ArgumentException("Can only merge one or two source streams.");
+ }
+ #endregion
+
+ #region Build Methods
+ private Dictionary MakeDecoders()
+ {
+ var decoders = new Dictionary();
+ foreach (var source in _sources.Values)
+ {
+ var decoder = new MediaDecoder(source.Path, HardwareDevice);
+ switch (source.StreamType)
+ {
+ case MediaStreamType.Audio:
+ decoder.SelectStream(AVMediaType.AVMEDIA_TYPE_AUDIO);
+ break;
+
+ case MediaStreamType.Video:
+ decoder.SelectStream(AVMediaType.AVMEDIA_TYPE_VIDEO);
+ break;
+
+ default:
+ throw new NotSupportedException($"Media stream type of {source.StreamType} is not supported.");
+ }
+ decoders.Add(source.StreamType, decoder);
+ }
+ return decoders;
+ }
+
+ public void Build()
+ {
+ var streams = new AVStream*[2];
+ var stream_dic = new Dictionary();
+
+ ValidateArguments();
+
+ // create decoders
+ var decoders = MakeDecoders();
+ try
+ {
+ // open output iocontext
+ using (var output = new IOContext(OutputPath, ffmpeg.AVIO_FLAG_WRITE))
+ // open muxer
+ using (var muxer = new MediaMuxer(output, OutputShortName, OutputMimeType))
+ {
+ // add streams
+ var index = 0;
+ foreach (var decoderPair in decoders)
+ {
+ var decoder = decoderPair.Value;
+ var targetCodec = decoder.CodecId;
+ var decoderCodec = decoder.CodecContext;
+
+ switch (decoderPair.Key)
+ {
+ case MediaStreamType.Audio:
+ if (TargetAudioCodec != null)
+ targetCodec = TargetAudioCodec.Value;
+ break;
+
+ case MediaStreamType.Video:
+ if (TargetVideoCodec != null)
+ targetCodec = TargetVideoCodec.Value;
+ break;
+ }
+
+ var encoder = ffmpeg.avcodec_find_encoder(targetCodec);
+ var outStream = muxer.AddStream(encoder);
+ var param = outStream->codecpar;
+ streams[index] = outStream;
+ stream_dic.Add(decoderPair.Key, index++);
+
+ if (decoder.CodecId == targetCodec)
+ {
+ // converting to the same codec
+ ffmpeg.avcodec_parameters_from_context(param, decoder.GetStream()->codec).ThrowOnError();
+ }
+ else
+ {
+ // converting to another codec
+ switch (decoderPair.Key)
+ {
+ case MediaStreamType.Audio:
+ param->codec_id = targetCodec;
+ param->codec_type = AVMediaType.AVMEDIA_TYPE_AUDIO;
+ param->sample_rate = decoderCodec->sample_rate;
+ param->channels = decoderCodec->channels;
+ param->channel_layout = decoderCodec->channel_layout;
+ outStream->time_base = decoderCodec->time_base;
+ break;
+
+ case MediaStreamType.Video:
+ throw new NotSupportedException();
+ }
+ }
+ outStream->codecpar->codec_tag = 0;
+ }
+
+ // write headers
+ muxer.WriteHeader();
+
+ // write packets
+ long audio_dts = ffmpeg.AV_NOPTS_VALUE, video_dts = ffmpeg.AV_NOPTS_VALUE;
+ long audio_pts = 0, video_pts = 0;
+ var audio_stream = decoders.Where(pair => pair.Key == MediaStreamType.Audio).First();
+ var video_stream = decoders.Where(pair => pair.Key == MediaStreamType.Video).First();
+ bool any_audio = true, any_video = true;
+
+ while (true)
+ {
+ bool anyPacket = false;
+
+ while (any_audio || any_video)
+ {
+ KeyValuePair decoderPair;
+
+ // choose a decoder pair
+ if (!any_audio)
+ decoderPair = video_stream;
+ else if (!any_video)
+ decoderPair = audio_stream;
+ else
+ {
+ // choose between audio and video
+ decoderPair = video_dts < audio_dts ? video_stream : audio_stream;
+ }
+
+ // decoder is chosen now,
+ // let's read and encode
+ var decoder = decoderPair.Value;
+ var stream_index = stream_dic[decoderPair.Key];
+ var outputStream = streams[stream_index];
+
+ if (decoder.CodecId == outputStream->codec->codec_id)
+ {
+ // simply copy to target stream
+ using (var inputFrame = decoder.ReadPacket())
+ {
+ if (inputFrame == null)
+ {
+ if (decoder == audio_stream.Value)
+ any_audio = false;
+ else
+ any_video = false;
+ continue;
+ }
+ var pck = inputFrame.Pointer;
+ ffmpeg.av_packet_rescale_ts(pck, decoder.TimeBase, outputStream->time_base);
+ pck->stream_index = stream_index;
+
+ long* last_dts, last_pts;
+ switch (decoder.CodecContext->codec_type)
+ {
+ case AVMediaType.AVMEDIA_TYPE_AUDIO:
+ last_dts = &audio_dts;
+ last_pts = &audio_pts;
+ break;
+
+ case AVMediaType.AVMEDIA_TYPE_VIDEO:
+ last_dts = &video_dts;
+ last_pts = &video_pts;
+ break;
+
+ default:
+ throw new NotSupportedException();
+ }
+
+ if (pck->dts < (*last_dts + ((muxer.FormatContextPtr->oformat->flags & ffmpeg.AVFMT_TS_NONSTRICT) > 0 ? 0 : 1)) && pck->dts != ffmpeg.AV_NOPTS_VALUE && *last_dts != ffmpeg.AV_NOPTS_VALUE)
+ {
+ var next_dts = (*last_dts) + 1;
+ if (pck->pts >= pck->dts && pck->pts != ffmpeg.AV_NOPTS_VALUE)
+ pck->pts = Math.Max(pck->pts, next_dts);
+
+ if (pck->pts == ffmpeg.AV_NOPTS_VALUE)
+ pck->pts = next_dts;
+ pck->dts = next_dts;
+ }
+ (*last_dts) = pck->dts;
+
+ muxer.WritePacket(pck);
+ anyPacket = true;
+ }
+ }
+ else
+ throw new NotSupportedException("Format conversion is not supported.");
+ }
+
+ if (!anyPacket)
+ break;
+ }
+
+ // write trailer
+ muxer.WriteTrailer();
+ }
+ }
+ finally
+ {
+ // dispose decoders
+ foreach (var decoder in decoders.Values)
+ decoder.Dispose();
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaDecoder.cs b/src/SharpGrabber.Converter/MediaDecoder.cs
new file mode 100644
index 0000000..f42c166
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaDecoder.cs
@@ -0,0 +1,234 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Text;
+using FFmpeg.AutoGen;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ public sealed unsafe class MediaDecoder : IDisposable
+ {
+ #region Fields
+ private AVFormatContext* _avFormatContext;
+ private AVCodecContext* _avCodecContext;
+ private int _streamIndex;
+ #endregion
+
+ #region Properties
+ public Uri Source { get; }
+
+ public AVHWDeviceType HardwareDevice { get; }
+
+ public AVCodecID CodecId { get; private set; }
+
+ public string CodecName { get; private set; }
+
+ public long BitRate { get; private set; }
+
+ public AVRational FrameRate { get; private set; }
+
+ public AVRational TimeBase { get; private set; }
+
+ public Size FrameSize { get; private set; }
+
+ public int AudioFrameSize { get; private set; }
+
+ public AVPixelFormat PixelFormat { get; private set; }
+
+ public AVCodecContext* CodecContext => _avCodecContext;
+ #endregion
+
+ #region Constructor
+ public MediaDecoder(Uri source, AVHWDeviceType hardwareDevice)
+ {
+ Source = source;
+ HardwareDevice = hardwareDevice;
+ Open();
+ }
+ #endregion
+
+ #region Internal Methods
+ private void Open()
+ {
+ string path = Source.IsFile ? Source.LocalPath : Source.ToString();
+ var avFormatContext = _avFormatContext = ffmpeg.avformat_alloc_context();
+ ffmpeg.avformat_open_input(&avFormatContext, path, null, null).ThrowOnError();
+ ffmpeg.avformat_find_stream_info(avFormatContext, null).ThrowOnError();
+ }
+
+ private bool ReadFrame(AVPacket* packet)
+ {
+ do
+ {
+ var result = ffmpeg.av_read_frame(_avFormatContext, packet);
+ if (result == ffmpeg.AVERROR_EOF)
+ return false;
+ result.ThrowOnError();
+ } while (packet->stream_index != _streamIndex);
+
+ return true;
+ }
+ #endregion
+
+ #region Methods
+ public void SelectStream(AVMediaType type)
+ {
+ AVCodec* avCodec = null;
+ _streamIndex = ffmpeg.av_find_best_stream(_avFormatContext, type, -1, -1, &avCodec, 0).ThrowOnError();
+ _avCodecContext = ffmpeg.avcodec_alloc_context3(avCodec);
+ var stream = _avFormatContext->streams[_streamIndex];
+
+ if (HardwareDevice != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
+ ffmpeg.av_hwdevice_ctx_create(&_avCodecContext->hw_device_ctx, HardwareDevice, null, null, 0).ThrowOnError();
+
+ ffmpeg.avcodec_parameters_to_context(_avCodecContext, stream->codecpar).ThrowOnError();
+ ffmpeg.avcodec_open2(_avCodecContext, avCodec, null).ThrowOnError();
+
+ CodecId = avCodec->id;
+ CodecName = ffmpeg.avcodec_get_name(CodecId);
+ FrameSize = new Size(_avCodecContext->width, _avCodecContext->height);
+ AudioFrameSize = _avCodecContext->frame_size; ;
+ PixelFormat = HardwareDevice == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE ? _avCodecContext->pix_fmt : GetHWPixelFormat(HardwareDevice);
+ BitRate = _avCodecContext->bit_rate;
+ FrameRate = _avCodecContext->framerate;
+ TimeBase = stream->time_base;
+ }
+
+ public AVStream* GetStream()
+ {
+ return _avFormatContext->streams[_streamIndex];
+ }
+
+ public void Dispose()
+ {
+ if (_avFormatContext != null)
+ {
+ var avFormatContext = _avFormatContext;
+ if (Source.IsFile)
+ ffmpeg.avformat_close_input(&avFormatContext);
+ else
+ ffmpeg.avformat_free_context(avFormatContext);
+ _avFormatContext = null;
+ }
+
+ if (_avCodecContext != null)
+ {
+ var avCodecContext = _avCodecContext;
+ ffmpeg.avcodec_free_context(&avCodecContext);
+ _avCodecContext = null;
+ }
+ }
+
+ ///
+ /// Tries to read the next packet. Returns NULL on EOF.
+ ///
+ public MediaPacket ReadPacket()
+ {
+ var packet = ffmpeg.av_packet_alloc();
+
+ try
+ {
+ ffmpeg.av_init_packet(packet);
+
+ if (!ReadFrame(packet))
+ return null;
+
+ return new MediaPacket(packet);
+ }
+ catch (Exception)
+ {
+ ffmpeg.av_packet_unref(packet);
+ throw;
+ }
+ }
+
+ public MediaFrame ReadFrame(MediaPacket packet)
+ {
+ var frame = ffmpeg.av_frame_alloc();
+ ffmpeg.avcodec_send_packet(_avCodecContext, packet.Pointer).ThrowOnError();
+
+ while (true)
+ {
+ var result = ffmpeg.avcodec_receive_frame(_avCodecContext, frame).ThrowOnError();
+ if (result == ffmpeg.AVERROR(ffmpeg.EAGAIN))
+ {
+ ReadFrame(packet.Pointer);
+ continue;
+ }
+ result.ThrowOnError();
+ break;
+ };
+
+ // hardware decode
+ if (HardwareDevice != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
+ {
+ var hwframe = ffmpeg.av_frame_alloc();
+ ffmpeg.av_hwframe_transfer_data(hwframe, frame, 0).ThrowOnError();
+ ffmpeg.av_frame_unref(frame);
+ frame = hwframe;
+ }
+
+ return new MediaFrame(frame);
+ }
+
+ public MediaFrame ReadFrame()
+ {
+ using (var packet = ReadPacket())
+ {
+ if (packet == null)
+ return null;
+ return ReadFrame(packet);
+ }
+ }
+
+ public bool ReadFrameAndConvert(Action frameFeed, AVPixelFormat pixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24)
+ {
+ using (var frame = ReadFrame())
+ {
+ if (frame == null)
+ return false;
+
+ using (var conf = new VideoFrameConverter(FrameSize, PixelFormat, FrameSize, pixelFormat))
+ {
+ var convFrame = conf.Convert(*frame.Pointer);
+ frameFeed.Invoke(convFrame, (IntPtr)convFrame.data[0]);
+ return true;
+ }
+ }
+ }
+ #endregion
+
+ #region Static Methods
+ private static AVPixelFormat GetHWPixelFormat(AVHWDeviceType hWDevice)
+ {
+ switch (hWDevice)
+ {
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_NONE:
+ return AVPixelFormat.AV_PIX_FMT_NONE;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_VDPAU:
+ return AVPixelFormat.AV_PIX_FMT_VDPAU;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_CUDA:
+ return AVPixelFormat.AV_PIX_FMT_CUDA;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_VAAPI:
+ return AVPixelFormat.AV_PIX_FMT_VAAPI;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_DXVA2:
+ return AVPixelFormat.AV_PIX_FMT_NV12;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_QSV:
+ return AVPixelFormat.AV_PIX_FMT_QSV;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
+ return AVPixelFormat.AV_PIX_FMT_VIDEOTOOLBOX;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA:
+ return AVPixelFormat.AV_PIX_FMT_NV12;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_DRM:
+ return AVPixelFormat.AV_PIX_FMT_DRM_PRIME;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_OPENCL:
+ return AVPixelFormat.AV_PIX_FMT_OPENCL;
+ case AVHWDeviceType.AV_HWDEVICE_TYPE_MEDIACODEC:
+ return AVPixelFormat.AV_PIX_FMT_MEDIACODEC;
+ default:
+ return AVPixelFormat.AV_PIX_FMT_NONE;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaFrame.cs b/src/SharpGrabber.Converter/MediaFrame.cs
new file mode 100644
index 0000000..4169c12
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaFrame.cs
@@ -0,0 +1,36 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ ///
+ /// Wrapper for .
+ ///
+ public sealed unsafe class MediaFrame : IDisposable
+ {
+ #region Properties
+ public AVFrame* Pointer { get; private set; }
+ #endregion
+
+ #region Constructor
+ public MediaFrame(AVFrame* frame)
+ {
+ Pointer = frame;
+ }
+ #endregion
+
+ #region Methods
+ public void Dispose()
+ {
+ if (Pointer != null)
+ {
+ var frame = Pointer;
+ ffmpeg.av_frame_unref(frame);
+ Pointer = null;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaHelper.cs b/src/SharpGrabber.Converter/MediaHelper.cs
new file mode 100644
index 0000000..f0fcaa8
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaHelper.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using FFmpeg.AutoGen;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ public static class MediaHelper
+ {
+ public static AVHWDeviceType[] GetHardwareDeviceTypes()
+ {
+ var set = new HashSet();
+ var type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;
+ while ((type = ffmpeg.av_hwdevice_iterate_types(type)) != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
+ set.Add(type);
+ return set.ToArray();
+ }
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaLibrary.cs b/src/SharpGrabber.Converter/MediaLibrary.cs
new file mode 100644
index 0000000..9df1be5
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaLibrary.cs
@@ -0,0 +1,17 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ public static class MediaLibrary
+ {
+ public static void Load(string dir)
+ {
+ ffmpeg.RootPath = dir;
+ }
+
+ public static string FFMpegVersion() => ffmpeg.av_version_info();
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaMuxer.cs b/src/SharpGrabber.Converter/MediaMuxer.cs
new file mode 100644
index 0000000..d8dcc60
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaMuxer.cs
@@ -0,0 +1,90 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ sealed unsafe class MediaMuxer : IDisposable
+ {
+ #region Fields
+ private AVFormatContext* _formatContext;
+ private IOContext _ioContext;
+ #endregion
+
+ #region Properties
+ public AVFormatContext* FormatContextPtr => _formatContext;
+ #endregion
+
+ #region Constructors
+ public MediaMuxer(IOContext output, string shortName, string mimeType)
+ {
+ _ioContext = output;
+ var formatContext = _formatContext;
+ var format = ffmpeg.av_guess_format(shortName, null, mimeType);
+ ffmpeg.avformat_alloc_output_context2(&formatContext, format, null, null).ThrowOnError();
+ _formatContext = formatContext;
+ _formatContext->pb = _ioContext.Pointer;
+ }
+ #endregion
+
+ #region Methods
+ public AVStream* AddStream(AVCodec* codec)
+ {
+ var stream = ffmpeg.avformat_new_stream(_formatContext, codec);
+ if (stream == null)
+ throw new Exception("Could not allocate stream.");
+ if ((_formatContext->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) > 0)
+ stream->codec->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
+ return stream;
+ }
+
+ public void WriteHeader()
+ {
+ ffmpeg.avformat_write_header(_formatContext, null).ThrowOnError();
+ }
+
+ public void WritePacket(AVPacket* packet)
+ {
+ ffmpeg.av_interleaved_write_frame(_formatContext, packet).ThrowOnError();
+ }
+
+ public static MediaPacket EncodeFrame(AVCodecContext* codecContext, MediaFrame mediaFrame)
+ {
+ var frame = mediaFrame.Pointer;
+ var packet = ffmpeg.av_packet_alloc();
+ ffmpeg.av_init_packet(packet);
+
+ ffmpeg.avcodec_send_frame(codecContext, frame).ThrowOnError();
+ ffmpeg.avcodec_receive_packet(codecContext, packet).ThrowOnError();
+
+ return new MediaPacket(packet);
+ }
+
+ public void WriteFrame(AVCodecContext* codecContext, MediaFrame mediaFrame)
+ {
+ using (var packet = EncodeFrame(codecContext, mediaFrame))
+ WritePacket(packet.Pointer);
+ }
+
+ public void WriteTrailer()
+ {
+ ffmpeg.av_write_trailer(_formatContext).ThrowOnError();
+ }
+
+ public void Dispose()
+ {
+ if (_formatContext != null)
+ {
+ ffmpeg.avformat_free_context(_formatContext);
+ _formatContext = null;
+ }
+ if (_ioContext != null)
+ {
+ _ioContext.Dispose();
+ _ioContext = null;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaPacket.cs b/src/SharpGrabber.Converter/MediaPacket.cs
new file mode 100644
index 0000000..3c2da77
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaPacket.cs
@@ -0,0 +1,36 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ ///
+ /// Wrapper for /
+ ///
+ public sealed unsafe class MediaPacket : IDisposable
+ {
+ #region Properties
+ public AVPacket* Pointer { get; private set; }
+ #endregion
+
+ #region Constructor
+ public MediaPacket(AVPacket* packet)
+ {
+ Pointer = packet;
+ }
+ #endregion
+
+ #region Methods
+ public void Dispose()
+ {
+ if (Pointer != null)
+ {
+ var packet = Pointer;
+ ffmpeg.av_packet_unref(packet);
+ Pointer = null;
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/MediaStreamSource.cs b/src/SharpGrabber.Converter/MediaStreamSource.cs
new file mode 100644
index 0000000..9504d2c
--- /dev/null
+++ b/src/SharpGrabber.Converter/MediaStreamSource.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ public enum MediaStreamType { Audio, Video }
+
+ class MediaStreamSource
+ {
+ #region Properties
+ public Uri Path { get; }
+
+ public MediaStreamType StreamType { get; }
+ #endregion
+
+ #region Constructors
+ public MediaStreamSource(Uri path, MediaStreamType streamType)
+ {
+ Path = path;
+ StreamType = streamType;
+ }
+ #endregion
+ }
+}
diff --git a/src/SharpGrabber.Converter/SharpGrabber.Converter.csproj b/src/SharpGrabber.Converter/SharpGrabber.Converter.csproj
new file mode 100644
index 0000000..873dbbb
--- /dev/null
+++ b/src/SharpGrabber.Converter/SharpGrabber.Converter.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.0
+
+
+
+ true
+
+
+
+ true
+
+
+
+
+
+
+
diff --git a/src/SharpGrabber.Converter/VideoFrameConverter.cs b/src/SharpGrabber.Converter/VideoFrameConverter.cs
new file mode 100644
index 0000000..3b729a7
--- /dev/null
+++ b/src/SharpGrabber.Converter/VideoFrameConverter.cs
@@ -0,0 +1,61 @@
+using FFmpeg.AutoGen;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace DotNetTools.SharpGrabber.Converter
+{
+ public sealed unsafe class VideoFrameConverter : IDisposable
+ {
+ private readonly IntPtr _convertedFrameBufferPtr;
+ private readonly Size _destinationSize;
+ private readonly byte_ptrArray4 _dstData;
+ private readonly int_array4 _dstLinesize;
+ private readonly SwsContext* _pConvertContext;
+
+ public VideoFrameConverter(Size sourceSize, AVPixelFormat sourcePixelFormat,
+ Size destinationSize, AVPixelFormat destinationPixelFormat)
+ {
+ _destinationSize = destinationSize;
+
+ _pConvertContext = ffmpeg.sws_getContext(sourceSize.Width, sourceSize.Height, sourcePixelFormat,
+ destinationSize.Width,
+ destinationSize.Height, destinationPixelFormat,
+ ffmpeg.SWS_FAST_BILINEAR, null, null, null);
+ if (_pConvertContext == null) throw new ApplicationException("Could not initialize the conversion context.");
+
+ var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixelFormat, destinationSize.Width, destinationSize.Height, 1);
+ _convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
+ _dstData = new byte_ptrArray4();
+ _dstLinesize = new int_array4();
+
+ ffmpeg.av_image_fill_arrays(ref _dstData, ref _dstLinesize, (byte*)_convertedFrameBufferPtr, destinationPixelFormat, destinationSize.Width, destinationSize.Height, 1);
+ }
+
+ public void Dispose()
+ {
+ Marshal.FreeHGlobal(_convertedFrameBufferPtr);
+ ffmpeg.sws_freeContext(_pConvertContext);
+ }
+
+ public AVFrame Convert(AVFrame sourceFrame)
+ {
+ ffmpeg.sws_scale(_pConvertContext, sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, _dstData, _dstLinesize);
+
+ var data = new byte_ptrArray8();
+ data.UpdateFrom(_dstData);
+ var linesize = new int_array8();
+ linesize.UpdateFrom(_dstLinesize);
+
+ return new AVFrame
+ {
+ data = data,
+ linesize = linesize,
+ width = _destinationSize.Width,
+ height = _destinationSize.Height
+ };
+ }
+ }
+}
diff --git a/src/SharpGrabber.Desktop/App.xaml b/src/SharpGrabber.Desktop/App.xaml
new file mode 100644
index 0000000..4045f1c
--- /dev/null
+++ b/src/SharpGrabber.Desktop/App.xaml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/SharpGrabber.Desktop/App.xaml.cs b/src/SharpGrabber.Desktop/App.xaml.cs
new file mode 100644
index 0000000..6385528
--- /dev/null
+++ b/src/SharpGrabber.Desktop/App.xaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Markup.Xaml;
+
+namespace SharpGrabber.Desktop
+{
+ public class App : Application
+ {
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml b/src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml
new file mode 100644
index 0000000..473a1fc
--- /dev/null
+++ b/src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml.cs b/src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml.cs
new file mode 100644
index 0000000..4aa881a
--- /dev/null
+++ b/src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace SharpGrabber.Desktop.Components
+{
+ public class LoadingSpinner : UserControl
+ {
+ public LoadingSpinner()
+ {
+ this.InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/SharpGrabber.Desktop/Components/MediaResourceView.xaml b/src/SharpGrabber.Desktop/Components/MediaResourceView.xaml
new file mode 100644
index 0000000..ddb1491
--- /dev/null
+++ b/src/SharpGrabber.Desktop/Components/MediaResourceView.xaml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SharpGrabber.Desktop/Components/MediaResourceView.xaml.cs b/src/SharpGrabber.Desktop/Components/MediaResourceView.xaml.cs
new file mode 100644
index 0000000..a687b8e
--- /dev/null
+++ b/src/SharpGrabber.Desktop/Components/MediaResourceView.xaml.cs
@@ -0,0 +1,120 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using DotNetTools.SharpGrabber.Media;
+using SharpGrabber.Desktop;
+using SharpGrabber.Desktop.UI;
+using SharpGrabber.Desktop.ViewModel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace SharpGrabber.Desktop.Components
+{
+ public class MediaResourceView : UserControl
+ {
+ #region Fields
+ private Button btnDownload, btnCopyLink;
+ private DrawingPresenter iconCheck, iconVideo, iconAudio, iconCreate;
+ #endregion
+
+ #region Properties
+ public GrabbedMediaViewModel GrabbedMedia => DataContext as GrabbedMediaViewModel;
+
+ public MainWindow MainWindow => Application.Current.MainWindow as MainWindow;
+ #endregion
+
+ public MediaResourceView(GrabbedMediaViewModel viewModel)
+ {
+ DataContext = viewModel;
+ InitializeComponent();
+
+ var channels = GrabbedMedia.Media.Channels;
+ DrawingPresenter icon;
+
+ if (viewModel.IsComposition)
+ {
+ icon = iconCreate;
+ btnCopyLink.IsVisible = false;
+ ((TextBlock)btnDownload.Content).Text = "Download & convert";
+ }
+ else
+ switch (channels)
+ {
+ case MediaChannels.Both:
+ icon = iconCheck;
+ break;
+
+ case MediaChannels.Audio:
+ icon = iconAudio;
+ break;
+
+ case MediaChannels.Video:
+ icon = iconVideo;
+ break;
+
+ default:
+ throw new NotSupportedException($"Media channel of {channels} is not supported.");
+ }
+ icon.IsVisible = true;
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+
+ btnDownload = this.Find