From 97f2bff5611c4c2809c7c8aa00b0ff6c7ab065ae Mon Sep 17 00:00:00 2001 From: Javid Shoaei Date: Sat, 7 Dec 2019 23:56:26 +0330 Subject: [PATCH 1/7] Add SharpGrabber.Converter and SharpGrabber.Desktop projects --- SharpGrabber.sln | 19 +- src/SharpGrabber.Converter/FFMpegHelper.cs | 27 ++ src/SharpGrabber.Converter/IOContext.cs | 51 +++ src/SharpGrabber.Converter/MediaBuilder.cs | 272 ++++++++++++ src/SharpGrabber.Converter/MediaDecoder.cs | 234 ++++++++++ src/SharpGrabber.Converter/MediaFrame.cs | 36 ++ src/SharpGrabber.Converter/MediaHelper.cs | 20 + src/SharpGrabber.Converter/MediaLibrary.cs | 17 + src/SharpGrabber.Converter/MediaMuxer.cs | 90 ++++ src/SharpGrabber.Converter/MediaPacket.cs | 36 ++ .../MediaStreamSource.cs | 25 ++ .../SharpGrabber.Converter.csproj | 19 + .../VideoFrameConverter.cs | 61 +++ src/SharpGrabber.Desktop/App.xaml | 8 + src/SharpGrabber.Desktop/App.xaml.cs | 13 + .../Components/LoadingSpinner.xaml | 30 ++ .../Components/LoadingSpinner.xaml.cs | 19 + .../Components/MediaResourceView.xaml | 73 ++++ .../Components/MediaResourceView.xaml.cs | 120 +++++ src/SharpGrabber.Desktop/Constants.cs | 15 + src/SharpGrabber.Desktop/ConvertHelper.cs | 64 +++ src/SharpGrabber.Desktop/Downloader.cs | 147 +++++++ src/SharpGrabber.Desktop/IOHelper.cs | 76 ++++ src/SharpGrabber.Desktop/MainWindow.xaml | 248 +++++++++++ src/SharpGrabber.Desktop/MainWindow.xaml.cs | 409 ++++++++++++++++++ src/SharpGrabber.Desktop/Program.cs | 27 ++ .../SharpGrabber.Desktop.csproj | 55 +++ src/SharpGrabber.Desktop/UI/ButtonStyle.xaml | 21 + src/SharpGrabber.Desktop/UI/Icons.xaml | 12 + src/SharpGrabber.Desktop/UI/StringHelpers.cs | 18 + src/SharpGrabber.Desktop/UI/UIHelpers.cs | 33 ++ .../ViewModel/GrabbedMediaViewModel.cs | 97 +++++ 32 files changed, 2389 insertions(+), 3 deletions(-) create mode 100644 src/SharpGrabber.Converter/FFMpegHelper.cs create mode 100644 src/SharpGrabber.Converter/IOContext.cs create mode 100644 src/SharpGrabber.Converter/MediaBuilder.cs create mode 100644 src/SharpGrabber.Converter/MediaDecoder.cs create mode 100644 src/SharpGrabber.Converter/MediaFrame.cs create mode 100644 src/SharpGrabber.Converter/MediaHelper.cs create mode 100644 src/SharpGrabber.Converter/MediaLibrary.cs create mode 100644 src/SharpGrabber.Converter/MediaMuxer.cs create mode 100644 src/SharpGrabber.Converter/MediaPacket.cs create mode 100644 src/SharpGrabber.Converter/MediaStreamSource.cs create mode 100644 src/SharpGrabber.Converter/SharpGrabber.Converter.csproj create mode 100644 src/SharpGrabber.Converter/VideoFrameConverter.cs create mode 100644 src/SharpGrabber.Desktop/App.xaml create mode 100644 src/SharpGrabber.Desktop/App.xaml.cs create mode 100644 src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml create mode 100644 src/SharpGrabber.Desktop/Components/LoadingSpinner.xaml.cs create mode 100644 src/SharpGrabber.Desktop/Components/MediaResourceView.xaml create mode 100644 src/SharpGrabber.Desktop/Components/MediaResourceView.xaml.cs create mode 100644 src/SharpGrabber.Desktop/Constants.cs create mode 100644 src/SharpGrabber.Desktop/ConvertHelper.cs create mode 100644 src/SharpGrabber.Desktop/Downloader.cs create mode 100644 src/SharpGrabber.Desktop/IOHelper.cs create mode 100644 src/SharpGrabber.Desktop/MainWindow.xaml create mode 100644 src/SharpGrabber.Desktop/MainWindow.xaml.cs create mode 100644 src/SharpGrabber.Desktop/Program.cs create mode 100644 src/SharpGrabber.Desktop/SharpGrabber.Desktop.csproj create mode 100644 src/SharpGrabber.Desktop/UI/ButtonStyle.xaml create mode 100644 src/SharpGrabber.Desktop/UI/Icons.xaml create mode 100644 src/SharpGrabber.Desktop/UI/StringHelpers.cs create mode 100644 src/SharpGrabber.Desktop/UI/UIHelpers.cs create mode 100644 src/SharpGrabber.Desktop/ViewModel/GrabbedMediaViewModel.cs 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/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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SharpGrabber.Desktop/MainWindow.xaml.cs b/src/SharpGrabber.Desktop/MainWindow.xaml.cs new file mode 100644 index 0000000..e17b52f --- /dev/null +++ b/src/SharpGrabber.Desktop/MainWindow.xaml.cs @@ -0,0 +1,409 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using DotNetTools.SharpGrabber; +using DotNetTools.SharpGrabber.Converter; +using DotNetTools.SharpGrabber.Media; +using FFmpeg.AutoGen; +using SharpGrabber.Desktop.Components; +using SharpGrabber.Desktop.ViewModel; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace SharpGrabber.Desktop +{ + public class MainWindow : Window + { + #region Fields + private bool _uiEnabled = true; + private TextBox tbUrl; + private TextBlock tbPlaceholder, txtGrab; + private Button btnGrab, btnPaste, btnSaveImages; + private LoadingSpinner spGrab; + private Grid overlayRoot, noContent; + private TextBlock txtMsgTitle, txtMsgContent, txtTitle; + private Button btnMsgOk; + private TextBlock txtMediaTitle; + private TextBlock[] txtCol = new TextBlock[3]; + private Image img; + private LoadingSpinner imgSpinner; + private StackPanel resourceContainer; + private Grid basicInfo; + #endregion + + #region Properties + /// + /// Result of the last grab + /// + public GrabResult CurrentGrab { get; set; } + + public bool IsUIEnabled + { + get => _uiEnabled; + set + { + if (_uiEnabled == value) + return; + _uiEnabled = value; + btnGrab.IsEnabled = btnPaste.IsEnabled = btnSaveImages.IsEnabled = value; + txtGrab.IsVisible = value; + spGrab.IsVisible = !value; + resourceContainer.IsEnabled = value; + } + } + #endregion + + public MainWindow() + { + Initialized += MainWindow_Initialized; + + InitializeComponent(); + basicInfo.IsVisible = resourceContainer.IsVisible = false; +#if DEBUG + this.AttachDevTools(); +#endif + CheckLibrary(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + + tbUrl = this.FindControl("tbUrl"); + tbPlaceholder = this.FindControl("tbPlaceholder"); + btnGrab = this.FindControl - @@ -134,7 +145,8 @@ - + @@ -146,7 +158,8 @@ - + @@ -162,7 +175,8 @@ - + @@ -179,7 +193,7 @@ - + @@ -204,7 +218,8 @@ - + @@ -212,7 +227,8 @@ - + @@ -230,7 +246,8 @@ - + @@ -245,4 +262,4 @@ - + \ No newline at end of file From c012da7c393b717ed4cdf1de1c1afa24caad72b5 Mon Sep 17 00:00:00 2001 From: Javid Shoaei Date: Sun, 8 Dec 2019 00:03:50 +0330 Subject: [PATCH 3/7] Add files via upload --- assets/SharpGrabberDesktop-ScreenShot-1.png | Bin 0 -> 73252 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/SharpGrabberDesktop-ScreenShot-1.png diff --git a/assets/SharpGrabberDesktop-ScreenShot-1.png b/assets/SharpGrabberDesktop-ScreenShot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..61916519ca8ef3cc61da904c9b2423563160443a GIT binary patch literal 73252 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 LiB=ulCF=hGQHL^4 literal 0 HcmV?d00001 From 1f97e07f55d056c2a3fd9e1b023b115cc4ced725 Mon Sep 17 00:00:00 2001 From: Javid Shoaei Date: Sun, 8 Dec 2019 00:38:38 +0330 Subject: [PATCH 4/7] Update readme.md --- README.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6def826..52aa194 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ # SharpGrabber 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 to audio/video files. +- `SharpGrabber.Converter` is a *.NET Core* 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 uses both of the 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 +## 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. + ## Installation Install *SharpGrabber* automatically using NuGet package manager. ### Install via NuGet Install-Package DotNetTools.SharpGrabber -Version 1.0.0 -## Usage Example +## SharpGrabber Usage Example ### Download specifically from a provider @@ -41,10 +49,12 @@ Install *SharpGrabber* automatically using NuGet package manager. ## 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 - Support for more media providers +## SharpGrabber.Desktop +SharpGrabber.Desktop application + ## Support If you want to support this project, the best thing is to star it :) From e8e05fe6fa9d8862268abae2694bc9e2bab2c1ef Mon Sep 17 00:00:00 2001 From: Javid Shoaei Date: Sun, 8 Dec 2019 00:40:57 +0330 Subject: [PATCH 5/7] Update readme.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 52aa194..6bdc036 100644 --- a/README.md +++ b/README.md @@ -46,15 +46,15 @@ Install *SharpGrabber* automatically using NuGet package manager. var result = await grabber.GrabAsync(new Uri("")); IList grabbedResources = result.Resources; +## SharpGrabber.Desktop +SharpGrabber.Desktop application + ## Roadmap This project is very much in progress and the following features are top priority: - Support for Android - Support for more media providers -## SharpGrabber.Desktop -SharpGrabber.Desktop application - ## Support If you want to support this project, the best thing is to star it :) From 7c5b6fd727046ec27345d39e83d3e27a81cb2d1e Mon Sep 17 00:00:00 2001 From: Javid Shoaei Date: Sun, 8 Dec 2019 21:33:12 +0330 Subject: [PATCH 6/7] Update readme.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6bdc036..8667804 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ SharpGrabber 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 to audio/video files. -- `SharpGrabber.Converter` is a *.NET Core* 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` 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 uses both of the mentioned libraries to expose their functionality for desktop end users. +which utilizes both mentioned libraries to expose their functionality for desktop end-users. ### Supported Providers The following providers are currently supported with the option @@ -47,12 +47,18 @@ Install *SharpGrabber* automatically using NuGet package manager. 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. + SharpGrabber.Desktop application ## Roadmap This project is very much in progress and the following features are top priority: - Support for Android +- Accelerate downloads in the desktop app (like a download manager) - Support for more media providers ## Support From 31a2cda09d09ffcfc9aa63fcdfb44a299ee817ca Mon Sep 17 00:00:00 2001 From: Javid Shoaei Date: Sun, 8 Dec 2019 21:35:51 +0330 Subject: [PATCH 7/7] Update readme.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8667804..0d734ad 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ to easily add more or even override part of grabbing algorithm with your own cod - 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. -## Installation -Install *SharpGrabber* automatically using NuGet package manager. +## SharpGrabber Installation +Include *SharpGrabber* library in your own .NET projects. ### Install via NuGet Install-Package DotNetTools.SharpGrabber -Version 1.0.0