diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 6192105a..69460f18 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -55,6 +55,11 @@ jobs:
dotnet-version: |
7.0.x
6.0.x
+
+ - uses: GuillaumeFalourd/setup-windows10-sdk-action@v2
+ name: Setup Windows 10 SDK
+ with:
+ sdk-version: 22621
- name: Restore dependencies
run: dotnet restore MusicX/MusicX.csproj --locked-mode
@@ -87,18 +92,19 @@ jobs:
contents: ${{ steps.changelog.outputs.changes }}
write-mode: overwrite
- - name: Install csq
- run: dotnet tool install --global csq --version 3.0.210-g5f9f594
+ - name: Install vpk
+ run: dotnet tool install -g vpk
- - run: mkdir rel
-
- - run: curl.exe --remove-on-error -sfLO https://github.com/Fooxboy/MusicX-WPF/releases/download/${{ needs.compute-version.outputs.previous-version }}/RELEASES && curl.exe --remove-on-error -sfLO https://github.com/Fooxboy/MusicX-WPF/releases/download/${{ needs.compute-version.outputs.previous-version }}/MusicX.WPF-${{ needs.compute-version.outputs.previous-version }}-full.nupkg
- continue-on-error: true
- working-directory: ./rel
+ - run: vpk download github --repoUrl https://github.com/Fooxboy/MusicX-WPF -c ${{ github.ref == 'refs/heads/develop' && 'win-beta' || 'win' }} --pre --token ${{ secrets.GITHUB_TOKEN }}
name: Download previous release
- name: Build package
- run: csq --csq-version=3.0.210-g5f9f594 pack -f vcredist140 -r rel -p pub -u MusicX.WPF -v ${{ needs.compute-version.outputs.version }} --packTitle "MusicX Player" -e MusicX.exe -i MusicX/StoreLogo.scale-400.ico --appIcon MusicX/StoreLogo.scale-400.ico --msi x64 --includePdb --packAuthors "Fooxboy, zznty" --releaseNotes notes.md -s MusicX/StoreLogo.scale-400.png
+ run: vpk pack -c ${{ github.ref == 'refs/heads/develop' && 'win-beta' || 'win' }} -f vcredist140-x64 -p pub -u MusicX.WPF -v ${{ needs.compute-version.outputs.version }} --packTitle "MusicX Player" -e MusicX.exe -i MusicX/StoreLogo.scale-400.ico --includePdb --skipVeloAppCheck --packAuthors "Fooxboy, zznty" --releaseNotes notes.md -s MusicX/StoreLogo.scale-400.png
- - name: Push package
- run: csq --csq-version=3.0.210-g5f9f594 github-up -r rel --repoUrl https://github.com/Fooxboy/MusicX-WPF --publish --token ${{ secrets.GITHUB_TOKEN }}
+ - name: Push pre-release package
+ if: ${{ github.ref == 'refs/heads/develop' }}
+ run: vpk upload github -c ${{ github.ref == 'refs/heads/develop' && 'win-beta' || 'win' }} --repoUrl https://github.com/Fooxboy/MusicX-WPF --publish --pre --token ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Push release package
+ if: ${{ github.ref != 'refs/heads/develop' }}
+ run: vpk upload github -c ${{ github.ref == 'refs/heads/develop' && 'win-beta' || 'win' }} --repoUrl https://github.com/Fooxboy/MusicX-WPF --publish --token ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 9aadd0f1..00000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "FFMediaToolkit"]
- path = FFMediaToolkit
- url = https://github.com/zznty/FFMediaToolkit.git
diff --git a/FFMediaToolkit b/FFMediaToolkit
deleted file mode 160000
index cbdabebf..00000000
--- a/FFMediaToolkit
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit cbdabebff2e34de4ed33fecc767dea1e49b0d9dc
diff --git a/FFMediaToolkit/Audio/AudioData.cs b/FFMediaToolkit/Audio/AudioData.cs
new file mode 100644
index 00000000..0744daa3
--- /dev/null
+++ b/FFMediaToolkit/Audio/AudioData.cs
@@ -0,0 +1,85 @@
+namespace FFMediaToolkit.Audio
+{
+ using System;
+ using FFMediaToolkit.Common.Internal;
+
+ ///
+ /// Represents a lightweight container for audio data.
+ ///
+ public ref struct AudioData
+ {
+ private readonly AudioFrame frame;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// frame object containing raw audio data.
+ internal AudioData(AudioFrame frame)
+ {
+ this.frame = frame;
+ }
+
+ ///
+ /// Gets the number of samples.
+ ///
+ public int NumSamples => frame.NumSamples;
+
+ ///
+ /// Gets the number of channels.
+ ///
+ public int NumChannels => frame.NumChannels;
+
+ ///
+ /// Fetches raw audio data from this audio frame for specified channel.
+ ///
+ /// The index of audio channel that should be retrieved, allowed range: [0..).
+ /// The span with samples in range of [-1.0, ..., 1.0].
+ public Span GetChannelData(uint channel)
+ where T : unmanaged
+ {
+ return frame.GetChannelData(channel);
+ }
+
+ ///
+ /// Copies raw multichannel audio data from this frame to a heap allocated array.
+ ///
+ ///
+ /// The span with rows and columns;
+ /// samples in range of [-1.0, ..., 1.0].
+ ///
+ public float[][] GetSampleData()
+ {
+ return frame.GetSampleData();
+ }
+
+ ///
+ /// Updates the specified channel of this audio frame with the given sample data.
+ ///
+ /// An array of samples with length .
+ /// The index of audio channel that should be updated, allowed range: [0..).
+ public void UpdateChannelData(float[] samples, uint channel)
+ {
+ frame.UpdateChannelData(samples, channel);
+ }
+
+ ///
+ /// Updates this audio frame with the specified multi-channel sample data.
+ ///
+ ///
+ /// A 2D jagged array of multi-channel sample data
+ /// with rows and columns.
+ ///
+ public void UpdateFromSampleData(float[][] samples)
+ {
+ frame.UpdateFromSampleData(samples);
+ }
+
+ ///
+ /// Releases all unmanaged resources associated with this instance.
+ ///
+ public void Dispose()
+ {
+ frame.Dispose();
+ }
+ }
+}
diff --git a/FFMediaToolkit/Audio/SampleFormat.cs b/FFMediaToolkit/Audio/SampleFormat.cs
new file mode 100644
index 00000000..1c26fa15
--- /dev/null
+++ b/FFMediaToolkit/Audio/SampleFormat.cs
@@ -0,0 +1,60 @@
+namespace FFMediaToolkit.Audio
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Enumerates common audio sample formats supported by FFmpeg.
+ ///
+ public enum SampleFormat
+ {
+ ///
+ /// Unsupported/Unknown.
+ ///
+ None = AVSampleFormat.AV_SAMPLE_FMT_NONE,
+
+ ///
+ /// Unsigned 8-bit integer.
+ ///
+ UnsignedByte = AVSampleFormat.AV_SAMPLE_FMT_U8,
+
+ ///
+ /// Signed 16-bit integer.
+ ///
+ SignedWord = AVSampleFormat.AV_SAMPLE_FMT_S16,
+
+ ///
+ /// Signed 32-bit integer.
+ ///
+ SignedDWord = AVSampleFormat.AV_SAMPLE_FMT_S32,
+
+ ///
+ /// Single precision floating point.
+ ///
+ Single = AVSampleFormat.AV_SAMPLE_FMT_FLT,
+
+ ///
+ /// Double precision floating point.
+ ///
+ Double = AVSampleFormat.AV_SAMPLE_FMT_DBL,
+
+ ///
+ /// Signed 16-bit integer (planar).
+ ///
+ SignedWordP = AVSampleFormat.AV_SAMPLE_FMT_S16P,
+
+ ///
+ /// Signed 32-bit integer (planar).
+ ///
+ SignedDWordP = AVSampleFormat.AV_SAMPLE_FMT_S32P,
+
+ ///
+ /// Single precision floating point (planar).
+ ///
+ SingleP = AVSampleFormat.AV_SAMPLE_FMT_FLTP,
+
+ ///
+ /// Double precision floating point (planar).
+ ///
+ DoubleP = AVSampleFormat.AV_SAMPLE_FMT_DBLP,
+ }
+}
diff --git a/FFMediaToolkit/Common/ContainerMetadata.cs b/FFMediaToolkit/Common/ContainerMetadata.cs
new file mode 100644
index 00000000..54a74ece
--- /dev/null
+++ b/FFMediaToolkit/Common/ContainerMetadata.cs
@@ -0,0 +1,132 @@
+namespace FFMediaToolkit.Common
+{
+ using System.Collections.Generic;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents multimedia file metadata info.
+ ///
+ public class ContainerMetadata
+ {
+ private const string TitleKey = "title";
+ private const string AuthorKey = "author";
+ private const string AlbumKey = "album";
+ private const string YearKey = "year";
+ private const string GenreKey = "genre";
+ private const string DescriptionKey = "description";
+ private const string LanguageKey = "language";
+ private const string CopyrightKey = "copyright";
+ private const string RatingKey = "rating";
+ private const string TrackKey = "track";
+ private const string DateKey = "date";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContainerMetadata() => Metadata = new Dictionary();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source metadata dictionary.
+ internal unsafe ContainerMetadata(AVDictionary* sourceMetadata)
+ => Metadata = FFDictionary.ToDictionary(sourceMetadata, true);
+
+ ///
+ /// Gets or sets the multimedia title.
+ ///
+ public string Title
+ {
+ get => Metadata.ContainsKey(TitleKey) ? Metadata[TitleKey] : string.Empty;
+ set => Metadata[TitleKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia author info.
+ ///
+ public string Author
+ {
+ get => Metadata.ContainsKey(AuthorKey) ? Metadata[AuthorKey] : string.Empty;
+ set => Metadata[AuthorKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia album name.
+ ///
+ public string Album
+ {
+ get => Metadata.ContainsKey(AlbumKey) ? Metadata[AlbumKey] : string.Empty;
+ set => Metadata[AlbumKey] = value;
+ }
+
+ ///
+ /// Gets or sets multimedia release date/year.
+ ///
+ public string Year
+ {
+ get => Metadata.ContainsKey(YearKey)
+ ? Metadata[YearKey]
+ : (Metadata.ContainsKey(DateKey) ? Metadata[DateKey] : string.Empty);
+ set => Metadata[YearKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia genre.
+ ///
+ public string Genre
+ {
+ get => Metadata.ContainsKey(GenreKey) ? Metadata[GenreKey] : string.Empty;
+ set => Metadata[GenreKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia description.
+ ///
+ public string Description
+ {
+ get => Metadata.ContainsKey(DescriptionKey) ? Metadata[DescriptionKey] : string.Empty;
+ set => Metadata[DescriptionKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia language.
+ ///
+ public string Language
+ {
+ get => Metadata.ContainsKey(LanguageKey) ? Metadata[LanguageKey] : string.Empty;
+ set => Metadata[LanguageKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia copyright info.
+ ///
+ public string Copyright
+ {
+ get => Metadata.ContainsKey(CopyrightKey) ? Metadata[CopyrightKey] : string.Empty;
+ set => Metadata[CopyrightKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia rating.
+ ///
+ public string Rating
+ {
+ get => Metadata.ContainsKey(RatingKey) ? Metadata[RatingKey] : string.Empty;
+ set => Metadata[RatingKey] = value;
+ }
+
+ ///
+ /// Gets or sets the multimedia track number string.
+ ///
+ public string TrackNumber
+ {
+ get => Metadata.ContainsKey(TrackKey) ? Metadata[TrackKey] : string.Empty;
+ set => Metadata[TrackKey] = value;
+ }
+
+ ///
+ /// Gets or sets the dictionary containing all metadata fields.
+ ///
+ public Dictionary Metadata { get; set; }
+ }
+}
diff --git a/FFMediaToolkit/Common/FFDictionary.cs b/FFMediaToolkit/Common/FFDictionary.cs
new file mode 100644
index 00000000..a6443ccc
--- /dev/null
+++ b/FFMediaToolkit/Common/FFDictionary.cs
@@ -0,0 +1,126 @@
+namespace FFMediaToolkit.Common
+{
+ using System;
+ using System.Collections.Generic;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a wrapper of . Used for applying codec and container settings.
+ ///
+ internal unsafe class FFDictionary : Wrapper
+ {
+ private bool requireDisposing;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Should the dictionary be disposed automatically?.
+ public FFDictionary(bool dispose = true)
+ : base(null)
+ {
+ requireDisposing = dispose;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dictionary to copy.
+ /// Should the dictionary be disposed automatically?.
+ public FFDictionary(Dictionary dictionary, bool dispose = true)
+ : base(null)
+ {
+ Copy(dictionary);
+ requireDisposing = dispose;
+ }
+
+ ///
+ /// Gets the number of elements in the dictionary.
+ ///
+ public int Count => Pointer == null ? 0 : ffmpeg.av_dict_count(Pointer);
+
+ ///
+ /// Gets or sets the value with the specified key.
+ ///
+ /// The key.
+ /// The value.
+ public string this[string key]
+ {
+ get => Get(key);
+ set => Set(key, value);
+ }
+
+ ///
+ /// Converts a to the managed string dictionary.
+ ///
+ /// The to converter.
+ /// If set the flag will be used.
+ /// The converted .
+ public static Dictionary ToDictionary(AVDictionary* dictionary, bool ignoreSuffix = false)
+ {
+ var result = new Dictionary();
+
+ var item = ffmpeg.av_dict_get(dictionary, string.Empty, null, ignoreSuffix ? ffmpeg.AV_DICT_IGNORE_SUFFIX : 0);
+
+ while (item != null)
+ {
+ result[new IntPtr(item->key).Utf8ToString()] = new IntPtr(item->value).Utf8ToString();
+ item = ffmpeg.av_dict_get(dictionary, string.Empty, item, ignoreSuffix ? ffmpeg.AV_DICT_IGNORE_SUFFIX : 0);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Gets the value with specified key.
+ ///
+ /// The dictionary key.
+ /// If matches case.
+ /// The value with specified key. If the key not exist, returns .
+ public string Get(string key, bool matchCase = true)
+ {
+ var ptr = ffmpeg.av_dict_get(Pointer, key, null, matchCase ? ffmpeg.AV_DICT_MATCH_CASE : 0);
+ return ptr != null ? new IntPtr(ptr).Utf8ToString() : null;
+ }
+
+ ///
+ /// Sets the value for the specified key.
+ ///
+ /// The key.
+ /// The value.
+ public void Set(string key, string value)
+ {
+ var ptr = Pointer;
+ ffmpeg.av_dict_set(&ptr, key, value, 0);
+ UpdatePointer(ptr);
+ }
+
+ ///
+ /// Copies items from specified dictionary to this .
+ ///
+ /// The dictionary to copy.
+ public void Copy(Dictionary dictionary)
+ {
+ foreach (var item in dictionary)
+ {
+ this[item.Key] = item.Value;
+ }
+ }
+
+ ///
+ /// Updates the pointer to the dictionary.
+ ///
+ /// The pointer to the .
+ internal void Update(AVDictionary* pointer) => UpdatePointer(pointer);
+
+ ///
+ protected override void OnDisposing()
+ {
+ if (requireDisposing && Pointer != null && Count > 0)
+ {
+ var ptr = Pointer;
+ ffmpeg.av_dict_free(&ptr);
+ }
+ }
+ }
+}
diff --git a/FFMediaToolkit/Common/Internal/AudioFrame.cs b/FFMediaToolkit/Common/Internal/AudioFrame.cs
new file mode 100644
index 00000000..f6f92f1e
--- /dev/null
+++ b/FFMediaToolkit/Common/Internal/AudioFrame.cs
@@ -0,0 +1,202 @@
+namespace FFMediaToolkit.Common.Internal
+{
+ using System;
+ using FFMediaToolkit.Audio;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represent an audio frame.
+ ///
+ internal unsafe class AudioFrame : MediaFrame
+ {
+ ///
+ /// Initializes a new instance of the class with empty frame data.
+ ///
+ public AudioFrame()
+ : base(ffmpeg.av_frame_alloc())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class using existing .
+ ///
+ /// The audio .
+ public AudioFrame(AVFrame* frame)
+ : base(frame)
+ {
+ if (frame->GetMediaType() != MediaType.Audio)
+ throw new ArgumentException("Cannot create an AudioFrame instance from the AVFrame with type: " + frame->GetMediaType());
+ }
+
+ ///
+ /// Gets the number of samples.
+ ///
+ public int NumSamples => Pointer != null ? Pointer->nb_samples : default;
+
+ ///
+ /// Gets the sample rate.
+ ///
+ public int SampleRate => Pointer != null ? Pointer->sample_rate : default;
+
+ ///
+ /// Gets the number of channels.
+ ///
+ public int NumChannels => Pointer != null ? Pointer->ch_layout.nb_channels : default;
+
+ ///
+ /// Gets the audio sample format.
+ ///
+ public SampleFormat SampleFormat => Pointer != null ? (SampleFormat)Pointer->format : SampleFormat.None;
+
+ ///
+ /// Gets the channel layout.
+ ///
+ internal AVChannelLayout ChannelLayout => Pointer != null ? Pointer->ch_layout : default;
+
+ private bool IsPlanar =>
+ SampleFormat is SampleFormat.SignedWordP or SampleFormat.SingleP or SampleFormat.SignedDWordP;
+
+ ///
+ /// Creates an audio frame with given dimensions and allocates a buffer for it.
+ ///
+ /// The sample rate of the audio frame.
+ /// The number of channels in the audio frame.
+ /// The number of samples in the audio frame.
+ /// The channel layout to be used by the audio frame.
+ /// The audio sample format.
+ /// The timestamp when the frame has to be decoded.
+ /// The timestamp when the frame has to be presented.
+ /// The new audio frame.
+ public static AudioFrame Create(int sample_rate, int num_channels, int num_samples, AVChannelLayout channel_layout, SampleFormat sampleFormat, long decodingTimestamp, long presentationTimestamp)
+ {
+ var frame = ffmpeg.av_frame_alloc();
+
+ frame->sample_rate = sample_rate;
+
+ frame->nb_samples = num_samples;
+ frame->ch_layout = channel_layout;
+ frame->format = (int)sampleFormat;
+
+ frame->pts = presentationTimestamp;
+ frame->pkt_dts = decodingTimestamp;
+
+ ffmpeg.av_frame_get_buffer(frame, 32);
+
+ return new AudioFrame(frame);
+ }
+
+ ///
+ /// Creates an empty frame for decoding.
+ ///
+ /// The empty .
+ public static AudioFrame CreateEmpty() => new AudioFrame();
+
+ ///
+ /// Fetches raw audio data from this audio frame for specified channel.
+ ///
+ /// The index of audio channel that should be retrieved, allowed range: [0..).
+ /// The span with samples in range of [-1.0, ..., 1.0].
+ public Span GetChannelData(uint channel)
+ where T : unmanaged
+ {
+ /*if (SampleFormat != SampleFormat.SingleP)
+ throw new Exception("Cannot extract channel data from an AudioFrame with a SampleFormat not equal to SampleFormat.SingleP");*/
+ return IsPlanar ? new Span(Pointer->data[channel], NumSamples) : new Span(Pointer->data[channel], NumSamples * NumChannels);
+ }
+
+ ///
+ /// Copies raw multichannel audio data from this frame to a heap allocated array.
+ ///
+ ///
+ /// The span with rows and columns;
+ /// samples in range of [-1.0, ..., 1.0].
+ ///
+ public float[][] GetSampleData()
+ {
+ if (SampleFormat != SampleFormat.SingleP)
+ throw new Exception("Cannot extract sample data from an AudioFrame with a SampleFormat not equal to SampleFormat.SingleP");
+
+ var samples = new float[NumChannels][];
+
+ for (uint ch = 0; ch < NumChannels; ch++)
+ {
+ samples[ch] = new float[NumSamples];
+
+ var channelData = GetChannelData(ch);
+ var sampleData = new Span(samples[ch], 0, NumSamples);
+
+ channelData.CopyTo(sampleData);
+ }
+
+ return samples;
+ }
+
+ ///
+ /// Updates the specified channel of this audio frame with the given sample data.
+ ///
+ /// An array of samples with length .
+ /// The index of audio channel that should be updated, allowed range: [0..).
+ public void UpdateChannelData(float[] samples, uint channel)
+ {
+ if (SampleFormat != SampleFormat.SingleP)
+ throw new Exception("Cannot update channel data of an AudioFrame with a SampleFormat not equal to SampleFormat.SingleP");
+
+ var frameData = GetChannelData(channel);
+ var sampleData = new Span(samples, 0, NumSamples);
+
+ sampleData.CopyTo(frameData);
+ }
+
+ ///
+ /// Updates this audio frame with the specified multi-channel sample data.
+ ///
+ ///
+ /// A 2D jagged array of multi-channel sample data
+ /// with rows and columns.
+ ///
+ public void UpdateFromSampleData(float[][] samples)
+ {
+ if (SampleFormat != SampleFormat.SingleP)
+ throw new Exception("Cannot update sample data of an AudioFrame with a SampleFormat not equal to SampleFormat.SingleP");
+
+ for (uint ch = 0; ch < NumChannels; ch++)
+ {
+ var newData = new Span(samples[ch], 0, NumSamples);
+ var frameData = GetChannelData(ch);
+ newData.CopyTo(frameData);
+ }
+ }
+
+ ///
+ /// Updates this audio frame with the specified audio data.
+ /// ( and
+ /// should match the respective values for this instance!).
+ ///
+ /// The audio data.
+ public void UpdateFromAudioData(AudioData audioData)
+ {
+ if (SampleFormat != SampleFormat.SingleP)
+ throw new Exception("Cannot update data of an AudioFrame with a SampleFormat not equal to SampleFormat.SingleP");
+
+ for (uint ch = 0; ch < NumChannels; ch++)
+ {
+ var newData = audioData.GetChannelData(ch);
+ var currData = GetChannelData(ch);
+
+ newData.CopyTo(currData);
+ }
+ }
+
+ ///
+ internal override unsafe void Update(AVFrame* newFrame)
+ {
+ if (newFrame->GetMediaType() != MediaType.Audio)
+ {
+ throw new ArgumentException("The new frame doesn't contain audio data.");
+ }
+
+ base.Update(newFrame);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Common/Internal/ImageConverter.cs b/FFMediaToolkit/Common/Internal/ImageConverter.cs
new file mode 100644
index 00000000..64607d94
--- /dev/null
+++ b/FFMediaToolkit/Common/Internal/ImageConverter.cs
@@ -0,0 +1,104 @@
+namespace FFMediaToolkit.Common.Internal
+{
+ using System;
+ using System.Drawing;
+ using FFMediaToolkit.Graphics;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// A class used to convert ffmpeg s to objects with specified image size and color format.
+ ///
+ internal unsafe class ImageConverter : Wrapper
+ {
+ // sws_scale requires up to 16 extra bytes allocated in the input buffer when resizing an image
+ // (reference: https://www.ffmpeg.org/doxygen/6.0/frame_8h_source.html#l00340)
+ private const int BufferPaddingSize = 16;
+ private byte[] tmpBuffer = { };
+
+ private readonly Size destinationSize;
+ private readonly AVPixelFormat destinationFormat;
+ private Size lastSourceSize;
+ private AVPixelFormat lastSourcePixelFormat;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Destination image size.
+ /// Destination image format.
+ public ImageConverter(Size destinationSize, AVPixelFormat destinationFormat)
+ : base(null)
+ {
+ this.destinationSize = destinationSize;
+ this.destinationFormat = destinationFormat;
+ }
+
+ ///
+ /// Overrides the image buffer with the converted bitmap. Used in encoding.
+ ///
+ /// The input bitmap.
+ /// The to override.
+ internal void FillAVFrame(ImageData bitmap, VideoFrame destinationFrame)
+ {
+ UpdateContext(bitmap.ImageSize, (AVPixelFormat)bitmap.PixelFormat);
+
+ var requiredBufferLength = (bitmap.Stride * bitmap.ImageSize.Height) + BufferPaddingSize;
+ var shouldUseTmpBuffer = bitmap.ImageSize != destinationSize && bitmap.Data.Length < requiredBufferLength;
+
+ if (shouldUseTmpBuffer)
+ {
+ if (tmpBuffer.Length < requiredBufferLength)
+ {
+ tmpBuffer = new byte[requiredBufferLength];
+ }
+
+ bitmap.Data.CopyTo(tmpBuffer);
+ }
+
+ var source = shouldUseTmpBuffer ? tmpBuffer : bitmap.Data;
+ fixed (byte* ptr = source)
+ {
+ var data = new byte*[4] { ptr, null, null, null };
+ var linesize = new int[4] { bitmap.Stride, 0, 0, 0 };
+ ffmpeg.sws_scale(Pointer, data, linesize, 0, bitmap.ImageSize.Height, destinationFrame.Pointer->data, destinationFrame.Pointer->linesize);
+ }
+ }
+
+ ///
+ /// Converts a video to the specified bitmap. Used in decoding.
+ ///
+ /// The video frame to convert.
+ /// The destination .
+ /// Size of the single bitmap row.
+ internal void AVFrameToBitmap(VideoFrame videoFrame, byte* destination, int stride)
+ {
+ UpdateContext(videoFrame.Layout, videoFrame.PixelFormat);
+
+ var data = new byte*[4] { destination, null, null, null };
+ var linesize = new int[4] { stride, 0, 0, 0 };
+ ffmpeg.sws_scale(Pointer, videoFrame.Pointer->data, videoFrame.Pointer->linesize, 0, videoFrame.Layout.Height, data, linesize);
+ }
+
+ ///
+ protected override void OnDisposing() => ffmpeg.sws_freeContext(Pointer);
+
+ private void UpdateContext(Size sourceSize, AVPixelFormat sourceFormat)
+ {
+ if (sourceSize != lastSourceSize || sourceFormat != lastSourcePixelFormat)
+ {
+ ffmpeg.sws_freeContext(Pointer);
+
+ var scaleMode = sourceSize == destinationSize ? ffmpeg.SWS_POINT : ffmpeg.SWS_BICUBIC;
+ var swsContext = ffmpeg.sws_getContext(sourceSize.Width, sourceSize.Height, sourceFormat, destinationSize.Width, destinationSize.Height, destinationFormat, scaleMode, null, null, null);
+
+ if (swsContext == null)
+ {
+ throw new FFmpegException("Cannot allocate SwsContext.");
+ }
+
+ UpdatePointer(swsContext);
+ lastSourceSize = sourceSize;
+ lastSourcePixelFormat = sourceFormat;
+ }
+ }
+ }
+}
diff --git a/FFMediaToolkit/Common/Internal/MediaFrame.cs b/FFMediaToolkit/Common/Internal/MediaFrame.cs
new file mode 100644
index 00000000..b2e4d426
--- /dev/null
+++ b/FFMediaToolkit/Common/Internal/MediaFrame.cs
@@ -0,0 +1,50 @@
+namespace FFMediaToolkit.Common.Internal
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a base class of audio and video frames.
+ ///
+ internal abstract unsafe class MediaFrame : Wrapper
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object.
+ public MediaFrame(AVFrame* frame)
+ : base(frame)
+ {
+ }
+
+ ///
+ /// Gets or sets the frame PTS value in the stream time base units.
+ ///
+ public long PresentationTimestamp
+ {
+ get => Pointer->pts;
+ set => Pointer->pts = value;
+ }
+
+ ///
+ /// Gets or sets the frame PTS value in the stream time base units.
+ ///
+ public long DecodingTimestamp
+ {
+ get => Pointer->pkt_dts;
+ set => Pointer->pkt_dts = value;
+ }
+
+ ///
+ /// Changes the pointer to the media frame.
+ ///
+ /// The new pointer to a object.
+ internal virtual void Update(AVFrame* newFrame) => UpdatePointer(newFrame);
+
+ ///
+ protected override void OnDisposing()
+ {
+ var ptr = Pointer;
+ ffmpeg.av_frame_free(&ptr);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Common/Internal/MediaPacket.cs b/FFMediaToolkit/Common/Internal/MediaPacket.cs
new file mode 100644
index 00000000..04b94eb8
--- /dev/null
+++ b/FFMediaToolkit/Common/Internal/MediaPacket.cs
@@ -0,0 +1,92 @@
+namespace FFMediaToolkit.Common.Internal
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a FFMpeg media packet.
+ ///
+ internal unsafe sealed class MediaPacket : Wrapper
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object.
+ private MediaPacket(AVPacket* packet)
+ : base(packet)
+ {
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this packet is a key packet.
+ ///
+ public bool IsKeyPacket
+ {
+ get => (Pointer->flags & ffmpeg.AV_PKT_FLAG_KEY) > 0;
+ set => Pointer->flags |= value ? ffmpeg.AV_PKT_FLAG_KEY : ~ffmpeg.AV_PKT_FLAG_KEY;
+ }
+
+ ///
+ /// Gets or sets the stream index.
+ ///
+ public int StreamIndex
+ {
+ get => Pointer->stream_index;
+ set => Pointer->stream_index = value;
+ }
+
+ ///
+ /// Gets the presentation time stamp of the packet. if is AV_NOPTS_VALUE.
+ ///
+ public long? Timestamp => Pointer->pts != ffmpeg.AV_NOPTS_VALUE ? Pointer->pts : (long?)null;
+
+ ///
+ /// Converts an instance of to the unmanaged pointer.
+ ///
+ /// A instance.
+ public static implicit operator AVPacket*(MediaPacket packet) => packet.Pointer;
+
+ ///
+ /// Allocates a new empty packet.
+ ///
+ /// The new .
+ public static MediaPacket AllocateEmpty()
+ {
+ var packet = ffmpeg.av_packet_alloc();
+ packet->stream_index = -1;
+ return new MediaPacket(packet);
+ }
+
+ ///
+ /// Creates a flush packet.
+ ///
+ /// The stream index.
+ /// The flush packet.
+ public static MediaPacket CreateFlushPacket(int streamIndex)
+ {
+ var packet = ffmpeg.av_packet_alloc();
+ packet->stream_index = streamIndex;
+ packet->data = null;
+ packet->size = 0;
+ return new MediaPacket(packet);
+ }
+
+ ///
+ /// Sets valid PTS/DTS values. Used only in encoding.
+ ///
+ /// The encoder time base.
+ /// The time base of media stream.
+ public void RescaleTimestamp(AVRational codecTimeBase, AVRational streamTimeBase) => ffmpeg.av_packet_rescale_ts(Pointer, codecTimeBase, streamTimeBase);
+
+ ///
+ /// Wipes the packet data.
+ ///
+ public void Wipe() => ffmpeg.av_packet_unref(Pointer);
+
+ ///
+ protected override void OnDisposing()
+ {
+ var ptr = Pointer;
+ ffmpeg.av_packet_free(&ptr);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Common/Internal/VideoFrame.cs b/FFMediaToolkit/Common/Internal/VideoFrame.cs
new file mode 100644
index 00000000..92eb1bf8
--- /dev/null
+++ b/FFMediaToolkit/Common/Internal/VideoFrame.cs
@@ -0,0 +1,105 @@
+namespace FFMediaToolkit.Common.Internal
+{
+ using System;
+ using System.Drawing;
+ using FFMediaToolkit.Graphics;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represent a video frame.
+ ///
+ internal unsafe class VideoFrame : MediaFrame
+ {
+ ///
+ /// Initializes a new instance of the class with empty frame data.
+ ///
+ public VideoFrame()
+ : base(ffmpeg.av_frame_alloc())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class using existing .
+ ///
+ /// The video .
+ public VideoFrame(AVFrame* frame)
+ : base(frame)
+ {
+ if (frame->GetMediaType() == MediaType.Audio)
+ throw new ArgumentException("Cannot create a VideoFrame instance from the AVFrame containing audio.");
+ }
+
+ ///
+ /// Gets the frame dimensions.
+ ///
+ public Size Layout => Pointer != null ? new Size(Pointer->width, Pointer->height) : default;
+
+ ///
+ /// Gets the frame pixel format.
+ ///
+ public AVPixelFormat PixelFormat => Pointer != null ? (AVPixelFormat)Pointer->format : default;
+
+ ///
+ /// Creates a video frame with given dimensions and allocates a buffer for it.
+ ///
+ /// The dimensions of the video frame.
+ /// The video pixel format.
+ /// The new video frame.
+ public static VideoFrame Create(Size dimensions, AVPixelFormat pixelFormat)
+ {
+ var frame = ffmpeg.av_frame_alloc();
+
+ frame->width = dimensions.Width;
+ frame->height = dimensions.Height;
+ frame->format = (int)pixelFormat;
+
+ ffmpeg.av_frame_get_buffer(frame, 32);
+
+ return new VideoFrame(frame);
+ }
+
+ ///
+ /// Creates an empty frame for decoding.
+ ///
+ /// The empty .
+ public static VideoFrame CreateEmpty() => new VideoFrame();
+
+ ///
+ /// Overrides this video frame data with the converted using specified object.
+ ///
+ /// The bitmap to convert.
+ /// A object, used for caching the FFMpeg when converting many frames of the same video.
+ public void UpdateFromBitmap(ImageData bitmap, ImageConverter converter) => converter.FillAVFrame(bitmap, this);
+
+ ///
+ /// Converts this video frame to the with the specified pixel format.
+ ///
+ /// A object, used for caching the FFMpeg when converting many frames of the same video.
+ /// The output bitmap pixel format.
+ /// /// The output bitmap size.
+ /// A instance containing converted bitmap data.
+ public ImageData ToBitmap(ImageConverter converter, ImagePixelFormat targetFormat, Size targetSize)
+ {
+ var bitmap = ImageData.CreatePooled(targetSize, targetFormat); // Rents memory for the output bitmap.
+ fixed (byte* ptr = bitmap.Data)
+ {
+ // Converts the raw video frame using the given size and pixel format and writes it to the ImageData bitmap.
+ converter.AVFrameToBitmap(this, ptr, bitmap.Stride);
+ }
+
+ return bitmap;
+ }
+
+ ///
+ internal override unsafe void Update(AVFrame* newFrame)
+ {
+ if (newFrame->GetMediaType() != MediaType.Video)
+ {
+ throw new ArgumentException("The new frame doesn't contain video data.");
+ }
+
+ base.Update(newFrame);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Common/MediaType.cs b/FFMediaToolkit/Common/MediaType.cs
new file mode 100644
index 00000000..068b295d
--- /dev/null
+++ b/FFMediaToolkit/Common/MediaType.cs
@@ -0,0 +1,23 @@
+namespace FFMediaToolkit.Common
+{
+ ///
+ /// Represents the multimedia stream types.
+ ///
+ public enum MediaType
+ {
+ ///
+ /// Other media type not supported by the FFMediaToolkit.
+ ///
+ None,
+
+ ///
+ /// Video.
+ ///
+ Video,
+
+ ///
+ /// Audio.
+ ///
+ Audio,
+ }
+}
diff --git a/FFMediaToolkit/Common/Wrapper{T}.cs b/FFMediaToolkit/Common/Wrapper{T}.cs
new file mode 100644
index 00000000..ecf44a1c
--- /dev/null
+++ b/FFMediaToolkit/Common/Wrapper{T}.cs
@@ -0,0 +1,58 @@
+namespace FFMediaToolkit.Common
+{
+ using System;
+
+ ///
+ /// A base class for wrappers of unmanaged objects with implementation.
+ ///
+ /// The type of the unmanaged object.
+ internal abstract unsafe class Wrapper : IDisposable
+ where T : unmanaged
+ {
+ private IntPtr pointer;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A pointer to a unmanaged object.
+ protected Wrapper(T* pointer) => this.pointer = new IntPtr(pointer);
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~Wrapper() => Disposing(false);
+
+ ///
+ /// Gets a pointer to the underlying object.
+ ///
+ public T* Pointer => isDisposed ? null : (T*)pointer;
+
+ ///
+ public void Dispose() => Disposing(true);
+
+ ///
+ /// Updates the pointer to the object.
+ ///
+ /// The new pointer.
+ protected void UpdatePointer(T* newPointer) => pointer = new IntPtr(newPointer);
+
+ ///
+ /// Free the unmanaged resources.
+ ///
+ protected abstract void OnDisposing();
+
+ private void Disposing(bool dispose)
+ {
+ if (isDisposed)
+ return;
+
+ OnDisposing();
+
+ isDisposed = true;
+
+ if (dispose)
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/AudioStream.cs b/FFMediaToolkit/Decoding/AudioStream.cs
new file mode 100644
index 00000000..6e028bee
--- /dev/null
+++ b/FFMediaToolkit/Decoding/AudioStream.cs
@@ -0,0 +1,155 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.IO;
+ using FFMediaToolkit.Audio;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Decoding.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents an audio stream in the .
+ ///
+ public unsafe class AudioStream : MediaStream
+ {
+ private readonly SampleFormat targetSampleFormat;
+ private SwrContext* swrContext;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The audio stream.
+ /// The decoder settings.
+ /// The output sample format.
+ internal AudioStream(Decoder stream, MediaOptions options, SampleFormat sampleFormat = SampleFormat.SingleP)
+ : base(stream, options)
+ {
+ targetSampleFormat = sampleFormat;
+ var layout = Info.ChannelLayout;
+ SwrContext* context;
+ ffmpeg.swr_alloc_set_opts2(
+ &context,
+ &layout,
+ (AVSampleFormat)sampleFormat,
+ Info.SampleRate,
+ &layout,
+ (AVSampleFormat)Info.SampleFormat,
+ Info.SampleRate,
+ 0,
+ null).ThrowIfError("Cannot allocate SwrContext");
+ ffmpeg.swr_init(context);
+ swrContext = context;
+ }
+
+ ///
+ /// Gets informations about this stream.
+ ///
+ public new AudioStreamInfo Info => base.Info as AudioStreamInfo;
+
+ ///
+ /// Reads the next frame from the audio stream.
+ ///
+ /// The decoded audio data.
+ public new AudioData GetNextFrame()
+ {
+ var frame = base.GetNextFrame() as AudioFrame;
+
+ var converted = AudioFrame.Create(
+ frame.SampleRate,
+ frame.NumChannels,
+ frame.NumSamples,
+ frame.ChannelLayout,
+ targetSampleFormat,
+ frame.DecodingTimestamp,
+ frame.PresentationTimestamp);
+
+ ffmpeg.swr_convert_frame(swrContext, converted.Pointer, frame.Pointer).ThrowIfError("Cannot resample frame");
+
+ return new AudioData(converted);
+ }
+
+ ///
+ /// Reads the next frame from the audio stream.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// The decoded audio data.
+ /// if reached end of the stream.
+ public bool TryGetNextFrame(out AudioData data)
+ {
+ try
+ {
+ data = GetNextFrame();
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ data = default;
+ return false;
+ }
+ }
+
+ ///
+ /// Reads the video frame found at the specified timestamp.
+ ///
+ /// The frame timestamp.
+ /// The decoded video frame.
+ public new AudioData GetFrame(TimeSpan time)
+ {
+ var frame = base.GetFrame(time) as AudioFrame;
+
+ var converted = AudioFrame.Create(
+ frame.SampleRate,
+ frame.NumChannels,
+ frame.NumSamples,
+ frame.ChannelLayout,
+ targetSampleFormat,
+ frame.DecodingTimestamp,
+ frame.PresentationTimestamp);
+
+ ffmpeg.swr_convert_frame(swrContext, converted.Pointer, frame.Pointer).ThrowIfError("Cannot resample frame");
+
+ return new AudioData(converted);
+ }
+
+ ///
+ /// Reads the audio data found at the specified timestamp.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// The frame timestamp.
+ /// The decoded audio data.
+ /// if reached end of the stream.
+ public bool TryGetFrame(TimeSpan time, out AudioData data)
+ {
+ try
+ {
+ data = GetFrame(time);
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ data = default;
+ return false;
+ }
+ }
+
+ ///
+ public override void Dispose()
+ {
+ if (!isDisposed)
+ {
+ fixed (SwrContext** ptr = &swrContext)
+ {
+ ffmpeg.swr_free(ptr);
+ }
+
+ isDisposed = true;
+ }
+
+ base.Dispose();
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/AudioStreamInfo.cs b/FFMediaToolkit/Decoding/AudioStreamInfo.cs
new file mode 100644
index 00000000..6b2e4421
--- /dev/null
+++ b/FFMediaToolkit/Decoding/AudioStreamInfo.cs
@@ -0,0 +1,58 @@
+namespace FFMediaToolkit.Decoding
+{
+ using FFMediaToolkit.Audio;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Decoding.Internal;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents informations about the audio stream.
+ ///
+ public class AudioStreamInfo : StreamInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A generic stream.
+ /// The input container.
+ internal unsafe AudioStreamInfo(AVStream* stream, InputContainer container)
+ : base(stream, MediaType.Audio, container)
+ {
+ var codec = stream->codecpar;
+ NumChannels = codec->ch_layout.nb_channels;
+ SampleRate = codec->sample_rate;
+ SamplesPerFrame = codec->frame_size > 0 ? codec->frame_size : codec->sample_rate / 20;
+ SampleFormat = (SampleFormat)codec->format;
+
+ AVChannelLayout layout;
+ ffmpeg.av_channel_layout_default(&layout, codec->ch_layout.nb_channels);
+ ChannelLayout = layout;
+ }
+
+ ///
+ /// Gets the number of audio channels stored in the stream.
+ ///
+ public int NumChannels { get; }
+
+ ///
+ /// Gets the number of samples per second of the audio stream.
+ ///
+ public int SampleRate { get; }
+
+ ///
+ /// Gets the average number of samples per frame (chunk of samples) calculated from metadata.
+ /// It is used to calculate timestamps in the internal decoder methods.
+ ///
+ public int SamplesPerFrame { get; }
+
+ ///
+ /// Gets the audio sample format.
+ ///
+ public SampleFormat SampleFormat { get; }
+
+ ///
+ /// Gets the channel layout for this stream.
+ ///
+ internal AVChannelLayout ChannelLayout { get; }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/ContainerOptions.cs b/FFMediaToolkit/Decoding/ContainerOptions.cs
new file mode 100644
index 00000000..fd20b5e8
--- /dev/null
+++ b/FFMediaToolkit/Decoding/ContainerOptions.cs
@@ -0,0 +1,129 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System.Collections.Generic;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a set of demuxer options and flags.
+ /// See https://ffmpeg.org/ffmpeg-formats.html#Format-Options.
+ ///
+ public class ContainerOptions
+ {
+ ///
+ /// Initializes a new instance of the class with the default settings.
+ ///
+ public ContainerOptions()
+ {
+ }
+
+ ///
+ /// Discard corrupted packets.
+ /// Port of 'discardcorrupt'.
+ ///
+ public bool FlagDiscardCorrupt { get; set; }
+
+ ///
+ /// Enable fast, but inaccurate seeks for some formats.
+ /// Port of 'fastseek'.
+ ///
+ public bool FlagEnableFastSeek { get; set; }
+
+ ///
+ /// Do not fill in missing values that can be exactly calculated.
+ /// Port of 'nofillin'.
+ ///
+ public bool FlagEnableNoFillIn { get; set; }
+
+ ///
+ /// Generate missing PTS if DTS is present.
+ /// Port of 'genpts'.
+ ///
+ public bool FlagGeneratePts { get; set; }
+
+ ///
+ /// Ignore DTS if PTS is set.
+ /// Port of 'igndts'.
+ ///
+ public bool FlagIgnoreDts { get; set; }
+
+ ///
+ /// Ignore index.
+ /// Port of 'ignidx'.
+ ///
+ public bool FlagIgnoreIndex { get; set; }
+
+ ///
+ /// Reduce the latency introduced by optional buffering.
+ /// Port of 'nobuffer'.
+ ///
+ public bool FlagNoBuffer { get; set; }
+
+ ///
+ /// Try to interleave output packets by DTS.
+ /// Port of 'sortdts'.
+ ///
+ public bool FlagSortDts { get; set; }
+
+ ///
+ /// Allow seeking to non-keyframes on demuxer level when supported.
+ /// Port of seek2any.
+ ///
+ public bool SeekToAny { get; set; }
+
+ ///
+ /// Gets or sets the private demuxer-specific options.
+ ///
+ public Dictionary PrivateOptions { get; set; } = new Dictionary();
+
+ ///
+ /// Applies flag settings specified in this class to an instance of .
+ ///
+ /// An empty instance of before opening the stream.
+ internal unsafe void ApplyFlags(AVFormatContext* formatContext)
+ {
+ ref var formatFlags = ref formatContext->flags;
+
+ if (FlagDiscardCorrupt)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_DISCARD_CORRUPT;
+ }
+
+ if (FlagEnableFastSeek)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_FAST_SEEK;
+ }
+
+ if (FlagEnableNoFillIn)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_NOFILLIN;
+ }
+
+ if (FlagGeneratePts)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_GENPTS;
+ }
+
+ if (FlagIgnoreDts)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_IGNDTS;
+ }
+
+ if (FlagIgnoreIndex)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_IGNIDX;
+ }
+
+ if (FlagNoBuffer)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_NOBUFFER;
+ }
+
+ if (FlagSortDts)
+ {
+ formatFlags |= ffmpeg.AVFMT_FLAG_SORT_DTS;
+ }
+
+ formatContext->seek2any = SeekToAny ? 1 : 0;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/Internal/AvioStream.cs b/FFMediaToolkit/Decoding/Internal/AvioStream.cs
new file mode 100644
index 00000000..f0baea6b
--- /dev/null
+++ b/FFMediaToolkit/Decoding/Internal/AvioStream.cs
@@ -0,0 +1,79 @@
+namespace FFMediaToolkit.Decoding.Internal
+{
+ using System;
+ using System.IO;
+ using System.Runtime.InteropServices;
+
+ using FFmpeg.AutoGen;
+
+ ///
+ /// A stream wrapper.
+ ///
+ internal unsafe class AvioStream : IDisposable
+ {
+ private readonly Stream inputStream;
+
+ private byte[] readBuffer = null;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Multimedia file stream.
+ public AvioStream(Stream input)
+ {
+ inputStream = input ?? throw new ArgumentNullException(nameof(input));
+ }
+
+ ///
+ /// A method for refilling the buffer. For stream protocols,
+ /// must never return 0 but rather a proper AVERROR code.
+ ///
+ /// An opaque pointer.
+ /// A buffer that needs to be filled with stream data.
+ /// The size of .
+ /// Number of read bytes.
+ public int Read(void* opaque, byte* buffer, int bufferLength)
+ {
+ if (readBuffer == null)
+ {
+ readBuffer = new byte[bufferLength];
+ }
+
+ int readed = inputStream.Read(readBuffer, 0, readBuffer.Length);
+
+ if (readed < 1)
+ {
+ return ffmpeg.AVERROR_EOF;
+ }
+
+ Marshal.Copy(readBuffer, 0, (IntPtr)buffer, readed);
+
+ return readed;
+ }
+
+ ///
+ /// A method for seeking to specified byte position.
+ ///
+ /// An opaque pointer.
+ /// The offset in a stream.
+ /// The seek option.
+ /// Position within the current stream or stream size.
+ public long Seek(void* opaque, long offset, int whence)
+ {
+ if (!inputStream.CanSeek)
+ {
+ return -1;
+ }
+
+ return whence == ffmpeg.AVSEEK_SIZE ?
+ inputStream.Length :
+ inputStream.Seek(offset, SeekOrigin.Begin);
+ }
+
+ ///
+ public void Dispose()
+ {
+ inputStream?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/Internal/Decoder.cs b/FFMediaToolkit/Decoding/Internal/Decoder.cs
new file mode 100644
index 00000000..3848bf83
--- /dev/null
+++ b/FFMediaToolkit/Decoding/Internal/Decoder.cs
@@ -0,0 +1,189 @@
+namespace FFMediaToolkit.Decoding.Internal
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a input multimedia stream.
+ ///
+ internal unsafe class Decoder : Wrapper
+ {
+ private readonly int bufferLimit;
+ private int bufferSize = 0;
+ private bool reuseLastPacket;
+ private bool flushing = false;
+ private MediaPacket packet;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying codec.
+ /// The multimedia stream.
+ /// The container that owns the stream.
+ public Decoder(AVCodecContext* codec, AVStream* stream, InputContainer owner)
+ : base(codec)
+ {
+ bufferLimit = owner.MaxBufferSize * 1024 * 1024; // convert megabytes to bytes
+ OwnerFile = owner;
+ Info = StreamInfo.Create(stream, owner);
+ switch (Info.Type)
+ {
+ case MediaType.Audio:
+ RecentlyDecodedFrame = new AudioFrame();
+ break;
+ case MediaType.Video:
+ RecentlyDecodedFrame = new VideoFrame();
+ break;
+ default:
+ throw new Exception("Tried to create a decoder from an unsupported stream or codec type.");
+ }
+
+ BufferedPackets = new Queue();
+ }
+
+ ///
+ /// Gets informations about the stream.
+ ///
+ public StreamInfo Info { get; }
+
+ ///
+ /// Gets the media container that owns this stream.
+ ///
+ public InputContainer OwnerFile { get; }
+
+ ///
+ /// Gets the recently decoded frame.
+ ///
+ public MediaFrame RecentlyDecodedFrame { get; }
+
+ ///
+ /// Indicates whether the codec has buffered packets.
+ ///
+ public bool IsBufferEmpty => BufferedPackets.Count == 0;
+
+ ///
+ /// Gets a FIFO collection of media packets that the codec has buffered.
+ ///
+ private Queue BufferedPackets { get; }
+
+ ///
+ /// Adds the specified packet to the codec buffer.
+ ///
+ /// The packet to be buffered.
+ public void BufferPacket(MediaPacket packet)
+ {
+ BufferedPackets.Enqueue(packet);
+ bufferSize += packet.Pointer->size;
+
+ if (bufferSize > bufferLimit)
+ {
+ var deletedPacket = BufferedPackets.Dequeue();
+ bufferSize -= deletedPacket.Pointer->size;
+ deletedPacket.Dispose();
+ }
+ }
+
+ ///
+ /// Reads the next frame from the stream.
+ ///
+ /// The decoded frame.
+ public MediaFrame GetNextFrame()
+ {
+ ReadNextFrame();
+ return RecentlyDecodedFrame;
+ }
+
+ ///
+ /// Decodes frames until reach the specified time stamp. Useful to seek few frames forward.
+ ///
+ /// The target time stamp.
+ public void SkipFrames(long targetTs)
+ {
+ do
+ {
+ ReadNextFrame();
+ }
+ while (RecentlyDecodedFrame.PresentationTimestamp < targetTs);
+ }
+
+ ///
+ /// Discards all packet data buffered by this instance.
+ ///
+ public void DiscardBufferedData()
+ {
+ ffmpeg.avcodec_flush_buffers(Pointer);
+
+ foreach (var packet in BufferedPackets)
+ {
+ packet.Wipe();
+ packet.Dispose();
+ }
+
+ BufferedPackets.Clear();
+ bufferSize = 0;
+ flushing = false;
+ }
+
+ ///
+ protected override void OnDisposing()
+ {
+ RecentlyDecodedFrame.Dispose();
+ ffmpeg.avcodec_close(Pointer);
+ }
+
+ private void ReadNextFrame()
+ {
+ ffmpeg.av_frame_unref(RecentlyDecodedFrame.Pointer);
+ int error;
+
+ do
+ {
+ if (!flushing)
+ {
+ DecodePacket(); // Gets the next packet and sends it to the decoder
+ }
+
+ error = ffmpeg.avcodec_receive_frame(Pointer, RecentlyDecodedFrame.Pointer); // Tries to decode frame from the packets.
+ }
+ while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN) || error == -35); // The EAGAIN code means that the frame decoding has not been completed and more packets are needed.
+
+ if (error == ffmpeg.AVERROR_EOF)
+ {
+ throw new EndOfStreamException("End of file.");
+ }
+
+ error.ThrowIfError("An error occurred while decoding the frame.");
+ }
+
+ private void DecodePacket()
+ {
+ if (!reuseLastPacket)
+ {
+ if (IsBufferEmpty)
+ {
+ flushing = !OwnerFile.GetPacketFromStream(Info.Index);
+ }
+
+ packet = BufferedPackets.Dequeue();
+ bufferSize -= packet.Pointer->size;
+ }
+
+ // Sends the packet to the decoder.
+ var result = ffmpeg.avcodec_send_packet(Pointer, packet);
+
+ reuseLastPacket = result == ffmpeg.AVERROR(ffmpeg.EAGAIN);
+
+ if (!reuseLastPacket)
+ {
+ packet.Wipe();
+ packet.Dispose();
+ result.ThrowIfError("Cannot send a packet to the decoder.");
+ }
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/Internal/DecoderFactory.cs b/FFMediaToolkit/Decoding/Internal/DecoderFactory.cs
new file mode 100644
index 00000000..b47a883b
--- /dev/null
+++ b/FFMediaToolkit/Decoding/Internal/DecoderFactory.cs
@@ -0,0 +1,46 @@
+using FFMediaToolkit.Audio;
+
+namespace FFMediaToolkit.Decoding.Internal
+{
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains method for opening media streams.
+ ///
+ internal unsafe class DecoderFactory
+ {
+ ///
+ /// Opens the video stream with the specified index in the media container.
+ ///
+ /// The media container.
+ /// The media options.
+ /// The stream.
+ /// The opened .
+ internal static Decoder OpenStream(InputContainer container, MediaOptions options, AVStream* stream)
+ {
+ var format = container.Pointer;
+ AVCodec* codec = null;
+
+ var index = ffmpeg.av_find_best_stream(format, stream->codecpar->codec_type, stream->index, -1, &codec, 0);
+ index.IfError(ffmpeg.AVERROR_DECODER_NOT_FOUND, "Cannot find a codec for the specified stream.");
+ if (index < 0)
+ {
+ return null;
+ }
+
+ var codecContext = ffmpeg.avcodec_alloc_context3(codec);
+ ffmpeg.avcodec_parameters_to_context(codecContext, stream->codecpar)
+ .ThrowIfError("Cannot open the stream codec!");
+ codecContext->pkt_timebase = stream->time_base;
+
+ var dict = new FFDictionary(options.DecoderOptions, false).Pointer;
+
+ ffmpeg.avcodec_open2(codecContext, codec, &dict)
+ .ThrowIfError("Cannot open the stream codec!");
+
+ return new Decoder(codecContext, stream, container);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/Internal/InputContainer.cs b/FFMediaToolkit/Decoding/Internal/InputContainer.cs
new file mode 100644
index 00000000..8bf7a3e2
--- /dev/null
+++ b/FFMediaToolkit/Decoding/Internal/InputContainer.cs
@@ -0,0 +1,227 @@
+namespace FFMediaToolkit.Decoding.Internal
+{
+ using System;
+ using System.IO;
+
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Helpers;
+
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents the multimedia file container.
+ ///
+ internal unsafe class InputContainer : Wrapper
+ {
+ private readonly avio_alloc_context_read_packet readCallback;
+
+ private readonly avio_alloc_context_seek seekCallBack;
+ private readonly IDisposable _callbackReference;
+
+ private InputContainer(AVFormatContext* formatContext, int bufferSizeLimit)
+ : base(formatContext)
+ {
+ Decoders = new Decoder[Pointer->nb_streams];
+ MaxBufferSize = bufferSizeLimit;
+ }
+
+ private InputContainer(AVFormatContext* formatContext, avio_alloc_context_read_packet read, avio_alloc_context_seek seek, int bufferSizeLimit, IDisposable callbackReference = null)
+ : base(formatContext)
+ {
+ Decoders = new Decoder[Pointer->nb_streams];
+ MaxBufferSize = bufferSizeLimit;
+ readCallback = read;
+ seekCallBack = seek;
+ _callbackReference = callbackReference;
+ }
+
+ private delegate void AVFormatContextDelegate(AVFormatContext* context);
+
+ ///
+ /// List of all stream codecs that have been opened from the file.
+ ///
+ public Decoder[] Decoders { get; }
+
+ ///
+ /// Gets the memory limit of packets stored in the decoder's buffer.
+ ///
+ public int MaxBufferSize { get; }
+
+ ///
+ /// Opens a media container and stream codecs from given path.
+ ///
+ /// A path to the multimedia file.
+ /// The media settings.
+ /// A new instance of the class.
+ public static InputContainer LoadFile(string path, MediaOptions options) => MakeContainer(path, options, _ => { });
+
+ ///
+ /// Opens a media container and stream codecs from given stream.
+ ///
+ /// A stream of the multimedia file.
+ /// The media settings.
+ /// A new instance of the class.
+ public static InputContainer LoadStream(Stream stream, MediaOptions options) => MakeContainer(stream, options);
+
+ ///
+ /// Seeks all streams in the container to the first key frame before the specified time stamp.
+ ///
+ /// The target time stamp in a stream time base.
+ /// The stream index. It will be used only to get the correct time base value.
+ public void SeekFile(long targetTs, int streamIndex)
+ {
+ ffmpeg.av_seek_frame(Pointer, streamIndex, targetTs, ffmpeg.AVSEEK_FLAG_BACKWARD).ThrowIfError($"Seek to {targetTs} failed.");
+
+ foreach (var decoder in Decoders)
+ {
+ decoder?.DiscardBufferedData();
+ }
+ }
+
+ ///
+ /// Reads a packet from the specified stream index and buffers it in the respective codec.
+ ///
+ /// Index of the stream to read from.
+ /// True if the requested packet was read, false if EOF ocurred and a flush packet was send to the buffer.
+ public bool GetPacketFromStream(int streamIndex)
+ {
+ MediaPacket packet;
+ do
+ {
+ if (!TryReadNextPacket(out packet))
+ {
+ Decoders[streamIndex].BufferPacket(MediaPacket.CreateFlushPacket(streamIndex));
+ return false;
+ }
+
+ var stream = Decoders[packet.StreamIndex];
+ if (stream == null)
+ {
+ packet.Wipe();
+ packet.Dispose();
+ packet = null;
+ }
+ else
+ {
+ stream.BufferPacket(packet);
+ }
+ }
+ while (packet?.StreamIndex != streamIndex);
+ return true;
+ }
+
+ ///
+ protected override void OnDisposing()
+ {
+ foreach (var decoder in Decoders)
+ {
+ decoder?.Dispose();
+ }
+
+ var ptr = Pointer;
+ ffmpeg.avformat_close_input(&ptr);
+
+ _callbackReference?.Dispose();
+ }
+
+ private static AVFormatContext* MakeContext(string url, MediaOptions options, AVFormatContextDelegate contextDelegate)
+ {
+ FFmpegLoader.LoadFFmpeg();
+
+ var context = ffmpeg.avformat_alloc_context();
+ options.DemuxerOptions.ApplyFlags(context);
+ var dict = new FFDictionary(options.DemuxerOptions.PrivateOptions, false).Pointer;
+
+ contextDelegate(context);
+
+ ffmpeg.avformat_open_input(&context, url, null, &dict)
+ .ThrowIfError("An error occurred while opening the file");
+
+ ffmpeg.avformat_find_stream_info(context, null)
+ .ThrowIfError("Cannot find stream info");
+
+ return context;
+ }
+
+ private static InputContainer MakeContainer(Stream input, MediaOptions options)
+ {
+ var avioStream = new AvioStream(input);
+ var read = (avio_alloc_context_read_packet)avioStream.Read;
+ var seek = (avio_alloc_context_seek)avioStream.Seek;
+
+ var context = MakeContext(null, options, ctx =>
+ {
+ int bufferLength = 4096;
+ var avioBuffer = (byte*)ffmpeg.av_malloc((ulong)bufferLength);
+
+ ctx->pb = ffmpeg.avio_alloc_context(avioBuffer, bufferLength, 0, null, read, null, seek);
+ if (ctx->pb == null)
+ {
+ throw new FFmpegException("Cannot allocate AVIOContext.");
+ }
+ });
+
+ var container = new InputContainer(context, read, seek, options.PacketBufferSizeLimit, avioStream);
+ container.OpenStreams(options);
+ return container;
+ }
+
+ private static InputContainer MakeContainer(string url, MediaOptions options, AVFormatContextDelegate contextDelegate)
+ {
+ var context = MakeContext(url, options, contextDelegate);
+
+ var container = new InputContainer(context, options.PacketBufferSizeLimit);
+ container.OpenStreams(options);
+ return container;
+ }
+
+ ///
+ /// Opens the streams in the file using the specified .
+ ///
+ /// The object.
+ private void OpenStreams(MediaOptions options)
+ {
+ for (int i = 0; i < Pointer->nb_streams; i++)
+ {
+ var stream = Pointer->streams[i];
+ if (options.ShouldLoadStreamsOfType(stream->codecpar->codec_type))
+ {
+ try
+ {
+ Decoders[i] = DecoderFactory.OpenStream(this, options, stream);
+ }
+ catch (Exception)
+ {
+ Decoders[i] = null;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Reads the next packet from this file.
+ ///
+ private bool TryReadNextPacket(out MediaPacket packet)
+ {
+ packet = MediaPacket.AllocateEmpty();
+ var result = ffmpeg.av_read_frame(Pointer, packet.Pointer); // Gets the next packet from the file.
+
+ // Check if the end of file error occurred
+ if (result < 0)
+ {
+ packet.Dispose();
+ if (result == ffmpeg.AVERROR_EOF)
+ {
+ return false;
+ }
+ else
+ {
+ result.ThrowIfError("Cannot read next packet from the file");
+ }
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/MediaChapter.cs b/FFMediaToolkit/Decoding/MediaChapter.cs
new file mode 100644
index 00000000..dd203d9c
--- /dev/null
+++ b/FFMediaToolkit/Decoding/MediaChapter.cs
@@ -0,0 +1,44 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Represents a chapter in a media file.
+ ///
+ public class MediaChapter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The starting time of this chapter.
+ /// The ending time of this chapter.
+ /// This chapter's metadata.
+ internal MediaChapter(TimeSpan start, TimeSpan end, Dictionary metadata)
+ {
+ StartTime = start;
+ EndTime = end;
+ Metadata = metadata;
+ }
+
+ ///
+ /// Gets the start time of this chapter.
+ ///
+ public TimeSpan StartTime { get; }
+
+ ///
+ /// Gets the end time of this chapter.
+ ///
+ public TimeSpan EndTime { get; }
+
+ ///
+ /// Gets the duration of this chapter.
+ ///
+ public TimeSpan Duration => EndTime - StartTime;
+
+ ///
+ /// Gets the metadata for this chapter (such as name).
+ ///
+ public Dictionary Metadata { get; }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/MediaFile.cs b/FFMediaToolkit/Decoding/MediaFile.cs
new file mode 100644
index 00000000..6f3bd134
--- /dev/null
+++ b/FFMediaToolkit/Decoding/MediaFile.cs
@@ -0,0 +1,148 @@
+using FFMediaToolkit.Audio;
+
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.IO;
+ using System.Linq;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Decoding.Internal;
+
+ ///
+ /// Represents a multimedia file.
+ ///
+ public class MediaFile : IDisposable
+ {
+ private readonly InputContainer container;
+
+ ///
+ /// Gets a value indicating whether the file has been disposed.
+ ///
+ public bool IsDisposed { get; private set; }
+
+ private unsafe MediaFile(InputContainer container, MediaOptions options)
+ {
+ this.container = container;
+
+ var video = container.Decoders.Where(codec => codec?.Info.Type == MediaType.Video);
+ var audio = container.Decoders.Where(codec => codec?.Info.Type == MediaType.Audio);
+
+ VideoStreams = video.Select(codec => new VideoStream(codec, options)).ToArray();
+ AudioStreams = audio.Select(codec => new AudioStream(codec, options, options.AudioSampleFormat)).ToArray();
+
+ Info = new MediaInfo(container.Pointer);
+ }
+
+ ///
+ /// Gets all the video streams in the media file.
+ ///
+ public VideoStream[] VideoStreams { get; }
+
+ ///
+ /// Gets the first video stream in the media file.
+ ///
+ public VideoStream Video => VideoStreams.FirstOrDefault();
+
+ ///
+ /// Gets a value indicating whether the file contains video streams.
+ ///
+ public bool HasVideo => VideoStreams.Length > 0;
+
+ ///
+ /// Gets all the audio streams in the media file.
+ ///
+ public AudioStream[] AudioStreams { get; }
+
+ ///
+ /// Gets the first audio stream in the media file.
+ ///
+ public AudioStream Audio => AudioStreams.FirstOrDefault();
+
+ ///
+ /// Gets a value indicating whether the file contains video streams.
+ ///
+ public bool HasAudio => AudioStreams.Length > 0;
+
+ ///
+ /// Gets informations about the media container.
+ ///
+ public MediaInfo Info { get; }
+
+ ///
+ /// Opens a media file from the specified path with default settings.
+ ///
+ /// A path to the media file.
+ /// The opened .
+ public static MediaFile Open(string path) => Open(path, new MediaOptions());
+
+ ///
+ /// Opens a media file from the specified path.
+ ///
+ /// A path to the media file.
+ /// The decoder settings.
+ /// The opened .
+ public static MediaFile Open(string path, MediaOptions options)
+ {
+ try
+ {
+ var container = InputContainer.LoadFile(path, options);
+ return new MediaFile(container, options);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("Failed to open the media file", ex);
+ }
+ }
+
+ ///
+ /// Opens a media stream with default settings.
+ ///
+ /// A stream of the multimedia file.
+ /// The opened .
+ public static MediaFile Open(Stream stream) => Open(stream, new MediaOptions());
+
+ ///
+ /// Opens a media stream.
+ ///
+ /// A stream of the multimedia file.
+ /// The decoder settings.
+ /// The opened .
+ public static MediaFile Open(Stream stream, MediaOptions options)
+ {
+ try
+ {
+ var container = InputContainer.LoadStream(stream, options);
+ return new MediaFile(container, options);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("Failed to open the media stream", ex);
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ var video = VideoStreams.Cast();
+ var audio = AudioStreams.Cast();
+
+ var streams = video.Concat(audio);
+
+ foreach (var stream in streams)
+ stream.Dispose();
+
+ container.Dispose();
+
+ IsDisposed = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/MediaInfo.cs b/FFMediaToolkit/Decoding/MediaInfo.cs
new file mode 100644
index 00000000..4b5ec6dc
--- /dev/null
+++ b/FFMediaToolkit/Decoding/MediaInfo.cs
@@ -0,0 +1,110 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.Collections.ObjectModel;
+ using System.IO;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains informations about the media container.
+ ///
+ public class MediaInfo
+ {
+ private readonly Lazy fileInfo;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The input container context.
+ internal unsafe MediaInfo(AVFormatContext* container)
+ {
+ FilePath = new IntPtr(container->url).Utf8ToString();
+ ContainerFormat = new IntPtr(container->iformat->name).Utf8ToString();
+ Metadata = new ContainerMetadata(container->metadata);
+ Bitrate = container->bit_rate > 0 ? container->bit_rate : 0;
+
+ var timeBase = new AVRational { num = 1, den = ffmpeg.AV_TIME_BASE };
+ Duration = container->duration != ffmpeg.AV_NOPTS_VALUE ?
+ container->duration.ToTimeSpan(timeBase) :
+ TimeSpan.Zero;
+ StartTime = container->start_time != ffmpeg.AV_NOPTS_VALUE ?
+ container->start_time.ToTimeSpan(timeBase) :
+ TimeSpan.Zero;
+ Chapters = new ReadOnlyCollection(ParseChapters(container));
+
+ fileInfo = new Lazy(() =>
+ {
+ try
+ {
+ var info = new FileInfo(FilePath);
+ return info;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ });
+ }
+
+ ///
+ /// Gets the file path used to open the container.
+ ///
+ public string FilePath { get; }
+
+ ///
+ /// Gets a object for the media file.
+ /// It contains file size, directory, last access, creation and write timestamps.
+ /// Returns if not available, for example when was used to open the .
+ ///
+ public FileInfo FileInfo => fileInfo.Value;
+
+ ///
+ /// Gets the container format name.
+ ///
+ public string ContainerFormat { get; }
+
+ ///
+ /// Gets the container bitrate in bytes per second (B/s) units. 0 if unknown.
+ ///
+ public long Bitrate { get; }
+
+ ///
+ /// Gets the duration of the media container.
+ ///
+ public TimeSpan Duration { get; }
+
+ ///
+ /// Gets the start time of the media container.
+ ///
+ public TimeSpan StartTime { get; }
+
+ ///
+ /// Gets the container file metadata. Streams may contain additional metadata.
+ ///
+ public ContainerMetadata Metadata { get; }
+
+ ///
+ /// Gets a collection of chapters existing in the media file.
+ ///
+ public ReadOnlyCollection Chapters { get; }
+
+ private static unsafe MediaChapter[] ParseChapters(AVFormatContext* container)
+ {
+ var streamChapters = new MediaChapter[container->nb_chapters];
+
+ for (var i = 0; i < container->nb_chapters; i++)
+ {
+ var chapter = container->chapters[i];
+ var meta = chapter->metadata;
+ var startTimespan = chapter->start.ToTimeSpan(chapter->time_base);
+ var endTimespan = chapter->end.ToTimeSpan(chapter->time_base);
+ streamChapters[i] =
+ new MediaChapter(startTimespan, endTimespan, FFDictionary.ToDictionary(meta, true));
+ }
+
+ return streamChapters;
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/MediaOptions.cs b/FFMediaToolkit/Decoding/MediaOptions.cs
new file mode 100644
index 00000000..925d810a
--- /dev/null
+++ b/FFMediaToolkit/Decoding/MediaOptions.cs
@@ -0,0 +1,112 @@
+using FFMediaToolkit.Audio;
+
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Drawing;
+ using FFMediaToolkit.Graphics;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents the audio/video streams loading modes.
+ ///
+ [Flags]
+ public enum MediaMode
+ {
+ ///
+ /// Enables loading only video streams.
+ ///
+ Video = 1 << AVMediaType.AVMEDIA_TYPE_VIDEO,
+
+ ///
+ /// Enables loading only audio streams.
+ ///
+ Audio = 1 << AVMediaType.AVMEDIA_TYPE_AUDIO,
+
+ ///
+ /// Enables loading both audio and video streams if they exist.
+ ///
+ AudioVideo = Audio | Video,
+ }
+
+ ///
+ /// Represents the multimedia file container options.
+ ///
+ public class MediaOptions
+ {
+ private const string Threads = "threads";
+ private const string Auto = "auto";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MediaOptions() => DecoderThreads = null;
+
+ ///
+ /// Gets or sets the limit of memory used by the packet buffer. Default limit is 40 MB per stream.
+ ///
+ public int PacketBufferSizeLimit { get; set; } = 40;
+
+ ///
+ /// Gets or sets the demuxer settings.
+ ///
+ public ContainerOptions DemuxerOptions { get; set; } = new ContainerOptions();
+
+ ///
+ /// Gets or sets the target pixel format for decoded video frames conversion. The default value is Bgr24.
+ ///
+ public ImagePixelFormat VideoPixelFormat { get; set; } = ImagePixelFormat.Bgr24;
+
+ ///
+ /// Gets or sets the target sample format for decoded audio frames conversion. The default value is SingleP.
+ ///
+ public SampleFormat AudioSampleFormat { get; set; } = SampleFormat.SingleP;
+
+ ///
+ /// Gets or sets the target video size for decoded video frames conversion. , if no rescale.
+ ///
+ public Size? TargetVideoSize { get; set; }
+
+ ///
+ /// Gets or sets the threshold value used to choose the best seek method. Set this to video GoP value (if know) to improve stream seek performance.
+ ///
+ public int VideoSeekThreshold { get; set; } = 12;
+
+ ///
+ /// Gets or sets the threshold value used to choose the best seek method.
+ ///
+ public int AudioSeekThreshold { get; set; } = 12;
+
+ ///
+ /// Gets or sets the number of decoder threads (by the 'threads' flag). The default value is - 'auto'.
+ ///
+ public int? DecoderThreads
+ {
+ get => int.TryParse(DecoderOptions[Threads], out var count) ? (int?)count : null;
+ set => DecoderOptions[Threads] = value.HasValue ? value.ToString() : Auto;
+ }
+
+ ///
+ /// Gets or sets the dictionary with global options for the multimedia decoders.
+ ///
+ public Dictionary DecoderOptions { get; set; } = new Dictionary();
+
+ ///
+ /// Gets or sets which streams (audio/video) will be loaded.
+ ///
+ public MediaMode StreamsToLoad { get; set; } = MediaMode.AudioVideo;
+
+ ///
+ /// Determines whether streams of a certain should be loaded
+ /// (Based on property).
+ ///
+ /// A given .
+ /// if streams of the given are to be loaded.
+ public bool ShouldLoadStreamsOfType(AVMediaType type)
+ {
+ var mode = (MediaMode)(1 << (int)type);
+ return StreamsToLoad.HasFlag(mode);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/MediaStream.cs b/FFMediaToolkit/Decoding/MediaStream.cs
new file mode 100644
index 00000000..45c3c797
--- /dev/null
+++ b/FFMediaToolkit/Decoding/MediaStream.cs
@@ -0,0 +1,109 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Decoding.Internal;
+ using FFMediaToolkit.Helpers;
+
+ ///
+ /// A base for streams of any kind of media.
+ ///
+ public class MediaStream : IDisposable
+ {
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The associated codec.
+ /// Extra options.
+ internal MediaStream(Decoder stream, MediaOptions options)
+ {
+ Stream = stream;
+ Options = options;
+
+ Threshold = TimeSpan.FromSeconds(0.5).ToTimestamp(Info.TimeBase);
+ }
+
+ ///
+ /// Gets informations about this stream.
+ ///
+ public StreamInfo Info => Stream.Info;
+
+ ///
+ /// Gets the timestamp of the recently decoded frame in the media stream.
+ ///
+ public TimeSpan Position => Math.Max(Stream.RecentlyDecodedFrame.PresentationTimestamp, 0).ToTimeSpan(Info.TimeBase);
+
+ ///
+ /// Indicates whether the stream has buffered frame data.
+ ///
+ public bool IsBufferEmpty => Stream.IsBufferEmpty;
+
+ ///
+ /// Gets the options configured for this .
+ ///
+ protected MediaOptions Options { get; }
+
+ private Decoder Stream { get; }
+
+ private long Threshold { get; }
+
+ ///
+ /// Discards all buffered frame data associated with this stream.
+ ///
+ [Obsolete("Do not call this method. Buffered data is automatically discarded when required")]
+ public void DiscardBufferedData() => Stream.DiscardBufferedData();
+
+ ///
+ public virtual void Dispose()
+ {
+ if (!isDisposed)
+ {
+ Stream.DiscardBufferedData();
+ Stream.Dispose();
+ isDisposed = true;
+ }
+ }
+
+ ///
+ /// Gets the data belonging to the next frame in the stream.
+ ///
+ /// The next frame's data.
+ internal MediaFrame GetNextFrame() => Stream.GetNextFrame();
+
+ ///
+ /// Seeks the stream to the specified time and returns the nearest frame's data.
+ ///
+ /// A specific point in time in this stream.
+ /// The nearest frame's data.
+ internal MediaFrame GetFrame(TimeSpan time)
+ {
+ var ts = time.ToTimestamp(Info.TimeBase);
+ var frame = GetFrameByTimestamp(ts);
+ return frame;
+ }
+
+ private MediaFrame GetFrameByTimestamp(long ts)
+ {
+ var frame = Stream.RecentlyDecodedFrame;
+ ts = Math.Max(0, Math.Min(ts, Info.DurationRaw));
+
+ if (ts > frame.PresentationTimestamp && ts < frame.PresentationTimestamp + Threshold)
+ {
+ return Stream.GetNextFrame();
+ }
+ else if (ts != frame.PresentationTimestamp)
+ {
+ if (ts < frame.PresentationTimestamp || ts >= frame.PresentationTimestamp + Threshold)
+ {
+ Stream.OwnerFile.SeekFile(ts, Info.Index);
+ }
+
+ Stream.SkipFrames(ts);
+ }
+
+ return Stream.RecentlyDecodedFrame;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/StreamInfo.cs b/FFMediaToolkit/Decoding/StreamInfo.cs
new file mode 100644
index 00000000..05bf9529
--- /dev/null
+++ b/FFMediaToolkit/Decoding/StreamInfo.cs
@@ -0,0 +1,169 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.Collections.ObjectModel;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Decoding.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents generic informations about the stream, specialized by subclasses for specific
+ /// kinds of streams.
+ ///
+ public class StreamInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A generic stream.
+ /// The media type of the stream.
+ /// The input container.
+ internal unsafe StreamInfo(AVStream* stream, MediaType type, InputContainer container)
+ {
+ var codecId = stream->codecpar->codec_id;
+ Metadata = new ReadOnlyDictionary(FFDictionary.ToDictionary(stream->metadata, true));
+ CodecName = ffmpeg.avcodec_get_name(codecId);
+ CodecId = codecId.FormatEnum(12);
+ Index = stream->index;
+ Type = type;
+
+ TimeBase = stream->time_base;
+ RealFrameRate = stream->r_frame_rate;
+ AvgFrameRate = stream->avg_frame_rate.ToDouble();
+ IsVariableFrameRate = RealFrameRate.ToDouble() != AvgFrameRate;
+
+ if (stream->duration >= 0)
+ {
+ Duration = stream->duration.ToTimeSpan(stream->time_base);
+ DurationRaw = stream->duration;
+ }
+ else
+ {
+ Duration = TimeSpan.FromTicks(container.Pointer->duration * 10);
+ DurationRaw = Duration.ToTimestamp(TimeBase);
+ }
+
+ if (stream->start_time >= 0)
+ {
+ StartTime = stream->start_time.ToTimeSpan(stream->time_base);
+ }
+
+ if (stream->nb_frames > 0)
+ {
+ IsFrameCountProvidedByContainer = true;
+ NumberOfFrames = (int)stream->nb_frames;
+#pragma warning disable CS0618 // Type or member is obsolete
+ FrameCount = NumberOfFrames.Value;
+ }
+ else
+ {
+ FrameCount = Duration.ToFrameNumber(stream->avg_frame_rate);
+ if (!IsVariableFrameRate)
+ {
+ NumberOfFrames = FrameCount;
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+ else
+ {
+ NumberOfFrames = null;
+ }
+ }
+ }
+
+ ///
+ /// Gets the stream index.
+ ///
+ public int Index { get; }
+
+ ///
+ /// Gets the codec name.
+ ///
+ public string CodecName { get; }
+
+ ///
+ /// Gets the codec identifier.
+ ///
+ public string CodecId { get; }
+
+ ///
+ /// Gets the stream's type.
+ ///
+ public MediaType Type { get; }
+
+ ///
+ /// Gets a value indicating whether the value is know from the multimedia container metadata.
+ ///
+ public bool IsFrameCountProvidedByContainer { get; }
+
+ ///
+ /// Gets the stream time base.
+ ///
+ public AVRational TimeBase { get; }
+
+ ///
+ /// Gets the number of frames value from the container metadata, if available (see )
+ /// Otherwise, it is estimated from the video duration and average frame rate.
+ /// This value may not be accurate, if the video is variable frame rate (see property).
+ ///
+ [Obsolete("Please use \"StreamInfo.NumberOfFrames\" property instead.")]
+ public int FrameCount { get; }
+
+ ///
+ /// Gets the number of frames value taken from the container metadata or estimated in constant frame rate videos. Returns if not available.
+ ///
+ public int? NumberOfFrames { get; }
+
+ ///
+ /// Gets the stream duration.
+ ///
+ public TimeSpan Duration { get; }
+
+ ///
+ /// Gets the stream start time. Null if undefined.
+ ///
+ public TimeSpan? StartTime { get; }
+
+ ///
+ /// Gets the average frame rate as a value.
+ ///
+ public double AvgFrameRate { get; }
+
+ ///
+ /// Gets the frame rate as a value.
+ /// It is used to calculate timestamps in the internal decoder methods.
+ ///
+ public AVRational RealFrameRate { get; }
+
+ ///
+ /// Gets a value indicating whether the video is variable frame rate (VFR).
+ ///
+ public bool IsVariableFrameRate { get; }
+
+ ///
+ /// Gets the stream metadata.
+ ///
+ public ReadOnlyDictionary Metadata { get; }
+
+ ///
+ /// Gets the duration of the stream in the time base units.
+ ///
+ internal long DurationRaw { get; }
+
+ ///
+ /// Creates the apprioriate type of class depending on the kind
+ /// of stream passed in.
+ ///
+ /// The represented stream.
+ /// The input container.
+ /// The resulting new object.
+ internal static unsafe StreamInfo Create(AVStream* stream, InputContainer owner)
+ {
+ if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
+ return new AudioStreamInfo(stream, owner);
+ if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
+ return new VideoStreamInfo(stream, owner);
+ return new StreamInfo(stream, MediaType.None, owner);
+ }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Decoding/VideoStream.cs b/FFMediaToolkit/Decoding/VideoStream.cs
new file mode 100644
index 00000000..f59394cf
--- /dev/null
+++ b/FFMediaToolkit/Decoding/VideoStream.cs
@@ -0,0 +1,250 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.Drawing;
+ using System.IO;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Decoding.Internal;
+ using FFMediaToolkit.Graphics;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a video stream in the .
+ ///
+ public class VideoStream : MediaStream
+ {
+ private readonly int outputFrameStride;
+ private readonly int requiredBufferSize;
+ private readonly ImageConverter converter;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The video stream.
+ /// The decoder settings.
+ internal VideoStream(Decoder stream, MediaOptions options)
+ : base(stream, options)
+ {
+ OutputFrameSize = options.TargetVideoSize ?? Info.FrameSize;
+ converter = new ImageConverter(OutputFrameSize, (AVPixelFormat)options.VideoPixelFormat);
+
+ outputFrameStride = ImageData.EstimateStride(OutputFrameSize.Width, Options.VideoPixelFormat);
+ requiredBufferSize = outputFrameStride * OutputFrameSize.Height;
+ }
+
+ ///
+ /// Gets informations about this stream.
+ ///
+ public new VideoStreamInfo Info => base.Info as VideoStreamInfo;
+
+ private Size OutputFrameSize { get; }
+
+ ///
+ /// Reads the next frame from the video stream.
+ ///
+ /// A decoded bitmap.
+ /// End of the stream.
+ /// Internal decoding error.
+ public new ImageData GetNextFrame()
+ {
+ var frame = base.GetNextFrame() as VideoFrame;
+ return frame.ToBitmap(converter, Options.VideoPixelFormat, OutputFrameSize);
+ }
+
+ ///
+ /// Reads the next frame from the video stream.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// The decoded video frame.
+ /// if reached end of the stream.
+ /// Internal decoding error.
+ public bool TryGetNextFrame(out ImageData bitmap)
+ {
+ try
+ {
+ bitmap = GetNextFrame();
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ bitmap = default;
+ return false;
+ }
+ }
+
+ ///
+ /// Reads the next frame from the video stream and writes the converted bitmap data directly to the provided buffer.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// Pointer to the memory buffer.
+ /// if reached end of the stream.
+ /// Too small buffer.
+ /// Internal decoding error.
+ public unsafe bool TryGetNextFrame(Span buffer)
+ {
+ if (buffer.Length < requiredBufferSize)
+ {
+ throw new ArgumentException(nameof(buffer), "Destination buffer is smaller than the converted bitmap data.");
+ }
+
+ try
+ {
+ fixed (byte* ptr = buffer)
+ {
+ ConvertCopyFrameToMemory(base.GetNextFrame() as VideoFrame, ptr);
+ }
+
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Reads the next frame from the video stream and writes the converted bitmap data directly to the provided buffer.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// Pointer to the memory buffer.
+ /// Size in bytes of a single row of pixels.
+ /// if reached end of the stream.
+ /// Too small buffer.
+ /// Internal decoding error.
+ public unsafe bool TryGetNextFrame(IntPtr buffer, int bufferStride)
+ {
+ if (bufferStride != outputFrameStride)
+ {
+ throw new ArgumentException(nameof(bufferStride), "Destination buffer is smaller than the converted bitmap data.");
+ }
+
+ try
+ {
+ ConvertCopyFrameToMemory(base.GetNextFrame() as VideoFrame, (byte*)buffer);
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Reads the video frame found at the specified timestamp.
+ ///
+ /// The frame timestamp.
+ /// The decoded video frame.
+ /// End of the stream.
+ /// Internal decoding error.
+ public new ImageData GetFrame(TimeSpan time)
+ {
+ var frame = base.GetFrame(time) as VideoFrame;
+ return frame.ToBitmap(converter, Options.VideoPixelFormat, OutputFrameSize);
+ }
+
+ ///
+ /// Reads the video frame found at the specified timestamp.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// The frame timestamp.
+ /// The decoded video frame.
+ /// if reached end of the stream.
+ /// Internal decoding error.
+ public bool TryGetFrame(TimeSpan time, out ImageData bitmap)
+ {
+ try
+ {
+ bitmap = GetFrame(time);
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ bitmap = default;
+ return false;
+ }
+ }
+
+ ///
+ /// Reads the video frame found at the specified timestamp and writes the converted bitmap data directly to the provided buffer.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// The frame timestamp.
+ /// Pointer to the memory buffer.
+ /// if reached end of the stream.
+ /// Too small buffer.
+ /// Internal decoding error.
+ public unsafe bool TryGetFrame(TimeSpan time, Span buffer)
+ {
+ if (buffer.Length < requiredBufferSize)
+ {
+ throw new ArgumentException(nameof(buffer), "Destination buffer is smaller than the converted bitmap data.");
+ }
+
+ try
+ {
+ fixed (byte* ptr = buffer)
+ {
+ ConvertCopyFrameToMemory(base.GetFrame(time) as VideoFrame, ptr);
+ }
+
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Reads the video frame found at the specified timestamp and writes the converted bitmap data directly to the provided buffer.
+ /// A return value indicates that reached end of stream.
+ /// The method throws exception if another error has occurred.
+ ///
+ /// The frame timestamp.
+ /// Pointer to the memory buffer.
+ /// Size in bytes of a single row of pixels.
+ /// if reached end of the stream.
+ /// Too small buffer.
+ /// Internal decoding error.
+ public unsafe bool TryGetFrame(TimeSpan time, IntPtr buffer, int bufferStride)
+ {
+ if (bufferStride != outputFrameStride)
+ {
+ throw new ArgumentException(nameof(bufferStride), "Destination buffer is smaller than the converted bitmap data.");
+ }
+
+ try
+ {
+ ConvertCopyFrameToMemory(base.GetFrame(time) as VideoFrame, (byte*)buffer);
+ return true;
+ }
+ catch (EndOfStreamException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ public override void Dispose()
+ {
+ if (!isDisposed)
+ {
+ converter.Dispose();
+ isDisposed = true;
+ }
+
+ base.Dispose();
+ }
+
+ private unsafe void ConvertCopyFrameToMemory(VideoFrame frame, byte* target)
+ {
+ converter.AVFrameToBitmap(frame, target, outputFrameStride);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Decoding/VideoStreamInfo.cs b/FFMediaToolkit/Decoding/VideoStreamInfo.cs
new file mode 100644
index 00000000..939e9b06
--- /dev/null
+++ b/FFMediaToolkit/Decoding/VideoStreamInfo.cs
@@ -0,0 +1,82 @@
+namespace FFMediaToolkit.Decoding
+{
+ using System;
+ using System.Drawing;
+ using System.Runtime.InteropServices;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Decoding.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents informations about the video stream.
+ ///
+ public class VideoStreamInfo : StreamInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A generic stream.
+ /// The input container.
+ internal unsafe VideoStreamInfo(AVStream* stream, InputContainer container)
+ : base(stream, MediaType.Video, container)
+ {
+ var codec = stream->codecpar;
+ IsInterlaced = codec->field_order != AVFieldOrder.AV_FIELD_PROGRESSIVE &&
+ codec->field_order != AVFieldOrder.AV_FIELD_UNKNOWN;
+ FrameSize = new Size(codec->width, codec->height);
+ PixelFormat = ((AVPixelFormat)codec->format).FormatEnum(11);
+ AVPixelFormat = (AVPixelFormat)codec->format;
+
+ var matrix = (IntPtr)ffmpeg.av_stream_get_side_data(stream, AVPacketSideDataType.AV_PKT_DATA_DISPLAYMATRIX, null);
+ Rotation = CalculateRotation(matrix);
+ }
+
+ ///
+ /// Gets the clockwise rotation angle computed from the display matrix.
+ ///
+ public double Rotation { get; }
+
+ ///
+ /// Gets a value indicating whether the frames in the stream are interlaced.
+ ///
+ public bool IsInterlaced { get; }
+
+ ///
+ /// Gets the video frame dimensions.
+ ///
+ public Size FrameSize { get; }
+
+ ///
+ /// Gets a lowercase string representing the video pixel format.
+ ///
+ public string PixelFormat { get; }
+
+ ///
+ /// Gets the video pixel format.
+ ///
+ internal AVPixelFormat AVPixelFormat { get; }
+
+ private static double CalculateRotation(IntPtr displayMatrix)
+ {
+ const int matrixLength = 9;
+
+ if (displayMatrix == IntPtr.Zero)
+ return 0;
+
+ var matrix = new int[matrixLength];
+ Marshal.Copy(displayMatrix, matrix, 0, matrixLength);
+
+ var scale = new double[2];
+ scale[0] = (matrix[0] != 0 && matrix[3] != 0) ? CalculateHypotenuse(matrix[0], matrix[3]) : 1;
+ scale[1] = (matrix[1] != 0 && matrix[4] != 0) ? CalculateHypotenuse(matrix[1], matrix[4]) : 1;
+
+ var rotation = Math.Atan2(matrix[1] / scale[1], matrix[0] / scale[0]) * 180 / Math.PI;
+ rotation -= 360 * Math.Floor((rotation / 360) + (0.9 / 360));
+
+ return rotation;
+ }
+
+ private static double CalculateHypotenuse(int a, int b) => Math.Sqrt((a * a) + (b * b));
+ }
+}
diff --git a/FFMediaToolkit/Encoding/AudioCodec.cs b/FFMediaToolkit/Encoding/AudioCodec.cs
new file mode 100644
index 00000000..643285ec
--- /dev/null
+++ b/FFMediaToolkit/Encoding/AudioCodec.cs
@@ -0,0 +1,41 @@
+namespace FFMediaToolkit.Encoding
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// This enum contains only supported audio encoders.
+ /// If you want to use a codec not included to this enum, you can cast to .
+ ///
+ public enum AudioCodec
+ {
+ ///
+ /// Default audio codec for the selected container format.
+ ///
+ Default = AVCodecID.AV_CODEC_ID_NONE,
+
+ ///
+ /// AAC (Advanced Audio Coding) audio codec
+ ///
+ AAC = AVCodecID.AV_CODEC_ID_AAC,
+
+ ///
+ /// ATSC A/52A (AC-3) audio codec
+ ///
+ AC3 = AVCodecID.AV_CODEC_ID_AC3,
+
+ ///
+ /// MP3 (MPEG audio layer 3) audio codec
+ ///
+ MP3 = AVCodecID.AV_CODEC_ID_MP3,
+
+ ///
+ /// Windows Media Audio V2 audio codec
+ ///
+ WMA = AVCodecID.AV_CODEC_ID_WMAV2,
+
+ ///
+ /// OGG Vorbis audio codec
+ ///
+ Vorbis = AVCodecID.AV_CODEC_ID_VORBIS,
+ }
+}
diff --git a/FFMediaToolkit/Encoding/AudioEncoderSettings.cs b/FFMediaToolkit/Encoding/AudioEncoderSettings.cs
new file mode 100644
index 00000000..0a7325de
--- /dev/null
+++ b/FFMediaToolkit/Encoding/AudioEncoderSettings.cs
@@ -0,0 +1,67 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System.Collections.Generic;
+ using FFMediaToolkit.Audio;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents an audio encoder configuration.
+ ///
+ public class AudioEncoderSettings
+ {
+ ///
+ /// Initializes a new instance of the class with default video settings values.
+ ///
+ /// The sample rate of the stream.
+ /// The number of channels in the stream.
+ /// The audio encoder.
+ public AudioEncoderSettings(int sampleRate, int channels, AudioCodec codec = AudioCodec.Default)
+ {
+ SampleRate = sampleRate;
+ Channels = channels;
+ Codec = codec;
+ CodecOptions = new Dictionary();
+ }
+
+ ///
+ /// Gets or sets the audio stream bitrate (bytes per second). The default value is 128,000 B/s.
+ ///
+ public int Bitrate { get; set; } = 128_000;
+
+ ///
+ /// Gets or sets the audio stream sample rate (samples per second). The default value is 44,100 samples/sec.
+ ///
+ public int SampleRate { get; set; } = 44_100;
+
+ ///
+ /// Gets or sets the number of channels in the audio stream. The default value is 2.
+ ///
+ public int Channels { get; set; } = 2;
+
+ ///
+ /// Gets or sets the number of samples per audio frame. Default is 2205 (1/20th of a second at 44.1kHz).
+ ///
+ public int SamplesPerFrame { get; set; } = 2205;
+
+ ///
+ /// Gets or the time base of the audio stream. Always equal to /.
+ ///
+ public AVRational TimeBase => new AVRational { num = SamplesPerFrame, den = SampleRate };
+
+ ///
+ /// Gets or sets the sample format to be used by the audio codec. The default value is (16-bit integer).
+ ///
+ public SampleFormat SampleFormat { get; set; } = SampleFormat.SignedWord;
+
+ ///
+ /// Gets or sets the dictionary with custom codec options.
+ ///
+ public Dictionary CodecOptions { get; set; }
+
+ ///
+ /// Gets or sets the codec for this stream.
+ /// If set to , encoder will use default audio codec for current container.
+ ///
+ public AudioCodec Codec { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/FFMediaToolkit/Encoding/AudioOutputStream.cs b/FFMediaToolkit/Encoding/AudioOutputStream.cs
new file mode 100644
index 00000000..9cf08fd9
--- /dev/null
+++ b/FFMediaToolkit/Encoding/AudioOutputStream.cs
@@ -0,0 +1,154 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System;
+ using FFMediaToolkit.Audio;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Encoding.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents an audio encoder stream.
+ ///
+ public unsafe class AudioOutputStream : IDisposable
+ {
+ private readonly OutputStream stream;
+ private readonly AudioFrame frame;
+
+ private SwrContext* swrContext;
+
+ private bool isDisposed;
+ private long lastFramePts = -1;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The audio stream.
+ /// The stream setting.
+ internal AudioOutputStream(OutputStream stream, AudioEncoderSettings config)
+ {
+ this.stream = stream;
+
+ AVChannelLayout channelLayout;
+ ffmpeg.av_channel_layout_default(&channelLayout, config.Channels);
+ SwrContext* context;
+ ffmpeg.swr_alloc_set_opts2(
+ &context,
+ &channelLayout,
+ (AVSampleFormat)config.SampleFormat,
+ config.SampleRate,
+ &channelLayout,
+ (AVSampleFormat)SampleFormat.SingleP,
+ config.SampleRate,
+ 0,
+ null).ThrowIfError("Cannot allocate SwrContext");
+ ffmpeg.swr_init(context);
+
+ swrContext = context;
+ Configuration = config;
+ frame = AudioFrame.Create(config.SampleRate, config.Channels, config.SamplesPerFrame, channelLayout, SampleFormat.SingleP, 0, 0);
+ }
+
+ ///
+ /// Gets the video encoding configuration used to create this stream.
+ ///
+ public AudioEncoderSettings Configuration { get; }
+
+ ///
+ /// Gets the current duration of this stream.
+ ///
+ public TimeSpan CurrentDuration => lastFramePts.ToTimeSpan(Configuration.TimeBase);
+
+ ///
+ /// Writes the specified audio data to the stream as the next frame.
+ ///
+ /// The audio data to write.
+ /// (optional) custom PTS value for the frame.
+ public void AddFrame(AudioData data, long customPtsValue)
+ {
+ if (customPtsValue <= lastFramePts)
+ throw new Exception("Cannot add a frame that occurs chronologically before the most recently written frame!");
+
+ frame.UpdateFromAudioData(data);
+
+ var converted = AudioFrame.Create(
+ frame.SampleRate,
+ frame.NumChannels,
+ frame.NumSamples,
+ frame.ChannelLayout,
+ Configuration.SampleFormat,
+ frame.DecodingTimestamp,
+ frame.PresentationTimestamp);
+ converted.PresentationTimestamp = customPtsValue;
+
+ ffmpeg.swr_convert_frame(swrContext, converted.Pointer, frame.Pointer);
+
+ stream.Push(converted);
+ converted.Dispose();
+
+ lastFramePts = customPtsValue;
+ }
+
+ ///
+ /// Writes the specified sample data to the stream as the next frame.
+ ///
+ /// The sample data to write.
+ /// (optional) custom PTS value for the frame.
+ public void AddFrame(float[][] samples, long customPtsValue)
+ {
+ if (customPtsValue <= lastFramePts)
+ throw new Exception("Cannot add a frame that occurs chronologically before the most recently written frame!");
+
+ frame.UpdateFromSampleData(samples);
+ frame.PresentationTimestamp = customPtsValue;
+ stream.Push(frame);
+
+ lastFramePts = customPtsValue;
+ }
+
+ ///
+ /// Writes the specified audio data to the stream as the next frame.
+ ///
+ /// The audio data to write.
+ /// Custom timestamp for this frame.
+ public void AddFrame(AudioData data, TimeSpan customTime) => AddFrame(data, customTime.ToTimestamp(Configuration.TimeBase));
+
+ ///
+ /// Writes the specified audio data to the stream as the next frame.
+ ///
+ /// The audio data to write.
+ public void AddFrame(AudioData data) => AddFrame(data, lastFramePts + 1);
+
+ ///
+ /// Writes the specified sample data to the stream as the next frame.
+ ///
+ /// The sample data to write.
+ /// Custom timestamp for this frame.
+ public void AddFrame(float[][] samples, TimeSpan customTime) => AddFrame(samples, customTime.ToTimestamp(Configuration.TimeBase));
+
+ ///
+ /// Writes the specified sample data to the stream as the next frame.
+ ///
+ /// The sample data to write.
+ public void AddFrame(float[][] samples) => AddFrame(samples, lastFramePts + 1);
+
+ ///
+ public void Dispose()
+ {
+ if (isDisposed)
+ {
+ return;
+ }
+
+ stream.Dispose();
+ frame.Dispose();
+
+ fixed (SwrContext** ptr = &swrContext)
+ {
+ ffmpeg.swr_free(ptr);
+ }
+
+ isDisposed = true;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/ContainerFormat.cs b/FFMediaToolkit/Encoding/ContainerFormat.cs
new file mode 100644
index 00000000..20e6f4da
--- /dev/null
+++ b/FFMediaToolkit/Encoding/ContainerFormat.cs
@@ -0,0 +1,71 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System.ComponentModel;
+
+ ///
+ /// Video container formats supported by FFMediaToolkit.
+ ///
+ public enum ContainerFormat
+ {
+ ///
+ /// The 3GPP container format (.3gp)
+ ///
+ [Description("3gp")]
+ Container3GP,
+
+ ///
+ /// The 3GPP2 container format (.3g2)
+ ///
+ [Description("3g2")]
+ Container3GP2,
+
+ ///
+ /// The Microsoft Advanced Systems Formats container format (.asf)
+ /// Use this container when encoding a .wmv (Windows Media) video file.
+ ///
+ [Description("asf")]
+ ASF,
+
+ ///
+ /// The Audio Video Interleave container format (.avi)
+ ///
+ [Description("avi")]
+ AVI,
+
+ ///
+ /// The Flash Video container format (.flv)
+ ///
+ [Description("flv")]
+ FLV,
+
+ ///
+ /// The Matroska Multimedia Container format (.mkv)
+ ///
+ [Description("mkv")]
+ MKV,
+
+ ///
+ /// The QuickTime container format (.mov)
+ ///
+ [Description("mov")]
+ MOV,
+
+ ///
+ /// The MPEG-4 container format (.mp4)
+ ///
+ [Description("mp4")]
+ MP4,
+
+ ///
+ /// The Ogg container format (.ogv extension for video files)
+ ///
+ [Description("ogv")]
+ Ogg,
+
+ ///
+ /// The WebM container format (.webm)
+ ///
+ [Description("webm")]
+ WebM,
+ }
+}
diff --git a/FFMediaToolkit/Encoding/EncoderPreset.cs b/FFMediaToolkit/Encoding/EncoderPreset.cs
new file mode 100644
index 00000000..dbcccdb3
--- /dev/null
+++ b/FFMediaToolkit/Encoding/EncoderPreset.cs
@@ -0,0 +1,66 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System.ComponentModel;
+
+ ///
+ /// The presets for H264 and H265 (HEVC) video encoders.
+ /// Fast presets = faster encoding, worse compression.
+ /// Slow presets = longer encoding, better compression.
+ ///
+ public enum EncoderPreset
+ {
+ ///
+ /// Port of 'ultrafast'
+ ///
+ [Description("ultrafast")]
+ UltraFast = 0,
+
+ ///
+ /// Port of 'superfast'
+ ///
+ [Description("superfast")]
+ SuperFast = 1,
+
+ ///
+ /// Port of 'veryfast'
+ ///
+ [Description("veryfast")]
+ VeryFast = 2,
+
+ ///
+ /// Port of 'faster'
+ ///
+ [Description("faster")]
+ Faster = 3,
+
+ ///
+ /// Port of 'fast'
+ ///
+ [Description("fast")]
+ Fast = 4,
+
+ ///
+ /// The default preset. Port of 'medium'
+ ///
+ [Description("medium")]
+ Medium = 5,
+
+ ///
+ /// Port of 'slow'
+ ///
+ [Description("slow")]
+ Slow = 6,
+
+ ///
+ /// Port of 'slower'
+ ///
+ [Description("slower")]
+ Slower = 7,
+
+ ///
+ /// Port of 'veryslow'
+ ///
+ [Description("veryslow")]
+ VerySlow = 8,
+ }
+}
diff --git a/FFMediaToolkit/Encoding/Internal/OutputContainer.cs b/FFMediaToolkit/Encoding/Internal/OutputContainer.cs
new file mode 100644
index 00000000..26533cdc
--- /dev/null
+++ b/FFMediaToolkit/Encoding/Internal/OutputContainer.cs
@@ -0,0 +1,162 @@
+namespace FFMediaToolkit.Encoding.Internal
+{
+ using System;
+ using System.Collections.Generic;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents the multimedia file container used for encoding.
+ ///
+ internal unsafe class OutputContainer : Wrapper
+ {
+ private OutputContainer(AVFormatContext* formatContext)
+ : base(formatContext)
+ {
+ Video = new List<(OutputStream, VideoEncoderSettings)>();
+ Audio = new List<(OutputStream, AudioEncoderSettings)>();
+ }
+
+ ///
+ /// Gets the video streams.
+ ///
+ public List<(OutputStream stream, VideoEncoderSettings config)> Video { get; }
+
+ ///
+ /// Gets the audio streams.
+ ///
+ public List<(OutputStream stream, AudioEncoderSettings config)> Audio { get; }
+
+ ///
+ /// Gets a value indicating whether the file is created.
+ ///
+ public bool IsFileCreated { get; private set; }
+
+ ///
+ /// Gets a dictionary containing format options.
+ ///
+ internal FFDictionary ContainerOptions { get; private set; } = new FFDictionary();
+
+ ///
+ /// Creates an empty FFmpeg format container for encoding.
+ ///
+ /// A output file extension. It is used only to guess the container format.
+ /// A new instance of the .
+ /// Before you write frames to the container, you must call the method to create an output file.
+ public static OutputContainer Create(string extension)
+ {
+ FFmpegLoader.LoadFFmpeg();
+
+ var format = ffmpeg.av_guess_format(null, "x." + extension, null);
+
+ if (format == null)
+ throw new NotSupportedException($"Cannot find a container format for the \"{extension}\" file extension.");
+
+ var formatContext = ffmpeg.avformat_alloc_context();
+ formatContext->oformat = format;
+ return new OutputContainer(formatContext);
+ }
+
+ ///
+ /// Applies a set of metadata fields to the output file.
+ ///
+ /// The metadata object to set.
+ public void SetMetadata(ContainerMetadata metadata)
+ {
+ foreach (var item in metadata.Metadata)
+ {
+ ffmpeg.av_dict_set(&Pointer->metadata, item.Key, item.Value, 0);
+ }
+ }
+
+ ///
+ /// Adds a new video stream to the container. Usable only in encoding, before locking file.
+ ///
+ /// The stream configuration.
+ public void AddVideoStream(VideoEncoderSettings config)
+ {
+ if (IsFileCreated)
+ {
+ throw new InvalidOperationException("The stream must be added before creating a file.");
+ }
+
+ Video.Add((OutputStreamFactory.CreateVideo(this, config), config));
+ }
+
+ ///
+ /// Adds a new audio stream to the container. Usable only in encoding, before locking file.
+ ///
+ /// The stream configuration.
+ public void AddAudioStream(AudioEncoderSettings config)
+ {
+ if (IsFileCreated)
+ {
+ throw new InvalidOperationException("The stream must be added before creating a file.");
+ }
+
+ Audio.Add((OutputStreamFactory.CreateAudio(this, config), config));
+ }
+
+ ///
+ /// Creates a media file for this container and writes format header into it.
+ ///
+ /// A path to create the file.
+ public void CreateFile(string path)
+ {
+ if (IsFileCreated)
+ {
+ return;
+ }
+
+ if (Video == null)
+ {
+ throw new InvalidOperationException("Cannot create empty media file. You have to add video stream before locking the file");
+ }
+
+ var ptr = ContainerOptions.Pointer;
+
+ ffmpeg.avio_open(&Pointer->pb, path, ffmpeg.AVIO_FLAG_WRITE).ThrowIfError("Cannot create the output file.");
+ ffmpeg.avformat_write_header(Pointer, &ptr);
+
+ IsFileCreated = true;
+ }
+
+ ///
+ /// Writes the specified packet to the container by the method.
+ ///
+ /// The media packet to write.
+ public void WritePacket(MediaPacket packet)
+ {
+ if (!IsFileCreated)
+ {
+ throw new InvalidOperationException("The file must be opened before writing a packet. Use the OutputContainer.CreateFile() method.");
+ }
+
+ ffmpeg.av_interleaved_write_frame(Pointer, packet);
+ }
+
+ ///
+ protected override void OnDisposing()
+ {
+ foreach (var output in Video)
+ {
+ output.stream.Dispose();
+ }
+
+ foreach (var output in Audio)
+ {
+ output.stream.Dispose();
+ }
+
+ if (IsFileCreated)
+ {
+ ffmpeg.av_write_trailer(Pointer);
+ ffmpeg.avio_close(Pointer->pb);
+ }
+
+ ffmpeg.avformat_free_context(Pointer);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/Internal/OutputStreamFactory.cs b/FFMediaToolkit/Encoding/Internal/OutputStreamFactory.cs
new file mode 100644
index 00000000..5ebe29d0
--- /dev/null
+++ b/FFMediaToolkit/Encoding/Internal/OutputStreamFactory.cs
@@ -0,0 +1,143 @@
+namespace FFMediaToolkit.Encoding.Internal
+{
+ using System;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains method for creating media streams.
+ ///
+ internal static unsafe class OutputStreamFactory
+ {
+ ///
+ /// Creates a new video stream for the specified .
+ ///
+ /// The media container.
+ /// The stream settings.
+ /// The new video stream.
+ public static OutputStream CreateVideo(OutputContainer container, VideoEncoderSettings config)
+ {
+ var codecId = config.Codec == VideoCodec.Default ? container.Pointer->oformat->video_codec : (AVCodecID)config.Codec;
+
+ if (codecId == AVCodecID.AV_CODEC_ID_NONE)
+ throw new InvalidOperationException("The media container doesn't support video!");
+
+ var codec = ffmpeg.avcodec_find_encoder(codecId);
+
+ if (codec == null)
+ throw new InvalidOperationException($"Cannot find an encoder with the {codecId}!");
+
+ if (codec->type != AVMediaType.AVMEDIA_TYPE_VIDEO)
+ throw new InvalidOperationException($"The {codecId} encoder doesn't support video!");
+
+ var stream = ffmpeg.avformat_new_stream(container.Pointer, codec);
+ if (stream == null)
+ throw new InvalidOperationException("Cannot allocate AVStream");
+
+ stream->time_base = config.TimeBase;
+ stream->r_frame_rate = config.FramerateRational;
+
+ var codecContext = ffmpeg.avcodec_alloc_context3(codec);
+ if (codecContext == null)
+ throw new InvalidOperationException("Cannot allocate AVCodecContext");
+
+ stream->codecpar->codec_id = codecId;
+ stream->codecpar->codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO;
+ stream->codecpar->width = config.VideoWidth;
+ stream->codecpar->height = config.VideoHeight;
+ stream->codecpar->format = (int)config.VideoFormat;
+ stream->codecpar->bit_rate = config.Bitrate;
+
+ ffmpeg.avcodec_parameters_to_context(codecContext, stream->codecpar).ThrowIfError("Cannot copy stream parameters to encoder");
+ codecContext->time_base = stream->time_base;
+ codecContext->framerate = stream->r_frame_rate;
+ codecContext->gop_size = config.KeyframeRate;
+
+ var dict = new FFDictionary(config.CodecOptions);
+
+ if (config.CRF.HasValue && config.Codec.IsMatch(VideoCodec.H264, VideoCodec.H265, VideoCodec.VP9, VideoCodec.VP8))
+ {
+ dict["crf"] = config.CRF.Value.ToString();
+ }
+
+ if (config.Codec.IsMatch(VideoCodec.H264, VideoCodec.H265))
+ {
+ dict["preset"] = config.EncoderPreset.GetDescription();
+ }
+
+ var ptr = dict.Pointer;
+ ffmpeg.avcodec_open2(codecContext, codec, &ptr).ThrowIfError("Failed to open video encoder.");
+
+ dict.Update(ptr);
+
+ ffmpeg.avcodec_parameters_from_context(stream->codecpar, codecContext).ThrowIfError("Cannot copy encoder parameters to output stream");
+
+ if ((container.Pointer->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
+ {
+ codecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ return new OutputStream(stream, codecContext, container);
+ }
+
+ ///
+ /// Creates a new audio stream for the specified .
+ ///
+ /// The media container.
+ /// The stream settings.
+ /// The new audio stream.
+ public static OutputStream CreateAudio(OutputContainer container, AudioEncoderSettings config)
+ {
+ var codecId = config.Codec == AudioCodec.Default ? container.Pointer->oformat->audio_codec : (AVCodecID)config.Codec;
+
+ if (codecId == AVCodecID.AV_CODEC_ID_NONE)
+ throw new InvalidOperationException("The media container doesn't support audio!");
+
+ var codec = ffmpeg.avcodec_find_encoder(codecId);
+
+ if (codec == null)
+ throw new InvalidOperationException($"Cannot find an encoder with the {codecId}!");
+
+ if (codec->type != AVMediaType.AVMEDIA_TYPE_AUDIO)
+ throw new InvalidOperationException($"The {codecId} encoder doesn't support audio!");
+
+ var stream = ffmpeg.avformat_new_stream(container.Pointer, codec);
+ if (stream == null)
+ throw new InvalidOperationException("Cannot allocate AVStream");
+
+ var codecContext = ffmpeg.avcodec_alloc_context3(codec);
+ if (codecContext == null)
+ throw new InvalidOperationException("Cannot allocate AVCodecContext");
+
+ stream->codecpar->codec_id = codecId;
+ stream->codecpar->codec_type = AVMediaType.AVMEDIA_TYPE_AUDIO;
+ stream->codecpar->sample_rate = config.SampleRate;
+ stream->codecpar->frame_size = config.SamplesPerFrame;
+ stream->codecpar->format = (int)config.SampleFormat;
+
+ ffmpeg.av_channel_layout_default(&stream->codecpar->ch_layout, config.Channels);
+ stream->codecpar->bit_rate = config.Bitrate;
+
+ ffmpeg.avcodec_parameters_to_context(codecContext, stream->codecpar).ThrowIfError("Cannot copy stream parameters to encoder");
+ codecContext->time_base = config.TimeBase;
+
+ var dict = new FFDictionary(config.CodecOptions);
+ var ptr = dict.Pointer;
+
+ ffmpeg.avcodec_open2(codecContext, codec, &ptr).ThrowIfError("Failed to open audio encoder.");
+
+ dict.Update(ptr);
+
+ ffmpeg.avcodec_parameters_from_context(stream->codecpar, codecContext).ThrowIfError("Cannot copy encoder parameters to output stream");
+
+ if ((container.Pointer->oformat->flags & ffmpeg.AVFMT_GLOBALHEADER) != 0)
+ {
+ codecContext->flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ return new OutputStream(stream, codecContext, container);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/Internal/OutputStream{TFrame}.cs b/FFMediaToolkit/Encoding/Internal/OutputStream{TFrame}.cs
new file mode 100644
index 00000000..1677d9cb
--- /dev/null
+++ b/FFMediaToolkit/Encoding/Internal/OutputStream{TFrame}.cs
@@ -0,0 +1,99 @@
+namespace FFMediaToolkit.Encoding.Internal
+{
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a output multimedia stream.
+ ///
+ /// The type of frames in the stream.
+ internal unsafe class OutputStream : Wrapper
+ where TFrame : MediaFrame
+ {
+ private readonly MediaPacket packet;
+ private AVCodecContext* codecContext;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The multimedia stream.
+ /// Codec context.
+ /// The container that owns the stream.
+ public OutputStream(AVStream* stream, AVCodecContext* codec, OutputContainer owner)
+ : base(stream)
+ {
+ OwnerFile = owner;
+ codecContext = codec;
+ packet = MediaPacket.AllocateEmpty();
+ }
+
+ ///
+ /// Gets the media container that owns this stream.
+ ///
+ public OutputContainer OwnerFile { get; }
+
+ ///
+ /// Gets the stream index.
+ ///
+ public int Index => Pointer->index;
+
+ ///
+ /// Gets the stream time base.
+ ///
+ public AVRational TimeBase => Pointer->time_base;
+
+ ///
+ /// Writes the specified frame to this stream.
+ ///
+ /// The media frame.
+ public void Push(TFrame frame)
+ {
+ ffmpeg.avcodec_send_frame(codecContext, frame.Pointer)
+ .ThrowIfError("Cannot send a frame to the encoder.");
+
+ if (ffmpeg.avcodec_receive_packet(codecContext, packet) == 0)
+ {
+ packet.RescaleTimestamp(codecContext->time_base, TimeBase);
+ packet.StreamIndex = Index;
+
+ OwnerFile.WritePacket(packet);
+ }
+
+ packet.Wipe();
+ }
+
+ ///
+ protected override void OnDisposing()
+ {
+ FlushEncoder();
+ packet.Dispose();
+
+ ffmpeg.avcodec_close(codecContext);
+ ffmpeg.av_free(codecContext);
+ codecContext = null;
+ }
+
+ private void FlushEncoder()
+ {
+ while (true)
+ {
+ ffmpeg.avcodec_send_frame(codecContext, null);
+
+ if (ffmpeg.avcodec_receive_packet(codecContext, packet) == 0)
+ {
+ packet.RescaleTimestamp(codecContext->time_base, TimeBase);
+ packet.StreamIndex = Index;
+ OwnerFile.WritePacket(packet);
+ }
+ else
+ {
+ break;
+ }
+
+ packet.Wipe();
+ }
+ }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/MediaBuilder.cs b/FFMediaToolkit/Encoding/MediaBuilder.cs
new file mode 100644
index 00000000..995fdb77
--- /dev/null
+++ b/FFMediaToolkit/Encoding/MediaBuilder.cs
@@ -0,0 +1,105 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System;
+ using System.IO;
+ using FFMediaToolkit.Common;
+ using FFMediaToolkit.Encoding.Internal;
+ using FFMediaToolkit.Helpers;
+
+ ///
+ /// Represents a multimedia file creator.
+ ///
+ public class MediaBuilder
+ {
+ private readonly OutputContainer container;
+ private readonly string outputPath;
+
+ private MediaBuilder(string path, ContainerFormat? format)
+ {
+ if (!Path.IsPathRooted(path))
+ throw new ArgumentException($"The path \"{path}\" is not valid.");
+
+ if (!Path.HasExtension(path) && format == null)
+ throw new ArgumentException("The file path has no extension.");
+
+ container = OutputContainer.Create(format?.GetDescription() ?? Path.GetExtension(path));
+ outputPath = path;
+ }
+
+ ///
+ /// Sets up a multimedia container with the specified .
+ ///
+ /// A path to create the output file.
+ /// A container format.
+ /// The instance.
+ public static MediaBuilder CreateContainer(string path, ContainerFormat format) => new MediaBuilder(path, format);
+
+ ///
+ /// Sets up a multimedia container with the format guessed from the file extension.
+ ///
+ /// A path to create the output file.
+ /// The instance.
+ public static MediaBuilder CreateContainer(string path) => new MediaBuilder(path, null);
+
+ ///
+ /// Applies a custom container option.
+ ///
+ /// The option key.
+ /// The value to set.
+ /// The instance.
+ public MediaBuilder UseFormatOption(string key, string value)
+ {
+ container.ContainerOptions[key] = value;
+ return this;
+ }
+
+ ///
+ /// Applies a set of metadata fields to the output file.
+ ///
+ /// The metadata object to set.
+ /// The instance.
+ public MediaBuilder UseMetadata(ContainerMetadata metadata)
+ {
+ container.SetMetadata(metadata);
+ return this;
+ }
+
+ ///
+ /// Adds a new video stream to the file.
+ ///
+ /// The video stream settings.
+ /// This object.
+ public MediaBuilder WithVideo(VideoEncoderSettings settings)
+ {
+ if (FFmpegLoader.IsFFmpegGplLicensed == false && (settings.Codec == VideoCodec.H264 || settings.Codec == VideoCodec.H265))
+ {
+ throw new NotSupportedException("The LGPL-licensed FFmpeg build does not contain libx264 and libx265 codecs.");
+ }
+
+ container.AddVideoStream(settings);
+ return this;
+ }
+
+ ///
+ /// Adds a new audio stream to the file.
+ ///
+ /// The video stream settings.
+ /// This object.
+ public MediaBuilder WithAudio(AudioEncoderSettings settings)
+ {
+ container.AddAudioStream(settings);
+ return this;
+ }
+
+ ///
+ /// Creates a multimedia file for specified video stream.
+ ///
+ /// A new .
+ public MediaOutput Create()
+ {
+ container.CreateFile(outputPath);
+
+ return new MediaOutput(container);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/MediaOutput.cs b/FFMediaToolkit/Encoding/MediaOutput.cs
new file mode 100644
index 00000000..3fe2647d
--- /dev/null
+++ b/FFMediaToolkit/Encoding/MediaOutput.cs
@@ -0,0 +1,68 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System;
+ using System.Linq;
+ using FFMediaToolkit.Encoding.Internal;
+
+ ///
+ /// Represents a multimedia output file.
+ ///
+ public class MediaOutput : IDisposable
+ {
+ private readonly OutputContainer container;
+ private bool isDisposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object.
+ internal MediaOutput(OutputContainer mediaContainer)
+ {
+ container = mediaContainer;
+
+ VideoStreams = container.Video
+ .Select(o => new VideoOutputStream(o.stream, o.config))
+ .ToArray();
+
+ AudioStreams = container.Audio
+ .Select(o => new AudioOutputStream(o.stream, o.config))
+ .ToArray();
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~MediaOutput() => Dispose();
+
+ ///
+ /// Gets the video streams in the media file.
+ ///
+ public VideoOutputStream[] VideoStreams { get; }
+
+ ///
+ /// Gets the audio streams in the media file.
+ ///
+ public AudioOutputStream[] AudioStreams { get; }
+
+ ///
+ /// Gets the first video stream in the media file.
+ ///
+ public VideoOutputStream Video => VideoStreams.FirstOrDefault();
+
+ ///
+ /// Gets the first audio stream in the media file.
+ ///
+ public AudioOutputStream Audio => AudioStreams.FirstOrDefault();
+
+ ///
+ public void Dispose()
+ {
+ if (isDisposed)
+ return;
+
+ container.Dispose();
+
+ isDisposed = true;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/VideoCodec.cs b/FFMediaToolkit/Encoding/VideoCodec.cs
new file mode 100644
index 00000000..0c711dc1
--- /dev/null
+++ b/FFMediaToolkit/Encoding/VideoCodec.cs
@@ -0,0 +1,101 @@
+namespace FFMediaToolkit.Encoding
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// This enum contains only supported video encoders.
+ /// If you want to use a codec not included to this enum, you can cast to .
+ ///
+ public enum VideoCodec
+ {
+ ///
+ /// Default video codec for the selected container format.
+ ///
+ Default = AVCodecID.AV_CODEC_ID_NONE,
+
+ ///
+ /// H.263 codec
+ ///
+ H263 = AVCodecID.AV_CODEC_ID_H263,
+
+ ///
+ /// H.263-I codec
+ ///
+ H263I = AVCodecID.AV_CODEC_ID_H263I,
+
+ ///
+ /// H.263-P codec
+ ///
+ H263P = AVCodecID.AV_CODEC_ID_H263P,
+
+ ///
+ /// Advanced Video Coding (AVC) - H.264 codec
+ ///
+ H264 = AVCodecID.AV_CODEC_ID_H264,
+
+ ///
+ /// High Efficiency Video Coding (HEVC) - H.265 codec
+ ///
+ H265 = AVCodecID.AV_CODEC_ID_HEVC,
+
+ ///
+ /// Microsoft Windows Media Video 9 (WMV3)
+ ///
+ WMV = AVCodecID.AV_CODEC_ID_WMV3,
+
+ ///
+ /// MPEG-1 video codec.
+ ///
+ MPEG = AVCodecID.AV_CODEC_ID_MPEG1VIDEO,
+
+ ///
+ /// MPEG-2 (H.262) video codec.
+ ///
+ MPEG2 = AVCodecID.AV_CODEC_ID_MPEG2VIDEO,
+
+ ///
+ /// MPEG-4 Part 2 video codec.
+ ///
+ MPEG4 = AVCodecID.AV_CODEC_ID_MPEG4,
+
+ ///
+ /// VP8 codec.
+ ///
+ VP8 = AVCodecID.AV_CODEC_ID_VP8,
+
+ ///
+ /// VP9 codec.
+ ///
+ VP9 = AVCodecID.AV_CODEC_ID_VP9,
+
+ ///
+ /// Theora codec.
+ ///
+ Theora = AVCodecID.AV_CODEC_ID_THEORA,
+
+ ///
+ /// Dirac codec.
+ ///
+ Dirac = AVCodecID.AV_CODEC_ID_DIRAC,
+
+ ///
+ /// Motion JPEG video codec.
+ ///
+ MJPEG = AVCodecID.AV_CODEC_ID_MJPEG,
+
+ ///
+ /// AV1 codec.
+ ///
+ AV1 = AVCodecID.AV_CODEC_ID_AV1,
+
+ ///
+ /// DV codec.
+ ///
+ DV = AVCodecID.AV_CODEC_ID_DVVIDEO,
+
+ ///
+ /// Cinepak codec.
+ ///
+ Cinepak = AVCodecID.AV_CODEC_ID_CINEPAK,
+ }
+}
diff --git a/FFMediaToolkit/Encoding/VideoEncoderSettings.cs b/FFMediaToolkit/Encoding/VideoEncoderSettings.cs
new file mode 100644
index 00000000..e6d88a78
--- /dev/null
+++ b/FFMediaToolkit/Encoding/VideoEncoderSettings.cs
@@ -0,0 +1,96 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System.Collections.Generic;
+
+ using FFMediaToolkit.Graphics;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a video encoder configuration.
+ ///
+ public class VideoEncoderSettings
+ {
+ ///
+ /// Initializes a new instance of the class with default video settings values.
+ ///
+ /// The video frame width.
+ /// The video frame height.
+ /// The video frames per seconds (fps) value.
+ /// The video encoder.
+ public VideoEncoderSettings(int width, int height, int framerate = 30, VideoCodec codec = VideoCodec.Default)
+ {
+ VideoWidth = width;
+ VideoHeight = height;
+ Framerate = framerate;
+ Codec = codec;
+ CodecOptions = new Dictionary();
+ }
+
+ ///
+ /// Gets or sets the video stream bitrate (bytes per second). The default value is 5,000,000 B/s.
+ /// If CRF (for H.264/H.265) is set, this value will be ignored.
+ ///
+ public int Bitrate { get; set; } = 5_000_000;
+
+ ///
+ /// Gets or sets the GoP value. The default value is 12.
+ ///
+ public int KeyframeRate { get; set; } = 12;
+
+ ///
+ /// Gets or sets the video frame width.
+ ///
+ public int VideoWidth { get; set; }
+
+ ///
+ /// Gets or sets the video frame height.
+ ///
+ public int VideoHeight { get; set; }
+
+ ///
+ /// Gets or sets the output video pixel format. The default value is YUV420p.
+ /// Added frames will be automatically converted to this format.
+ ///
+ public ImagePixelFormat VideoFormat { get; set; } = ImagePixelFormat.Yuv420;
+
+ ///
+ /// Gets or sets video frame rate (FPS) value. The default value is 30 frames/s.
+ ///
+ public int Framerate
+ {
+ get => FramerateRational.num / FramerateRational.den;
+ set => FramerateRational = new AVRational { num = value, den = 1 };
+ }
+
+ ///
+ /// Gets or sets the video frame rate as a FFmpeg value. Optional. Overwrites property.
+ ///
+ public AVRational FramerateRational { get; set; }
+
+ ///
+ /// Gets the calculated time base for the video stream. Value is always equal to reciporical of .
+ ///
+ public AVRational TimeBase => new AVRational { num = FramerateRational.den, den = FramerateRational.num };
+
+ ///
+ /// Gets or sets the Constant Rate Factor. It supports only H.264 and H.265 codecs.
+ ///
+ public int? CRF { get; set; }
+
+ ///
+ /// Gets or sets the encoder preset. It supports only H.264 and H.265 codecs.
+ ///
+ public EncoderPreset EncoderPreset { get; set; }
+
+ ///
+ /// Gets or sets the dictionary with custom codec options.
+ ///
+ public Dictionary CodecOptions { get; set; }
+
+ ///
+ /// Gets or sets the codec for this stream.
+ /// If set to , encoder will use default video codec for current container.
+ ///
+ public VideoCodec Codec { get; set; }
+ }
+}
diff --git a/FFMediaToolkit/Encoding/VideoOutputStream.cs b/FFMediaToolkit/Encoding/VideoOutputStream.cs
new file mode 100644
index 00000000..233eee05
--- /dev/null
+++ b/FFMediaToolkit/Encoding/VideoOutputStream.cs
@@ -0,0 +1,93 @@
+namespace FFMediaToolkit.Encoding
+{
+ using System;
+ using System.Drawing;
+ using FFMediaToolkit.Common.Internal;
+ using FFMediaToolkit.Encoding.Internal;
+ using FFMediaToolkit.Graphics;
+ using FFMediaToolkit.Helpers;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents a video encoder stream.
+ ///
+ public class VideoOutputStream : IDisposable
+ {
+ private readonly OutputStream stream;
+ private readonly VideoFrame encodedFrame;
+ private readonly ImageConverter converter;
+
+ private bool isDisposed;
+ private long lastFramePts = -1;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The video stream.
+ /// The stream setting.
+ internal VideoOutputStream(OutputStream stream, VideoEncoderSettings config)
+ {
+ this.stream = stream;
+ Configuration = config;
+
+ var frameSize = new Size(config.VideoWidth, config.VideoHeight);
+ encodedFrame = VideoFrame.Create(frameSize, (AVPixelFormat)config.VideoFormat);
+ converter = new ImageConverter(frameSize, (AVPixelFormat)config.VideoFormat);
+ }
+
+ ///
+ /// Gets the video encoding configuration used to create this stream.
+ ///
+ public VideoEncoderSettings Configuration { get; }
+
+ ///
+ /// Gets the current duration of this stream.
+ ///
+ public TimeSpan CurrentDuration => lastFramePts.ToTimeSpan(Configuration.TimeBase);
+
+ ///
+ /// Writes the specified bitmap to the video stream as the next frame.
+ ///
+ /// The bitmap to write.
+ /// (optional) custom PTS value for the frame.
+ public void AddFrame(ImageData frame, long customPtsValue)
+ {
+ if (customPtsValue <= lastFramePts)
+ throw new Exception("Cannot add a frame that occurs chronologically before the most recently written frame!");
+
+ encodedFrame.UpdateFromBitmap(frame, converter);
+ encodedFrame.PresentationTimestamp = customPtsValue;
+ stream.Push(encodedFrame);
+
+ lastFramePts = customPtsValue;
+ }
+
+ ///
+ /// Writes the specified bitmap to the video stream as the next frame.
+ ///
+ /// The bitmap to write.
+ /// Custom timestamp for this frame.
+ public void AddFrame(ImageData frame, TimeSpan customTime) => AddFrame(frame, customTime.ToTimestamp(Configuration.TimeBase));
+
+ ///
+ /// Writes the specified bitmap to the video stream as the next frame.
+ ///
+ /// The bitmap to write.
+ public void AddFrame(ImageData frame) => AddFrame(frame, lastFramePts + 1);
+
+ ///
+ public void Dispose()
+ {
+ if (isDisposed)
+ {
+ return;
+ }
+
+ stream.Dispose();
+ encodedFrame.Dispose();
+ converter.Dispose();
+
+ isDisposed = true;
+ }
+ }
+}
diff --git a/FFMediaToolkit/FFMediaToolkit.csproj b/FFMediaToolkit/FFMediaToolkit.csproj
new file mode 100644
index 00000000..d27d2f00
--- /dev/null
+++ b/FFMediaToolkit/FFMediaToolkit.csproj
@@ -0,0 +1,50 @@
+
+
+
+ netstandard2.0;netstandard2.1
+ true
+ true
+ Radosław Kmiotek
+ radek-k
+ Copyright (c) 2019-2023 Radosław Kmiotek
+ Cross-platform audio/video processing library based on FFmpeg native libraries. Supports audio/video frames extraction (fast access to any frame by timestamp), reading file metadata and encoding media files from bitmap images and audio data.
+ ffmpeg;video;audio;encoder;encoding;decoder;decoding;h264;mp4;c#;netstandard;netcore;frame-extraction
+ https://github.com/radek-k/FFMediaToolkit
+ https://github.com/radek-k/FFMediaToolkit
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+ true
+ true
+ MIT
+ true
+ true
+ snupkg
+ 11
+ Debug;Release
+ x64
+
+
+
+ v
+ normal
+ 4.3
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/FFMediaToolkit/FFMediaToolkit.ruleset b/FFMediaToolkit/FFMediaToolkit.ruleset
new file mode 100644
index 00000000..718dc217
--- /dev/null
+++ b/FFMediaToolkit/FFMediaToolkit.ruleset
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FFMediaToolkit/FFmpegException.cs b/FFMediaToolkit/FFmpegException.cs
new file mode 100644
index 00000000..14584496
--- /dev/null
+++ b/FFMediaToolkit/FFmpegException.cs
@@ -0,0 +1,52 @@
+namespace FFMediaToolkit
+{
+ using System;
+ using FFMediaToolkit.Helpers;
+
+ ///
+ /// Represents an exception thrown when FFMpeg method call returns an error code.
+ ///
+ [Serializable]
+ public class FFmpegException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FFmpegException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class using a message and a error code.
+ ///
+ /// The exception message.
+ public FFmpegException(string message)
+ : base(message)
+ => ErrorMessage = string.Empty;
+
+ ///
+ /// Initializes a new instance of the class using a message and a error code.
+ ///
+ /// The exception message.
+ /// The error code returned by the FFmpeg method.
+ public FFmpegException(string message, int errorCode)
+ : base(CreateMessage(message, errorCode))
+ {
+ ErrorCode = errorCode;
+ ErrorMessage = StringConverter.DecodeMessage(errorCode);
+ }
+
+ ///
+ /// Gets the error code returned by the FFmpeg method.
+ ///
+ public int? ErrorCode { get; }
+
+ ///
+ /// Gets the message text decoded from error code.
+ ///
+ public string ErrorMessage { get; }
+
+ private static string CreateMessage(string msg, int errCode)
+ => $"{msg} Error code: {errCode} : {StringConverter.DecodeMessage(errCode)}";
+ }
+}
diff --git a/FFMediaToolkit/FFmpegLoader.cs b/FFMediaToolkit/FFmpegLoader.cs
new file mode 100644
index 00000000..41e5b7b3
--- /dev/null
+++ b/FFMediaToolkit/FFmpegLoader.cs
@@ -0,0 +1,178 @@
+namespace FFMediaToolkit
+{
+ using System;
+ using System.IO;
+ using System.Runtime.InteropServices;
+ using FFMediaToolkit.Interop;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains methods for managing FFmpeg libraries.
+ ///
+ public static class FFmpegLoader
+ {
+ private static LogLevel logLevel = LogLevel.Error;
+ private static bool isPathSet;
+
+ ///
+ /// Delegate for log message callback.
+ ///
+ /// The message.
+ public delegate void LogCallbackDelegate(string message);
+
+ ///
+ /// Log message callback event.
+ ///
+ public static event LogCallbackDelegate LogCallback;
+
+ ///
+ /// Gets or sets the verbosity level of FFMpeg logs printed to standard error/output.
+ /// Default value is .
+ ///
+ public static LogLevel LogVerbosity
+ {
+ get => logLevel;
+ set
+ {
+ if (IsFFmpegLoaded)
+ {
+ ffmpeg.av_log_set_level((int)value);
+ }
+
+ logLevel = value;
+ }
+ }
+
+ ///
+ /// Gets or sets path to the directory containing FFmpeg binaries.
+ ///
+ /// Thrown when FFmpeg was already loaded.
+ /// Thrown when specified directory does not exist.
+ public static string FFmpegPath
+ {
+ get => ffmpeg.RootPath ?? string.Empty;
+ set
+ {
+ if (IsFFmpegLoaded)
+ {
+ throw new InvalidOperationException("FFmpeg libraries were already loaded!");
+ }
+
+ if (!Directory.Exists(value))
+ {
+ throw new DirectoryNotFoundException("The specified FFmpeg directory does not exist!");
+ }
+
+ ffmpeg.RootPath = value;
+ isPathSet = true;
+ }
+ }
+
+ ///
+ /// Gets the FFmpeg version info string.
+ /// Empty when FFmpeg libraries were not yet loaded.
+ ///
+ public static string FFmpegVersion { get; private set; } = string.Empty;
+
+ ///
+ /// Gets a value indicating whether the loaded FFmpeg binary files are licensed under the GPL.
+ /// Null when FFmpeg libraries were not yet loaded.
+ ///
+ public static bool? IsFFmpegGplLicensed { get; private set; }
+
+ ///
+ /// Gets the FFmpeg license text
+ /// Empty when FFmpeg libraries were not yet loaded.
+ ///
+ public static string FFmpegLicense { get; private set; } = string.Empty;
+
+ ///
+ /// Gets a value indicating whether the FFmpeg binary files were successfully loaded.
+ ///
+ internal static bool IsFFmpegLoaded { get; private set; }
+
+ ///
+ /// Manually loads FFmpeg libraries from the specified (or the default path for current platform if not set).
+ /// If you will not call this method, FFmpeg will be loaded while opening or creating a video file.
+ ///
+ ///
+ /// Thrown when default FFmpeg directory does not exist.
+ /// On Windows you have to specify a path to a directory containing the FFmpeg shared build DLL files.
+ ///
+ ///
+ /// Thrown when required FFmpeg libraries do not exist or when you try to load 64bit binaries from 32bit application process.
+ ///
+ public static void LoadFFmpeg()
+ {
+ if (IsFFmpegLoaded)
+ {
+ return;
+ }
+
+ if (!isPathSet)
+ {
+ try
+ {
+ FFmpegPath = NativeMethods.GetFFmpegDirectory();
+ }
+ catch (DirectoryNotFoundException)
+ {
+ throw new DirectoryNotFoundException("Cannot found the default FFmpeg directory.\n" +
+ "On Windows you have to set \"FFmpegLoader.FFmpegPath\" with full path to the directory containing FFmpeg 5.x shared build \".dll\" files\n" +
+ "For more informations please see https://github.com/radek-k/FFMediaToolkit#setup");
+ }
+ }
+
+ try
+ {
+ FFmpegVersion = ffmpeg.av_version_info();
+ FFmpegLicense = ffmpeg.avcodec_license();
+ IsFFmpegGplLicensed = FFmpegLicense.StartsWith("GPL");
+ }
+ catch (DllNotFoundException ex)
+ {
+ HandleLibraryLoadError(ex);
+ }
+ catch (NotSupportedException ex)
+ {
+ HandleLibraryLoadError(ex);
+ }
+
+ IsFFmpegLoaded = true;
+ LogVerbosity = logLevel;
+ }
+
+ ///
+ /// Start logging ffmpeg output.
+ ///
+ public static unsafe void SetupLogging()
+ {
+ ffmpeg.av_log_set_level(ffmpeg.AV_LOG_VERBOSE);
+
+ // do not convert to local function
+ av_log_set_callback_callback logCallback = (p0, level, format, vl) =>
+ {
+ if (level > ffmpeg.av_log_get_level())
+ return;
+
+ var lineSize = 1024;
+ var lineBuffer = stackalloc byte[lineSize];
+ var printPrefix = 1;
+ ffmpeg.av_log_format_line(p0, level, format, vl, lineBuffer, lineSize, &printPrefix);
+ var line = Marshal.PtrToStringAnsi((IntPtr)lineBuffer);
+ LogCallback?.Invoke(line);
+ };
+
+ ffmpeg.av_log_set_callback(logCallback);
+ }
+
+ ///
+ /// Throws a FFmpeg library loading exception.
+ ///
+ /// The original exception.
+ internal static void HandleLibraryLoadError(Exception exception)
+ {
+ throw new DllNotFoundException($"Cannot load FFmpeg libraries from {FFmpegPath} directory.\nRequired FFmpeg version: 6.x (shared build)\nMake sure the \"Build\"Prefer 32-bit\" option in the project settings is turned off.\nFor more information please see https://github.com/radek-k/FFMediaToolkit#setup", exception);
+ }
+ }
+}
diff --git a/FFMediaToolkit/Graphics/ImageData.cs b/FFMediaToolkit/Graphics/ImageData.cs
new file mode 100644
index 00000000..45a1c176
--- /dev/null
+++ b/FFMediaToolkit/Graphics/ImageData.cs
@@ -0,0 +1,188 @@
+namespace FFMediaToolkit.Graphics
+{
+ using System;
+ using System.Buffers;
+ using System.Drawing;
+
+ ///
+ /// Represent a lightweight container for bitmap images.
+ ///
+ public ref struct ImageData
+ {
+ private readonly Span span;
+ private readonly IMemoryOwner pooledMemory;
+
+ ///
+ /// Initializes a new instance of the struct using a as the data source.
+ ///
+ /// The bitmap data.
+ /// The pixel format.
+ /// The image dimensions.
+ /// When data span size doesn't match size calculated from width, height and the pixel format.
+ public ImageData(Span data, ImagePixelFormat pixelFormat, Size imageSize)
+ {
+ var size = EstimateStride(imageSize.Width, pixelFormat) * imageSize.Height;
+ if (data.Length < size)
+ {
+ throw new ArgumentException("Pixel buffer size doesn't match size required by this image format.");
+ }
+
+ span = data;
+ pooledMemory = null;
+
+ ImageSize = imageSize;
+ PixelFormat = pixelFormat;
+ }
+
+ ///
+ /// Initializes a new instance of the struct using a as the data source.
+ ///
+ /// The bitmap data.
+ /// The pixel format.
+ /// The image width.
+ /// The image height.
+ /// When data span size doesn't match size calculated from width, height and the pixel format.
+ public ImageData(Span data, ImagePixelFormat pixelFormat, int width, int height)
+ : this(data, pixelFormat, new Size(width, height))
+ {
+ }
+
+ private ImageData(IMemoryOwner memory, Size size, ImagePixelFormat pixelFormat)
+ {
+ span = null;
+ pooledMemory = memory;
+
+ ImageSize = size;
+ PixelFormat = pixelFormat;
+ }
+
+ ///
+ /// Gets the object containing the bitmap data.
+ ///
+ public Span Data => IsPooled ? pooledMemory.Memory.Span : span;
+
+ ///
+ /// Gets a value indicating whether this instance of uses memory pooling.
+ ///
+ public bool IsPooled => pooledMemory != null;
+
+ ///
+ /// Gets the image size.
+ ///
+ public Size ImageSize { get; }
+
+ ///
+ /// Gets the bitmap pixel format.
+ ///
+ public ImagePixelFormat PixelFormat { get; }
+
+ ///
+ /// Gets the estimated number of bytes in one row of image pixels.
+ ///
+ public int Stride => EstimateStride(ImageSize.Width, PixelFormat);
+
+ ///
+ /// Rents a memory buffer from pool and creates a new instance of class from it.
+ ///
+ /// The image dimensions.
+ /// The bitmap pixel format.
+ /// The new instance.
+ public static ImageData CreatePooled(Size imageSize, ImagePixelFormat pixelFormat)
+ {
+ var size = EstimateStride(imageSize.Width, pixelFormat) * imageSize.Height;
+ var pool = MemoryPool.Shared;
+ var memory = pool.Rent(size);
+ return new ImageData(memory, imageSize, pixelFormat);
+ }
+
+ ///
+ /// Creates a new instance of the class using a byte array as the data source.
+ ///
+ /// The byte array containing bitmap data.
+ /// The bitmap pixel format.
+ /// The image dimensions.
+ /// A new instance.
+ public static ImageData FromArray(byte[] pixels, ImagePixelFormat pixelFormat, Size imageSize)
+ => new ImageData(new Span(pixels), pixelFormat, imageSize);
+
+ ///
+ /// Creates a new instance of the class using a byte array as the data source.
+ ///
+ /// The byte array containing bitmap data.
+ /// The bitmap pixel format.
+ /// The image width.
+ /// The image height.
+ /// A new instance.
+ public static ImageData FromArray(byte[] pixels, ImagePixelFormat pixelFormat, int width, int height)
+ => FromArray(pixels, pixelFormat, new Size(width, height));
+
+ ///
+ /// Creates a new instance of the class using a pointer to the unmanaged memory as the data source.
+ ///
+ /// The byte array containing bitmap data.
+ /// The bitmap pixel format.
+ /// The image dimensions.
+ /// A new instance.
+ public static ImageData FromPointer(IntPtr pointer, ImagePixelFormat pixelFormat, Size imageSize)
+ {
+ var span = CreateSpan(pointer, imageSize, pixelFormat);
+ return new ImageData(span, pixelFormat, imageSize);
+ }
+
+ ///
+ /// Creates a new instance of the class using a pointer to the unmanaged memory as the data source.
+ ///
+ /// The byte array containing bitmap data.
+ /// The bitmap pixel format.
+ /// The image width.
+ /// The image height.
+ /// A new instance.
+ public static ImageData FromPointer(IntPtr pointer, ImagePixelFormat pixelFormat, int width, int height)
+ => FromPointer(pointer, pixelFormat, new Size(width, height));
+
+ ///
+ /// Gets the estimated image line size based on the pixel format and width.
+ ///
+ /// The image width.
+ /// The image pixel format.
+ /// The size of a single line of the image measured in bytes.
+ public static int EstimateStride(int width, ImagePixelFormat format) => 4 * (((GetBitsPerPixel(format) * width) + 31) / 32);
+
+ private static unsafe Span CreateSpan(IntPtr pointer, Size imageSize, ImagePixelFormat pixelFormat)
+ {
+ var size = EstimateStride(imageSize.Width, pixelFormat) * imageSize.Height;
+ return new Span((void*)pointer, size);
+ }
+
+ private static int GetBitsPerPixel(ImagePixelFormat format)
+ {
+ switch (format)
+ {
+ case ImagePixelFormat.Bgr24:
+ return 24;
+ case ImagePixelFormat.Bgra32:
+ return 32;
+ case ImagePixelFormat.Rgb24:
+ return 24;
+ case ImagePixelFormat.Rgba32:
+ return 32;
+ case ImagePixelFormat.Argb32:
+ return 32;
+ case ImagePixelFormat.Uyvy422:
+ return 16;
+ case ImagePixelFormat.Yuv420:
+ return 12;
+ case ImagePixelFormat.Yuv422:
+ return 16;
+ case ImagePixelFormat.Yuv444:
+ return 24;
+ case ImagePixelFormat.Gray16:
+ return 16;
+ case ImagePixelFormat.Gray8:
+ return 8;
+ default:
+ return 0;
+ }
+ }
+ }
+}
diff --git a/FFMediaToolkit/Graphics/ImagePixelFormat.cs b/FFMediaToolkit/Graphics/ImagePixelFormat.cs
new file mode 100644
index 00000000..38b096bf
--- /dev/null
+++ b/FFMediaToolkit/Graphics/ImagePixelFormat.cs
@@ -0,0 +1,65 @@
+namespace FFMediaToolkit.Graphics
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Represents the most used image pixel formats. Partially compatible with .
+ ///
+ public enum ImagePixelFormat
+ {
+ ///
+ /// Represents a BGR 24bpp bitmap pixel format. Used by default in GDI+ and WPF graphics.
+ ///
+ Bgr24 = AVPixelFormat.AV_PIX_FMT_BGR24,
+
+ ///
+ /// Represents a BGRA(with alpha channel) 32bpp bitmap pixel format.
+ ///
+ Bgra32 = AVPixelFormat.AV_PIX_FMT_BGRA,
+
+ ///
+ /// Represents a RGB 24bpp bitmap pixel format.
+ ///
+ Rgb24 = AVPixelFormat.AV_PIX_FMT_RGB24,
+
+ ///
+ /// Represents a RGBA(with alpha channel) 32bpp bitmap pixel format.
+ ///
+ Rgba32 = AVPixelFormat.AV_PIX_FMT_RGBA,
+
+ ///
+ /// Represents a ARGB(with alpha channel) 32bpp bitmap pixel format.
+ ///
+ Argb32 = AVPixelFormat.AV_PIX_FMT_ARGB,
+
+ ///
+ /// Represents a UYVY422 pixel format.
+ ///
+ Uyvy422 = AVPixelFormat.AV_PIX_FMT_UYVY422,
+
+ ///
+ /// Represents a YUV 24bpp 4:4:4 video pixel format.
+ ///
+ Yuv444 = AVPixelFormat.AV_PIX_FMT_YUV444P,
+
+ ///
+ /// Represents a YUV 16bpp 4:2:2 video pixel format.
+ ///
+ Yuv422 = AVPixelFormat.AV_PIX_FMT_YUV422P,
+
+ ///
+ /// Represents a YUV 12bpp 4:2:0 video pixel format.
+ ///
+ Yuv420 = AVPixelFormat.AV_PIX_FMT_YUV420P,
+
+ ///
+ /// Represents a Gray 16bpp little-endian video pixel format.
+ ///
+ Gray16 = AVPixelFormat.AV_PIX_FMT_GRAY16LE,
+
+ ///
+ /// Represents a Gray 8bpp video pixel format.
+ ///
+ Gray8 = AVPixelFormat.AV_PIX_FMT_GRAY8,
+ }
+}
diff --git a/FFMediaToolkit/Helpers/ExceptionHandler.cs b/FFMediaToolkit/Helpers/ExceptionHandler.cs
new file mode 100644
index 00000000..621955fe
--- /dev/null
+++ b/FFMediaToolkit/Helpers/ExceptionHandler.cs
@@ -0,0 +1,58 @@
+namespace FFMediaToolkit.Helpers
+{
+ using System.Runtime.CompilerServices;
+
+ ///
+ /// Contains common methods for handling FFMpeg exceptions.
+ ///
+ internal static class ExceptionHandler
+ {
+ ///
+ /// A delegate for error code handling.
+ ///
+ /// The error code.
+ internal delegate void ErrorHandler(int errorCode);
+
+ ///
+ /// Checks if specified integer is error code and throws an .
+ ///
+ /// The exit code returned by a method.
+ /// The exception message.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void ThrowIfError(this int errorCode, string exceptionMessage)
+ {
+ if (errorCode < 0)
+ {
+ throw new FFmpegException(exceptionMessage, errorCode);
+ }
+ }
+
+ ///
+ /// Checks if the integer is equal to the specified and executes the method.
+ ///
+ /// The exit code returned by a method.
+ /// The error code to handle.
+ /// The method to execute if error handled.
+ /// If this method after handling exception will return 0 instead of the original code.
+ /// Original error code or 0 if error handled and the is .
+ internal static int IfError(this int errorCode, int handledError, ErrorHandler action, bool handles = true)
+ {
+ if (errorCode == handledError)
+ {
+ action(errorCode);
+ }
+
+ return handles ? 0 : errorCode;
+ }
+
+ ///
+ /// Checks if the integer is equal to the and throws an .
+ ///
+ /// The exit code returned by a method.
+ /// The error code to handle.
+ /// The exception message.
+ /// The original error code.
+ internal static int IfError(this int errorCode, int handledError, string exceptionMessage)
+ => errorCode.IfError(handledError, x => throw new FFmpegException(exceptionMessage, x));
+ }
+}
diff --git a/FFMediaToolkit/Helpers/Extensions.cs b/FFMediaToolkit/Helpers/Extensions.cs
new file mode 100644
index 00000000..a6b1f638
--- /dev/null
+++ b/FFMediaToolkit/Helpers/Extensions.cs
@@ -0,0 +1,74 @@
+namespace FFMediaToolkit.Helpers
+{
+ using System;
+ using System.ComponentModel;
+ using FFMediaToolkit.Common;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains extension methods.
+ ///
+ internal static class Extensions
+ {
+ ///
+ /// Gets the value of the specified enumeration value.
+ ///
+ /// The enum value.
+ /// The description attribute string of this enum value.
+ public static string GetDescription(this Enum value)
+ {
+ var field = value.GetType().GetField(value.ToString());
+ return Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute
+ ? attribute.Description : value.ToString();
+ }
+
+ ///
+ /// Checks if this object is equal to at least one of specified objects.
+ ///
+ /// Type of the objects.
+ /// This object.
+ /// Objects to check.
+ /// is the object is equal to at least one of specified objects.
+ public static bool IsMatch(this T value, params T[] valueToCompare)
+ where T : struct, Enum
+ {
+ foreach (T x in valueToCompare)
+ {
+ if (value.Equals(x))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Normalizes this enumeration value - makes it lowercase and trims the specified amount of chars.
+ ///
+ /// The enumeration value to format.
+ /// Number of chars to trim.
+ /// The normalized string.
+ internal static string FormatEnum(this Enum value, int charsToTrim) => value.ToString().Substring(charsToTrim).ToLower();
+
+ ///
+ /// Gets the type of content in the .
+ ///
+ /// The .
+ /// The type of frame content.
+ internal static MediaType GetMediaType(this AVFrame frame)
+ {
+ if (frame.width > 0 && frame.height > 0)
+ {
+ return MediaType.Video;
+ }
+
+ if (frame.ch_layout.nb_channels > 0)
+ {
+ return MediaType.Audio;
+ }
+
+ return MediaType.None;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Helpers/MathHelper.cs b/FFMediaToolkit/Helpers/MathHelper.cs
new file mode 100644
index 00000000..e235bbac
--- /dev/null
+++ b/FFMediaToolkit/Helpers/MathHelper.cs
@@ -0,0 +1,95 @@
+namespace FFMediaToolkit.Helpers
+{
+ using System;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains extension methods for math types.
+ ///
+ internal static class MathHelper
+ {
+ ///
+ /// Converts a rational number to a double.
+ ///
+ /// The to convert.
+ /// The value.
+ public static double ToDouble(this AVRational rational)
+ => rational.den == 0 ? 0 : Convert.ToDouble(rational.num) / Convert.ToDouble(rational.den);
+
+ ///
+ /// Converts the given in the units to a object.
+ ///
+ /// The timestamp.
+ /// The time base unit.
+ /// The converted .
+ public static TimeSpan ToTimeSpan(this long timestamp, AVRational timeBase)
+ {
+ var ts = Convert.ToDouble(timestamp);
+ var tb = timeBase.ToDouble();
+
+ return TimeSpan.FromMilliseconds(ts * tb * 1000);
+ }
+
+ ///
+ /// Converts the frame number to a .
+ ///
+ /// The frame number.
+ /// The stream frame rate.
+ /// The converted .
+ public static TimeSpan ToTimeSpan(this int frameNumber, double fps) => TimeSpan.FromMilliseconds(frameNumber * (1000 / fps));
+
+ ///
+ /// Converts this to a frame number based on the specified frame rate/>.
+ ///
+ /// The time.
+ /// The stream frame rate.
+ /// The frame number.
+ public static int ToFrameNumber(this TimeSpan time, AVRational framerate)
+ => (int)(time.TotalSeconds * framerate.num / framerate.den);
+
+ ///
+ /// Converts a frame index to a timestamp in the units.
+ ///
+ /// The frame number.
+ /// The stream frame rate.
+ /// The stream time base.
+ /// The timestamp.
+ public static long ToTimestamp(this int frameNumber, AVRational fps, AVRational timeBase)
+ {
+ long num = frameNumber * fps.den * timeBase.den;
+ long den = fps.num * timeBase.num;
+ return Convert.ToInt64(num / (double)den);
+ }
+
+ ///
+ /// Converts the to a timestamp in the units.
+ ///
+ /// The time.
+ /// The stream time base.
+ /// The timestamp.
+ public static long ToTimestamp(this TimeSpan time, AVRational timeBase)
+ => Convert.ToInt64(time.TotalSeconds * timeBase.den / timeBase.num);
+
+ ///
+ /// Clamps the specified number between min and max values.
+ ///
+ /// The value to clamp.
+ /// The minimum value.
+ /// The maximum value.
+ /// The clamped value.
+ public static int Clamp(this int number, int min, int max)
+ {
+ if (number < min)
+ {
+ return min;
+ }
+
+ if (number > max)
+ {
+ return max;
+ }
+
+ return number;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Helpers/StringConverter.cs b/FFMediaToolkit/Helpers/StringConverter.cs
new file mode 100644
index 00000000..ad058600
--- /dev/null
+++ b/FFMediaToolkit/Helpers/StringConverter.cs
@@ -0,0 +1,48 @@
+namespace FFMediaToolkit.Helpers
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using System.Text;
+ using FFmpeg.AutoGen;
+
+ ///
+ /// Contains string conversion methods.
+ ///
+ internal static class StringConverter
+ {
+ ///
+ /// Creates a new from a pointer to the unmanaged UTF-8 string.
+ ///
+ /// A pointer to the unmanaged string.
+ /// The converted string.
+ public static string Utf8ToString(this IntPtr pointer)
+ {
+ var lenght = 0;
+
+ while (Marshal.ReadByte(pointer, lenght) != 0)
+ {
+ ++lenght;
+ }
+
+ var buffer = new byte[lenght];
+ Marshal.Copy(pointer, buffer, 0, lenght);
+
+ return Encoding.UTF8.GetString(buffer);
+ }
+
+ ///
+ /// Gets the FFmpeg error message based on the error code.
+ ///
+ /// The error code.
+ /// The decoded error message.
+ public static unsafe string DecodeMessage(int errorCode)
+ {
+ const int bufferSize = 1024;
+ var buffer = stackalloc byte[bufferSize];
+ ffmpeg.av_strerror(errorCode, buffer, bufferSize);
+
+ var message = new IntPtr(buffer).Utf8ToString();
+ return message;
+ }
+ }
+}
diff --git a/FFMediaToolkit/Interop/NativeMethods.cs b/FFMediaToolkit/Interop/NativeMethods.cs
new file mode 100644
index 00000000..fb8ee036
--- /dev/null
+++ b/FFMediaToolkit/Interop/NativeMethods.cs
@@ -0,0 +1,41 @@
+namespace FFMediaToolkit.Interop
+{
+ using System;
+ using System.Runtime.InteropServices;
+
+ ///
+ /// Contains the native operating system methods.
+ ///
+ internal static class NativeMethods
+ {
+ private static string MacOSDefautDirectory => "/opt/local/lib/";
+
+ private static string LinuxDefaultDirectory => $"/usr/lib/{(Environment.Is64BitOperatingSystem ? "x86_64" : "x86")}-linux-gnu";
+
+ private static string WindowsDefaultDirectory => $@"\runtimes\{(Environment.Is64BitProcess ? "win-x64" : "win-x86")}\native";
+
+ ///
+ /// Gets the default FFmpeg directory for current platform.
+ ///
+ /// A path to the default directory for FFmpeg libraries.
+ internal static string GetFFmpegDirectory()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return Environment.CurrentDirectory + WindowsDefaultDirectory;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ return LinuxDefaultDirectory;
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ return MacOSDefautDirectory;
+ }
+ else
+ {
+ throw new PlatformNotSupportedException("This OS is not supported by the FFMediaToolkit");
+ }
+ }
+ }
+}
diff --git a/FFMediaToolkit/LogLevel.cs b/FFMediaToolkit/LogLevel.cs
new file mode 100644
index 00000000..c1914e55
--- /dev/null
+++ b/FFMediaToolkit/LogLevel.cs
@@ -0,0 +1,35 @@
+namespace FFMediaToolkit
+{
+ using FFmpeg.AutoGen;
+
+ ///
+ /// FFMpeg logging verbosity levels.
+ ///
+ public enum LogLevel
+ {
+ ///
+ /// Doesn't print any messages.
+ ///
+ Quiet = ffmpeg.AV_LOG_QUIET,
+
+ ///
+ /// Prints only error messages.
+ ///
+ Error = ffmpeg.AV_LOG_ERROR,
+
+ ///
+ /// Prints error and warning messages.
+ ///
+ Warning = ffmpeg.AV_LOG_WARNING,
+
+ ///
+ /// Prints errors, warnings and informational messages.
+ ///
+ Info = ffmpeg.AV_LOG_INFO,
+
+ ///
+ /// Prints the most detailed messages.
+ ///
+ Verbose = ffmpeg.AV_LOG_VERBOSE,
+ }
+}
diff --git a/MusicX.Core/Helpers/GithubSource.cs b/MusicX.Core/Helpers/GithubSource.cs
deleted file mode 100644
index 1968eb79..00000000
--- a/MusicX.Core/Helpers/GithubSource.cs
+++ /dev/null
@@ -1,278 +0,0 @@
-using System.Collections.Immutable;
-using System.Runtime.Serialization;
-using System.Text;
-using Newtonsoft.Json;
-using NuGet.Versioning;
-using Squirrel;
-using Squirrel.Sources;
-
-namespace MusicX.Core.Helpers;
-
-/// Describes a GitHub release, including attached assets.
-[DataContract]
-public class GithubRelease
-{
- /// The name of this release.
- [DataMember(Name = "name")]
- public string Name { get; set; }
-
- /// True if this release is a prerelease.
- [DataMember(Name = "prerelease")]
- public bool Prerelease { get; set; }
-
- /// The date which this release was published publically.
- [DataMember(Name = "published_at")]
- public DateTime PublishedAt { get; set; }
-
- /// A list of assets (files) uploaded to this release.
- [DataMember(Name = "assets")]
- public GithubReleaseAsset[] Assets { get; set; }
-
- /// The name of tag tio which the release is created for.
- [DataMember(Name = "tag_name")]
- public string TagName { get; set; }
-}
-
-/// Describes a asset (file) uploaded to a GitHub release.
-[DataContract]
-public class GithubReleaseAsset
-{
- ///
- /// The asset URL for this release asset. Requests to this URL will use API
- /// quota and return JSON unless the 'Accept' header is "application/octet-stream".
- ///
- [DataMember(Name = "url")]
- public string Url { get; set; }
-
- ///
- /// The browser URL for this release asset. This does not use API quota,
- /// however this URL only works for public repositories. If downloading
- /// assets from a private repository, the property must
- /// be used with an appropriate access token.
- ///
- [DataMember(Name = "browser_download_url")]
- public string BrowserDownloadUrl { get; set; }
-
- /// The name of this release asset.
- [DataMember(Name = "name")]
- public string Name { get; set; }
-
- /// The mime type of this release asset (as detected by GitHub).
- [DataMember(Name = "content_type")]
- public string ContentType { get; set; }
-}
-
-///
-/// Retrieves available releases from a GitHub repository. This class only
-/// downloads assets from the very latest GitHub release.
-///
-public class GithubSource : IUpdateSource
-{
- ///
- /// The URL of the GitHub repository to download releases from
- /// (e.g. https://github.com/myuser/myrepo)
- ///
- public virtual Uri RepoUri { get; }
-
- ///
- /// If true, the latest pre-release will be downloaded. If false, the latest
- /// stable release will be downloaded.
- ///
- public virtual bool Prerelease { get; }
-
- ///
- /// The file downloader used to perform HTTP requests.
- ///
- public virtual IFileDownloader Downloader { get; }
-
- ///
- /// The GitHub releases which this class should download assets from when
- /// executing . This property can be set
- /// explicitly, otherwise it will also be set automatically when executing
- /// .
- ///
- public virtual ImmutableSortedDictionary Releases { get; set; }
-
- ///
- /// The GitHub access token to use with the request to download releases.
- /// If left empty, the GitHub rate limit for unauthenticated requests allows
- /// for up to 60 requests per hour, limited by IP address.
- ///
- protected virtual string AccessToken { get; }
-
- /// The Bearer token used in the request.
- protected virtual string? Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "Bearer " + AccessToken;
-
- ///
- ///
- /// The URL of the GitHub repository to download releases from
- /// (e.g. https://github.com/myuser/myrepo)
- ///
- ///
- /// The GitHub access token to use with the request to download releases.
- /// If left empty, the GitHub rate limit for unauthenticated requests allows
- /// for up to 60 requests per hour, limited by IP address.
- ///
- ///
- /// If true, the latest pre-release will be downloaded. If false, the latest
- /// stable release will be downloaded.
- ///
- ///
- /// The file downloader used to perform HTTP requests.
- ///
- public GithubSource(string repoUrl, string accessToken, bool prerelease, IFileDownloader downloader)
- {
- RepoUri = new(repoUrl);
- AccessToken = accessToken;
- Prerelease = prerelease;
- Downloader = downloader;
- }
-
- ///
- public virtual async Task GetReleaseFeed(Guid? stagingId = null,
- ReleaseEntry? latestLocalRelease = null)
- {
- var releases = await GetReleases(Prerelease).ConfigureAwait(false);
- if (releases == null || !releases.Any())
- throw new($"No GitHub releases found at '{RepoUri}'.");
-
- // CS: we 'cache' the release here, so subsequent calls to DownloadReleaseEntry
- // will download assets from the same release in which we returned ReleaseEntry's
- // from. A better architecture would be to return an array of "GithubReleaseEntry"
- // containing a reference to the GithubReleaseAsset instead.
- Releases = releases.Where(b =>
- SemanticVersion.TryParse(b.TagName, out _) && b.Assets.Select(c => c.Name)
- .Contains("RELEASES", StringComparer.OrdinalIgnoreCase))
- .ToImmutableSortedDictionary(b => SemanticVersion.Parse(b.TagName), b => b);
-
- return await Releases.Values.ToAsyncEnumerable().SelectManyAwait(async release =>
- {
- // this might be a browser url or an api url (depending on whether we have a AccessToken or not)
- // https://docs.github.com/en/rest/reference/releases#get-a-release-asset
- var assetUrl = GetAssetUrlFromName(release, "RELEASES");
- var releaseBytes = await Downloader.DownloadBytes(assetUrl, Authorization, "application/octet-stream")
- .ConfigureAwait(false);
- var txt = RemoveByteOrderMarkerIfPresent(releaseBytes);
- return ReleaseEntry.ParseReleaseFileAndApplyStaging(txt, stagingId).ToAsyncEnumerable();
- }).ToArrayAsync();
- }
-
- ///
- public virtual Task DownloadReleaseEntry(ReleaseEntry releaseEntry, string localFile, Action progress)
- {
- if (Releases == null)
- throw new InvalidOperationException("No GitHub Release specified. Call GetReleaseFeed or set " +
- "GithubSource.Release before calling this function.");
-
- // this might be a browser url or an api url (depending on whether we have a AccessToken or not)
- // https://docs.github.com/en/rest/reference/releases#get-a-release-asset
- var assetUrl = GetAssetUrlFromName(Releases[releaseEntry.Version], releaseEntry.Filename);
- return Downloader.DownloadFile(assetUrl, localFile, progress, Authorization, "application/octet-stream");
- }
-
- ///
- /// Retrieves a list of from the current repository.
- ///
- public virtual async Task GetReleases(bool includePrereleases, int perPage = 30, int page = 1)
- {
- // https://docs.github.com/en/rest/reference/releases
- var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}";
- var baseUri = GetApiBaseUrl(RepoUri);
- var getReleasesUri = new Uri(baseUri, releasesPath);
- var response = await Downloader
- .DownloadString(getReleasesUri.ToString(), Authorization, "application/vnd.github.v3+json")
- .ConfigureAwait(false);
- var releases = JsonConvert.DeserializeObject>(response)!;
- return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases ? x.Prerelease : !x.Prerelease).ToArray();
- }
-
- ///
- /// Given a and an asset filename (eg. 'RELEASES') this
- /// function will return either or
- /// , depending whether an access token is available
- /// or not. Throws if the specified release has no matching assets.
- ///
- protected virtual string GetAssetUrlFromName(GithubRelease release, string assetName)
- {
- if (release.Assets == null || !release.Assets.Any())
- throw new ArgumentException($"No assets found in Github Release '{release.Name}'.");
-
- var allReleasesFiles = release.Assets
- .Where(a => a.Name.Equals(assetName, StringComparison.InvariantCultureIgnoreCase)).ToArray();
- if (allReleasesFiles == null || !allReleasesFiles.Any())
- throw new ArgumentException(
- $"Could not find asset called '{assetName}' in Github Release '{release.Name}'.");
-
- var asset = allReleasesFiles.First();
-
- return string.IsNullOrWhiteSpace(AccessToken)
- ?
- // if no AccessToken provided, we use the BrowserDownloadUrl which does not
- // count towards the "unauthenticated api request" limit of 60 per hour per IP.
- asset.BrowserDownloadUrl
- :
- // otherwise, we use the regular asset url, which will allow us to retrieve
- // assets from private repositories
- // https://docs.github.com/en/rest/reference/releases#get-a-release-asset
- asset.Url;
- }
-
- ///
- /// Given a repository URL (e.g. https://github.com/myuser/myrepo) this function
- /// returns the API base for performing requests. (eg. "https://api.github.com/"
- /// or http://internal.github.server.local/api/v3)
- ///
- ///
- ///
- protected virtual Uri GetApiBaseUrl(Uri repoUrl)
- {
- var baseAddress = repoUrl.Host.EndsWith("github.com", StringComparison.OrdinalIgnoreCase)
- ? new("https://api.github.com/")
- :
- // if it's not github.com, it's probably an Enterprise server
- // now the problem with Enterprise is that the API doesn't come prefixed
- // it comes suffixed so the API path of http://internal.github.server.local
- // API location is http://internal.github.server.local/api/v3
- new Uri($"{repoUrl.Scheme}{Uri.SchemeDelimiter}{repoUrl.Host}/api/v3/");
- // above ^^ notice the end slashes for the baseAddress, explained here: http://stackoverflow.com/a/23438417/162694
- return baseAddress;
- }
-
- private static string RemoveByteOrderMarkerIfPresent(byte[]? content)
- {
- var output = Array.Empty();
-
- if (content == null) goto done;
-
- Func matches = (bom, src) =>
- {
- if (src.Length < bom.Length) return false;
-
- return !bom.Where((chr, index) => src[index] != chr).Any();
- };
-
- var utf32Be = new byte[] { 0x00, 0x00, 0xFE, 0xFF };
- var utf32Le = new byte[] { 0xFF, 0xFE, 0x00, 0x00 };
- var utf16Be = new byte[] { 0xFE, 0xFF };
- var utf16Le = new byte[] { 0xFF, 0xFE };
- var utf8 = new byte[] { 0xEF, 0xBB, 0xBF };
-
- if (matches(utf32Be, content))
- output = new byte[content.Length - utf32Be.Length];
- else if (matches(utf32Le, content))
- output = new byte[content.Length - utf32Le.Length];
- else if (matches(utf16Be, content))
- output = new byte[content.Length - utf16Be.Length];
- else if (matches(utf16Le, content))
- output = new byte[content.Length - utf16Le.Length];
- else if (matches(utf8, content))
- output = new byte[content.Length - utf8.Length];
- else
- output = content;
-
- done:
- if (output.Length > 0) Buffer.BlockCopy(content!, content!.Length - output.Length, output, 0, output.Length);
-
- return Encoding.UTF8.GetString(output);
- }
-}
\ No newline at end of file
diff --git a/MusicX.Core/MusicX.Core.csproj b/MusicX.Core/MusicX.Core.csproj
index 989eafb3..f35eb45a 100644
--- a/MusicX.Core/MusicX.Core.csproj
+++ b/MusicX.Core/MusicX.Core.csproj
@@ -15,10 +15,9 @@
-
-
-
-
+
+
+
diff --git a/MusicX.Shared/Player/PlaylistTrack.cs b/MusicX.Shared/Player/PlaylistTrack.cs
index dc883277..f3152bf9 100644
--- a/MusicX.Shared/Player/PlaylistTrack.cs
+++ b/MusicX.Shared/Player/PlaylistTrack.cs
@@ -22,6 +22,7 @@ public bool Equals(PlaylistTrack? other)
[JsonDerivedType(typeof(VkTrackData), "vk")]
[JsonDerivedType(typeof(BoomTrackData), "boom")]
+[JsonDerivedType(typeof(DownloaderData), "downloader")]
[ProtoContract(ImplicitFields = ImplicitFields.AllPublic, SkipConstructor = true)]
[ProtoInclude(100, typeof(BoomTrackData))]
[ProtoInclude(101, typeof(VkTrackData))]
diff --git a/MusicX.sln b/MusicX.sln
index 4a9dc207..d293e526 100644
--- a/MusicX.sln
+++ b/MusicX.sln
@@ -36,134 +36,74 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicX.Server.Tests", "Musi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SignalR.Protobuf", "SignalR.Protobuf\SignalR.Protobuf.csproj", "{6A511BD3-95E2-4087-9F68-A17DBC237B71}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMediaToolkit", "FFMediaToolkit\FFMediaToolkit\FFMediaToolkit.csproj", "{1A698859-8147-458D-9FC3-C4302F24AA86}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMediaToolkit", "FFMediaToolkit\FFMediaToolkit.csproj", "{1A698859-8147-458D-9FC3-C4302F24AA86}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
- Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Release|x64.ActiveCfg = Release|x64
+ {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Release|x64.Build.0 = Release|x64
{80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Debug|x64.ActiveCfg = Debug|x64
{80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Debug|x64.Build.0 = Debug|x64
- {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Release|Any CPU.Build.0 = Release|Any CPU
- {80B284C3-9FE8-4C90-9BD4-81037AC1EAAF}.Release|x64.ActiveCfg = Release|x64
- {A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Debug|x64.ActiveCfg = Debug|x64
{A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Debug|x64.Build.0 = Debug|x64
- {A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Release|Any CPU.Build.0 = Release|Any CPU
{A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Release|x64.ActiveCfg = Release|x64
{A5A9EB77-FE73-4AE9-9324-4675EDFD16CE}.Release|x64.Build.0 = Release|x64
- {AB09062A-83B9-4441-8D69-27E144875F6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AB09062A-83B9-4441-8D69-27E144875F6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB09062A-83B9-4441-8D69-27E144875F6B}.Debug|x64.ActiveCfg = Debug|x64
{AB09062A-83B9-4441-8D69-27E144875F6B}.Debug|x64.Build.0 = Debug|x64
- {AB09062A-83B9-4441-8D69-27E144875F6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AB09062A-83B9-4441-8D69-27E144875F6B}.Release|Any CPU.Build.0 = Release|Any CPU
{AB09062A-83B9-4441-8D69-27E144875F6B}.Release|x64.ActiveCfg = Release|x64
{AB09062A-83B9-4441-8D69-27E144875F6B}.Release|x64.Build.0 = Release|x64
- {05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Debug|x64.ActiveCfg = Debug|x64
{05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Debug|x64.Build.0 = Debug|x64
- {05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Release|Any CPU.Build.0 = Release|Any CPU
{05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Release|x64.ActiveCfg = Release|x64
{05EE1D52-3FF7-4BEE-9213-B8CE70F7240C}.Release|x64.Build.0 = Release|x64
- {5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Debug|x64.ActiveCfg = Debug|x64
{5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Debug|x64.Build.0 = Debug|x64
- {5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Release|Any CPU.Build.0 = Release|Any CPU
{5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Release|x64.ActiveCfg = Release|x64
{5B6E87D8-33EE-46FA-8C2C-D9C04F7E99F1}.Release|x64.Build.0 = Release|x64
- {5A5786D3-5305-4C26-9EFF-BE1699620B34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5A5786D3-5305-4C26-9EFF-BE1699620B34}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A5786D3-5305-4C26-9EFF-BE1699620B34}.Debug|x64.ActiveCfg = Debug|x64
{5A5786D3-5305-4C26-9EFF-BE1699620B34}.Debug|x64.Build.0 = Debug|x64
- {5A5786D3-5305-4C26-9EFF-BE1699620B34}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5A5786D3-5305-4C26-9EFF-BE1699620B34}.Release|Any CPU.Build.0 = Release|Any CPU
{5A5786D3-5305-4C26-9EFF-BE1699620B34}.Release|x64.ActiveCfg = Release|x64
{5A5786D3-5305-4C26-9EFF-BE1699620B34}.Release|x64.Build.0 = Release|x64
- {46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Debug|x64.ActiveCfg = Debug|x64
{46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Debug|x64.Build.0 = Debug|x64
- {46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Release|Any CPU.Build.0 = Release|Any CPU
{46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Release|x64.ActiveCfg = Release|x64
{46B3CE5D-F16C-4180-9F45-6CF6959A287A}.Release|x64.Build.0 = Release|x64
- {F05AC765-4098-45D9-B98D-C309995FDDDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F05AC765-4098-45D9-B98D-C309995FDDDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F05AC765-4098-45D9-B98D-C309995FDDDA}.Debug|x64.ActiveCfg = Debug|x64
- {F05AC765-4098-45D9-B98D-C309995FDDDA}.Debug|x64.Build.0 = Debug|x64
- {F05AC765-4098-45D9-B98D-C309995FDDDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F05AC765-4098-45D9-B98D-C309995FDDDA}.Release|Any CPU.Build.0 = Release|Any CPU
{F05AC765-4098-45D9-B98D-C309995FDDDA}.Release|x64.ActiveCfg = Release|x64
{F05AC765-4098-45D9-B98D-C309995FDDDA}.Release|x64.Build.0 = Release|x64
- {B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F05AC765-4098-45D9-B98D-C309995FDDDA}.Debug|x64.ActiveCfg = Debug|x64
+ {F05AC765-4098-45D9-B98D-C309995FDDDA}.Debug|x64.Build.0 = Debug|x64
{B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Debug|x64.ActiveCfg = Debug|x64
{B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Debug|x64.Build.0 = Debug|x64
- {B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Release|Any CPU.Build.0 = Release|Any CPU
{B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Release|x64.ActiveCfg = Release|x64
{B67F48FE-FE54-4E0D-8A3D-69F4BE6B6424}.Release|x64.Build.0 = Release|x64
- {9A7108E9-98F6-4D90-AB03-788DA1769566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9A7108E9-98F6-4D90-AB03-788DA1769566}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A7108E9-98F6-4D90-AB03-788DA1769566}.Debug|x64.ActiveCfg = Debug|x64
{9A7108E9-98F6-4D90-AB03-788DA1769566}.Debug|x64.Build.0 = Debug|x64
- {9A7108E9-98F6-4D90-AB03-788DA1769566}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9A7108E9-98F6-4D90-AB03-788DA1769566}.Release|Any CPU.Build.0 = Release|Any CPU
{9A7108E9-98F6-4D90-AB03-788DA1769566}.Release|x64.ActiveCfg = Release|x64
{9A7108E9-98F6-4D90-AB03-788DA1769566}.Release|x64.Build.0 = Release|x64
- {E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Debug|x64.ActiveCfg = Debug|x64
{E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Debug|x64.Build.0 = Debug|x64
- {E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Release|Any CPU.Build.0 = Release|Any CPU
{E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Release|x64.ActiveCfg = Release|x64
{E2C61095-E619-4A6D-8B56-4E2A7D86782E}.Release|x64.Build.0 = Release|x64
- {646DE14C-B71A-4A62-8249-204D98DD5334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {646DE14C-B71A-4A62-8249-204D98DD5334}.Debug|Any CPU.Build.0 = Debug|Any CPU
{646DE14C-B71A-4A62-8249-204D98DD5334}.Debug|x64.ActiveCfg = Debug|x64
{646DE14C-B71A-4A62-8249-204D98DD5334}.Debug|x64.Build.0 = Debug|x64
- {646DE14C-B71A-4A62-8249-204D98DD5334}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {646DE14C-B71A-4A62-8249-204D98DD5334}.Release|Any CPU.Build.0 = Release|Any CPU
{646DE14C-B71A-4A62-8249-204D98DD5334}.Release|x64.ActiveCfg = Release|x64
{646DE14C-B71A-4A62-8249-204D98DD5334}.Release|x64.Build.0 = Release|x64
- {52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Debug|x64.ActiveCfg = Debug|x64
{52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Debug|x64.Build.0 = Debug|x64
- {52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Release|Any CPU.Build.0 = Release|Any CPU
{52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Release|x64.ActiveCfg = Release|x64
{52E65E39-5947-48FA-B7D4-CEEEFBE33470}.Release|x64.Build.0 = Release|x64
- {6A511BD3-95E2-4087-9F68-A17DBC237B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6A511BD3-95E2-4087-9F68-A17DBC237B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A511BD3-95E2-4087-9F68-A17DBC237B71}.Debug|x64.ActiveCfg = Debug|x64
{6A511BD3-95E2-4087-9F68-A17DBC237B71}.Debug|x64.Build.0 = Debug|x64
- {6A511BD3-95E2-4087-9F68-A17DBC237B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6A511BD3-95E2-4087-9F68-A17DBC237B71}.Release|Any CPU.Build.0 = Release|Any CPU
{6A511BD3-95E2-4087-9F68-A17DBC237B71}.Release|x64.ActiveCfg = Release|x64
{6A511BD3-95E2-4087-9F68-A17DBC237B71}.Release|x64.Build.0 = Release|x64
- {1A698859-8147-458D-9FC3-C4302F24AA86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1A698859-8147-458D-9FC3-C4302F24AA86}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A698859-8147-458D-9FC3-C4302F24AA86}.Release|x64.ActiveCfg = Release|x64
+ {1A698859-8147-458D-9FC3-C4302F24AA86}.Release|x64.Build.0 = Release|x64
{1A698859-8147-458D-9FC3-C4302F24AA86}.Debug|x64.ActiveCfg = Debug|x64
{1A698859-8147-458D-9FC3-C4302F24AA86}.Debug|x64.Build.0 = Debug|x64
- {1A698859-8147-458D-9FC3-C4302F24AA86}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1A698859-8147-458D-9FC3-C4302F24AA86}.Release|Any CPU.Build.0 = Release|Any CPU
- {1A698859-8147-458D-9FC3-C4302F24AA86}.Release|x64.ActiveCfg = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MusicX/App.xaml.cs b/MusicX/App.xaml.cs
index a9edc85b..e48b7e9c 100644
--- a/MusicX/App.xaml.cs
+++ b/MusicX/App.xaml.cs
@@ -1,5 +1,4 @@
-// ReSharper disable once RedundantUsingDirective
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading;
using System.Windows;
using Microsoft.AppCenter;
@@ -7,7 +6,6 @@
using Microsoft.AppCenter.Crashes;
using MusicX.Services;
using MusicX.Views;
-using Squirrel;
namespace MusicX
{
@@ -19,7 +17,7 @@ public partial class App : Application
protected async override void OnStartup(StartupEventArgs e)
{
#if !DEBUG
- SquirrelAwareApp.HandleEvents((_, tools) => tools.CreateShortcutForThisExe(), onAppUninstall: (_, tool) => tool.RemoveShortcutForThisExe());
+ Velopack.VelopackApp.Build().Run();
if (!InstanceCheck())
{
@@ -32,8 +30,7 @@ protected async override void OnStartup(StartupEventArgs e)
return;
}
#endif
-
-
+
base.OnStartup(e);
AppCenter.Start("02130c6d-0a3b-4aa2-b46c-8aeb66c3fd71",
diff --git a/MusicX/Controls/BlockControl.xaml b/MusicX/Controls/BlockControl.xaml
index 3421e535..8abb4d39 100644
--- a/MusicX/Controls/BlockControl.xaml
+++ b/MusicX/Controls/BlockControl.xaml
@@ -86,7 +86,7 @@
-
+
diff --git a/MusicX/Controls/Blocks/LinksBlockControl.xaml.cs b/MusicX/Controls/Blocks/LinksBlockControl.xaml.cs
index 0e6d79fd..76394033 100644
--- a/MusicX/Controls/Blocks/LinksBlockControl.xaml.cs
+++ b/MusicX/Controls/Blocks/LinksBlockControl.xaml.cs
@@ -9,33 +9,39 @@ namespace MusicX.Controls.Blocks
///
public partial class LinksBlockControl : UserControl
{
- public LinksBlockControl()
- {
- InitializeComponent();
- this.Loaded += LinksBlockControl_Loaded;
- }
+ public static readonly DependencyProperty BlockProperty = DependencyProperty.Register(
+ nameof(Block), typeof(Block), typeof(LinksBlockControl), new(default(Block), BlockChanged));
- private void LinksBlockControl_Loaded(object sender, RoutedEventArgs e)
+ private static void BlockChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
- if (DataContext is not Block block)
+ if (d is not LinksBlockControl control || e.NewValue is not Block block)
return;
- LinksBlock.Visibility = Visibility.Visible;
-
+
if (block.Layout.Name == "list")
{
foreach (var link in block.Links)
{
- ListLinks.Children.Add(new LinkControl() { Height = 80, Width = 300, Link = link, FullLink = true, Margin = new Thickness(0, 0, 10, 10) });
+ control.ListLinks.Children.Add(new LinkControl() { Height = 80, Width = 300, Link = link, FullLink = true, Margin = new Thickness(0, 0, 10, 10) });
}
-
}
else
{
foreach (var link in block.Links)
{
- ListLinksRec.Children.Add(new LinkControl() { Height = 140, Width = 140, Link = link, Margin = new Thickness(0, 0, 10, 0) });
+ control.ListLinksRec.Children.Add(new LinkControl() { Height = 140, Width = 140, Link = link, Margin = new Thickness(0, 0, 10, 0) });
}
}
}
+
+ public Block Block
+ {
+ get => (Block)GetValue(BlockProperty);
+ set => SetValue(BlockProperty, value);
+ }
+
+ public LinksBlockControl()
+ {
+ InitializeComponent();
+ }
}
}
diff --git a/MusicX/MusicX.csproj b/MusicX/MusicX.csproj
index 78935174..5ec0f682 100644
--- a/MusicX/MusicX.csproj
+++ b/MusicX/MusicX.csproj
@@ -2,7 +2,7 @@
WinExe
- net7.0-windows10.0.22000.0
+ net7.0-windows10.0.22621.0
10.0.19041.0
10.0.19041.0
enable
@@ -14,6 +14,7 @@
true
app.manifest
true
+ x64
@@ -72,14 +73,13 @@
-
-
-
-
+
+
+
-
-
-
+
+
+
all
@@ -87,15 +87,16 @@
-
+
-
+
+
-
+
diff --git a/MusicX/RootWindow.xaml.cs b/MusicX/RootWindow.xaml.cs
index 820f5a33..224aeed8 100644
--- a/MusicX/RootWindow.xaml.cs
+++ b/MusicX/RootWindow.xaml.cs
@@ -23,12 +23,11 @@
using MusicX.Views;
using MusicX.Views.Modals;
using NLog;
-using Squirrel;
-using Squirrel.Sources;
+using Velopack;
+using Velopack.Sources;
using Wpf.Ui;
using Wpf.Ui.Controls;
using Wpf.Ui.Extensions;
-using GithubSource = MusicX.Core.Helpers.GithubSource;
using NavigationService = MusicX.Services.NavigationService;
namespace MusicX
@@ -470,18 +469,17 @@ private async Task CheckUpdatesInStart()
var getBetaUpdates = config.GetBetaUpdates.GetValueOrDefault(false);
var manager = new UpdateManager(new GithubSource("https://github.com/Fooxboy/MusicX-WPF",
- string.Empty, getBetaUpdates, new HttpClientFileDownloader()));
+ string.Empty, getBetaUpdates, new HttpClientFileDownloader()), new()
+ {
+ ExplicitChannel = getBetaUpdates ? "win-beta" : "win"
+ });
- var updateInfo = await manager.CheckForUpdate(manager.Config.CurrentlyInstalledVersion.HasMetadata ? !getBetaUpdates : getBetaUpdates);
+ var updateInfo = await manager.CheckForUpdatesAsync();
- if (updateInfo.ReleasesToApply.Count == 0)
- {
- manager.Dispose();
+ if (updateInfo is null)
return;
- }
- var viewModel = new AvailableNewUpdateModalViewModel(manager, updateInfo,
- StaticService.Container.GetRequiredService());
+ var viewModel = new AvailableNewUpdateModalViewModel(manager, updateInfo);
navigationService.OpenModal(viewModel);
}catch(Exception ex)
diff --git a/MusicX/Services/Player/PlayerService.cs b/MusicX/Services/Player/PlayerService.cs
index 8ee285dd..167b8234 100644
--- a/MusicX/Services/Player/PlayerService.cs
+++ b/MusicX/Services/Player/PlayerService.cs
@@ -86,10 +86,10 @@ public PlayerService(Logger logger, ISnackbarService snackbarService,
SubscribeToListenTogetherEvents();
}
-
- public async Task JoinToListenTogetherSession(string sessionId)
+ public async Task RestoreFromStateAsync(PlayerState state)
{
-
+ await PlayAsync(state.Playlist, state.Track);
+ Seek(state.Position);
}
public async void Play()
diff --git a/MusicX/Services/Player/PlayerState.cs b/MusicX/Services/Player/PlayerState.cs
new file mode 100644
index 00000000..e472824c
--- /dev/null
+++ b/MusicX/Services/Player/PlayerState.cs
@@ -0,0 +1,13 @@
+using System;
+using MusicX.Services.Player.Playlists;
+using MusicX.Shared.Player;
+
+namespace MusicX.Services.Player;
+
+public record PlayerState(IPlaylist Playlist, PlaylistTrack Track, TimeSpan Position)
+{
+ public static PlayerState? CreateOrNull(PlayerService service) =>
+ service is { CurrentPlaylist: null } or { CurrentTrack: null }
+ ? null
+ : new(service.CurrentPlaylist, service.CurrentTrack, service.Position);
+}
\ No newline at end of file
diff --git a/MusicX/Services/Player/Playlists/IPlaylist.cs b/MusicX/Services/Player/Playlists/IPlaylist.cs
index 271b7cd9..7baf5b5a 100644
--- a/MusicX/Services/Player/Playlists/IPlaylist.cs
+++ b/MusicX/Services/Player/Playlists/IPlaylist.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.DependencyInjection;
using MusicX.Shared.Player;
namespace MusicX.Services.Player.Playlists;
@@ -9,6 +12,11 @@ public interface IPlaylist : IPlaylist where TData : class, IEquatabl
TData Data { get; }
}
+[JsonDerivedType(typeof(SinglePlaylist), "single")]
+[JsonDerivedType(typeof(ListPlaylist), "list")]
+[JsonDerivedType(typeof(RadioPlaylist), "radio")]
+[JsonDerivedType(typeof(VkBlockPlaylist), "vkBlock")]
+[JsonDerivedType(typeof(VkPlaylistPlaylist), "vkPlaylist")]
public interface IPlaylist : IEquatable
{
bool CanLoad { get; }
@@ -29,4 +37,19 @@ public bool Equals(IPlaylist? other)
public override bool Equals(object? obj) => Equals((IPlaylist?)obj);
public override int GetHashCode() => Data.GetHashCode();
+}
+
+public class PlaylistJsonConverter : JsonConverter where TPlaylist : class, IPlaylist where TData : class, IEquatable
+{
+ public override TPlaylist? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var data = JsonSerializer.Deserialize(ref reader, options);
+
+ return data is null ? null : ActivatorUtilities.CreateInstance(StaticService.Container, data);
+ }
+
+ public override void Write(Utf8JsonWriter writer, TPlaylist value, JsonSerializerOptions options)
+ {
+ JsonSerializer.Serialize(writer, value.Data, options);
+ }
}
\ No newline at end of file
diff --git a/MusicX/Services/Player/Playlists/ListPlaylist.cs b/MusicX/Services/Player/Playlists/ListPlaylist.cs
index a4bb6ec4..5f74d8eb 100644
--- a/MusicX/Services/Player/Playlists/ListPlaylist.cs
+++ b/MusicX/Services/Player/Playlists/ListPlaylist.cs
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json.Serialization;
using MusicX.Shared.Player;
namespace MusicX.Services.Player.Playlists;
+[JsonConverter(typeof(PlaylistJsonConverter>))]
public class ListPlaylist : PlaylistBase>
{
private bool _canLoad = true;
diff --git a/MusicX/Services/Player/Playlists/RadioPlaylist.cs b/MusicX/Services/Player/Playlists/RadioPlaylist.cs
index 7d93d8a1..a4ced55f 100644
--- a/MusicX/Services/Player/Playlists/RadioPlaylist.cs
+++ b/MusicX/Services/Player/Playlists/RadioPlaylist.cs
@@ -1,22 +1,25 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Text.Json.Serialization;
using MusicX.Core.Models.Boom;
using MusicX.Core.Services;
using MusicX.Shared.Player;
namespace MusicX.Services.Player.Playlists;
-public class RadioPlaylist : PlaylistBase
+public record RadioData(Radio Playlist, BoomRadioType PlaylistType);
+
+[JsonConverter(typeof(PlaylistJsonConverter))]
+public class RadioPlaylist : PlaylistBase
{
private readonly BoomService _boomService;
private bool _firstCall = true;
- private BoomRadioType _radioType;
-
- public RadioPlaylist(BoomService boomService, Radio data, BoomRadioType radioType)
+
+ public RadioPlaylist(BoomService boomService, RadioData radioData)
{
_boomService = boomService;
- Data = data;
- _radioType = radioType;
+ Data = radioData;
}
public override IAsyncEnumerable LoadAsync()
@@ -25,25 +28,18 @@ public override IAsyncEnumerable LoadAsync()
return LoadAsyncInternal();
_firstCall = false;
- return Data.Tracks.Select(TrackExtensions.ToTrack).ToAsyncEnumerable();
+ return Data.Playlist.Tracks.Select(TrackExtensions.ToTrack).ToAsyncEnumerable();
}
private async IAsyncEnumerable LoadAsyncInternal()
{
- Radio radio;
- if(_radioType == BoomRadioType.Artist)
- {
- radio = await _boomService.GetArtistMixAsync(Data.Artist.ApiId);
- }else if(_radioType == BoomRadioType.Personal)
+ var radio = Data.PlaylistType switch
{
- radio = await _boomService.GetPersonalMixAsync();
- }else if(_radioType == BoomRadioType.Tag)
- {
- radio = await _boomService.GetTagMixAsync(Data.Tag.ApiId);
- }else
- {
- radio = null;
- }
+ BoomRadioType.Artist => await _boomService.GetArtistMixAsync(Data.Playlist.Artist.ApiId),
+ BoomRadioType.Personal => await _boomService.GetPersonalMixAsync(),
+ BoomRadioType.Tag => await _boomService.GetTagMixAsync(Data.Playlist.Tag.ApiId),
+ _ => throw new ArgumentOutOfRangeException(null, "Unknown radio type")
+ };
foreach (var track in radio.Tracks)
{
@@ -52,5 +48,5 @@ private async IAsyncEnumerable LoadAsyncInternal()
}
public override bool CanLoad => true;
- public override Radio Data { get; }
+ public override RadioData Data { get; }
}
\ No newline at end of file
diff --git a/MusicX/Services/Player/Playlists/SinglePlaylist.cs b/MusicX/Services/Player/Playlists/SinglePlaylist.cs
index 64a75c22..d4e73956 100644
--- a/MusicX/Services/Player/Playlists/SinglePlaylist.cs
+++ b/MusicX/Services/Player/Playlists/SinglePlaylist.cs
@@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json.Serialization;
using MusicX.Shared.Player;
namespace MusicX.Services.Player.Playlists;
+[JsonConverter(typeof(PlaylistJsonConverter))]
public class SinglePlaylist : PlaylistBase
{
private bool _canLoad = true;
diff --git a/MusicX/Services/Player/Playlists/VkBlockPlaylist.cs b/MusicX/Services/Player/Playlists/VkBlockPlaylist.cs
index 18f8f3f0..2e2ee1eb 100644
--- a/MusicX/Services/Player/Playlists/VkBlockPlaylist.cs
+++ b/MusicX/Services/Player/Playlists/VkBlockPlaylist.cs
@@ -1,14 +1,21 @@
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.DependencyInjection;
using MusicX.Core.Services;
using MusicX.Helpers;
using MusicX.Shared.Player;
namespace MusicX.Services.Player.Playlists;
+[JsonConverter(typeof(PlaylistJsonConverter))]
public class VkBlockPlaylist : PlaylistBase
{
private readonly VkService _vkService;
+
+ [ActivatorUtilitiesConstructor]
+ // ReSharper disable once RedundantOverload.Global
+ public VkBlockPlaylist(VkService vkService, string blockId) : this(vkService, blockId, true) {}
public VkBlockPlaylist(VkService vkService, string blockId, bool loadOther = true)
{
diff --git a/MusicX/Services/Player/Playlists/VkPlaylistPlaylist.cs b/MusicX/Services/Player/Playlists/VkPlaylistPlaylist.cs
index 4924f0f6..319b0d17 100644
--- a/MusicX/Services/Player/Playlists/VkPlaylistPlaylist.cs
+++ b/MusicX/Services/Player/Playlists/VkPlaylistPlaylist.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Text.Json.Serialization;
using MusicX.Core.Models;
using MusicX.Core.Services;
using MusicX.Helpers;
@@ -7,6 +8,7 @@
namespace MusicX.Services.Player.Playlists;
+[JsonConverter(typeof(PlaylistJsonConverter))]
public class VkPlaylistPlaylist : PlaylistBase
{
private readonly VkService _vkService;
diff --git a/MusicX/Services/Player/Sources/MediaSourceBase.cs b/MusicX/Services/Player/Sources/MediaSourceBase.cs
index 7f3e78e3..8f3f872d 100644
--- a/MusicX/Services/Player/Sources/MediaSourceBase.cs
+++ b/MusicX/Services/Player/Sources/MediaSourceBase.cs
@@ -173,7 +173,10 @@ protected static MediaPlaybackItem CreateMediaPlaybackItem(MediaFile file)
_source = await FFmpegMediaSource.CreateFromUriAsync(data.Url, new()
{
FFmpegOptions = options,
- ReadAheadBufferEnabled = true
+ General =
+ {
+ ReadAheadBufferEnabled = true
+ }
});
_source.PlaybackSession = playbackSession;
diff --git a/MusicX/ViewModels/BoomViewModelBase.cs b/MusicX/ViewModels/BoomViewModelBase.cs
index 7c4c50d5..dfda1ae9 100644
--- a/MusicX/ViewModels/BoomViewModelBase.cs
+++ b/MusicX/ViewModels/BoomViewModelBase.cs
@@ -60,7 +60,7 @@ public virtual async Task ArtistSelected()
IsLoadingMix = true;
var radioByArtist = await BoomService.GetArtistMixAsync(SelectedArtist.ApiId);
- await PlayerService.PlayAsync(new RadioPlaylist(BoomService, radioByArtist, BoomRadioType.Artist), radioByArtist.Tracks[0].ToTrack());
+ await PlayerService.PlayAsync(new RadioPlaylist(BoomService, new(radioByArtist, BoomRadioType.Artist)), radioByArtist.Tracks[0].ToTrack());
IsLoadingMix = false;
}catch(UnauthorizedException ex)
@@ -108,7 +108,7 @@ public virtual async Task TagSelected()
var radio = await BoomService.GetTagMixAsync(SelectedTag.ApiId);
- await PlayerService.PlayAsync(new RadioPlaylist(BoomService, radio, BoomRadioType.Tag), radio.Tracks[0].ToTrack());
+ await PlayerService.PlayAsync(new RadioPlaylist(BoomService, new(radio, BoomRadioType.Tag)), radio.Tracks[0].ToTrack());
IsLoadingMix = false;
}
diff --git a/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs b/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs
index 7c598fb0..3740d49c 100644
--- a/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs
+++ b/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs
@@ -1,25 +1,27 @@
using System;
using System.Collections.Generic;
-using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
+using System.Windows;
using System.Windows.Input;
-using AsyncAwaitBestPractices;
using AsyncAwaitBestPractices.MVVM;
using Microsoft.AppCenter.Crashes;
using Microsoft.Extensions.DependencyInjection;
-using MusicX.Core.Services;
using MusicX.Helpers;
using MusicX.Services;
+using MusicX.Services.Player;
using NLog;
-using Squirrel;
+using NuGet.Versioning;
+using Velopack;
using Wpf.Ui;
namespace MusicX.ViewModels.Modals;
-public sealed class AvailableNewUpdateModalViewModel : BaseViewModel, IDisposable
+public sealed class AvailableNewUpdateModalViewModel : BaseViewModel
{
private readonly UpdateManager _updateManager;
- private readonly GithubService _githubService;
+
+ public SemanticVersion? CurrentVersion => _updateManager.CurrentVersion;
public UpdateInfo UpdateInfo { get; set; }
@@ -29,47 +31,11 @@ public sealed class AvailableNewUpdateModalViewModel : BaseViewModel, IDisposabl
public int Progress { get; set; }
- public string Changelog { get; private set; } = "Загрузка...";
-
- public AvailableNewUpdateModalViewModel(UpdateManager updateManager, UpdateInfo updateInfo,
- GithubService githubService)
+ public AvailableNewUpdateModalViewModel(UpdateManager updateManager, UpdateInfo updateInfo)
{
_updateManager = updateManager;
- _githubService = githubService;
UpdateInfo = updateInfo;
ApplyUpdatesCommand = new AsyncCommand(Execute);
- LoadChangelogAsync().SafeFireAndForget(ex =>
- {
- var snackbarService = StaticService.Container.GetRequiredService();
- var logger = StaticService.Container.GetRequiredService();
-
- var properties = new Dictionary
- {
-#if DEBUG
- { "IsDebug", "True" },
-#endif
- {"Version", StaticService.Version }
- };
- Crashes.TrackError(ex, properties);
- logger.Error(ex, ex.Message);
-
- snackbarService.ShowException("Неудалось получить список изменений",
- $"Произошла ошибка при получении списка изменений: {ex.Message}");
- Changelog = "Нет информации.";
- });
- }
-
- private async Task LoadChangelogAsync()
- {
- var sb = new StringBuilder();
- foreach (var entry in UpdateInfo.ReleasesToApply)
- {
- var release = await _githubService.GetReleaseByTag(entry.Version.ToFullString());
-
- sb.AppendLine($"- {entry.Version.ToFullString()}:").AppendLine(release.Body);
- }
-
- Changelog = sb.ToString();
}
private async Task Execute()
@@ -79,10 +45,17 @@ private async Task Execute()
{
void ProgressHandler(int i) => Progress = i;
- await _updateManager.DownloadReleases(UpdateInfo.ReleasesToApply, ProgressHandler);
- await _updateManager.ApplyReleases(UpdateInfo, ProgressHandler);
+ await _updateManager.DownloadUpdatesAsync(UpdateInfo, ProgressHandler);
- UpdateManager.RestartApp();
+ var playerState = PlayerState.CreateOrNull(StaticService.Container.GetRequiredService());
+
+ _updateManager.WaitExitThenApplyUpdates(UpdateInfo, restartArgs: new []
+ {
+ "--play",
+ playerState is null ? "null" : JsonSerializer.Serialize(playerState)
+ });
+
+ Application.Current.Shutdown();
}
catch (Exception ex)
{
@@ -101,15 +74,7 @@ private async Task Execute()
snackbarService.ShowException("Неудалось обновить приложение",
$"Произошла ошибка при обновлении приложения: {ex.Message}");
- }
- finally
- {
IsUpdating = false;
}
}
-
- public void Dispose()
- {
- _updateManager.Dispose();
- }
}
diff --git a/MusicX/ViewModels/VKMixViewModel.cs b/MusicX/ViewModels/VKMixViewModel.cs
index 346eed7d..b06131a7 100644
--- a/MusicX/ViewModels/VKMixViewModel.cs
+++ b/MusicX/ViewModels/VKMixViewModel.cs
@@ -132,7 +132,7 @@ private async Task PlayPersonalMixAsync()
IsLoadingMix = true;
var personalMix = await BoomService.GetPersonalMixAsync();
- await PlayerService.PlayAsync(new RadioPlaylist(BoomService, personalMix, BoomRadioType.Personal), personalMix.Tracks[0].ToTrack());
+ await PlayerService.PlayAsync(new RadioPlaylist(BoomService, new(personalMix, BoomRadioType.Personal)), personalMix.Tracks[0].ToTrack());
IsLoadingMix = false;
diff --git a/MusicX/Views/Modals/AvailableNewUpdateModal.xaml b/MusicX/Views/Modals/AvailableNewUpdateModal.xaml
index d924c073..9f67cc82 100644
--- a/MusicX/Views/Modals/AvailableNewUpdateModal.xaml
+++ b/MusicX/Views/Modals/AvailableNewUpdateModal.xaml
@@ -26,9 +26,9 @@
-
+
-
+
+ Markdown="{Binding UpdateInfo.TargetFullRelease.NotesMarkdown}" />
diff --git a/MusicX/Views/StartingWindow.xaml.cs b/MusicX/Views/StartingWindow.xaml.cs
index 20fb0e55..f7275dab 100644
--- a/MusicX/Views/StartingWindow.xaml.cs
+++ b/MusicX/Views/StartingWindow.xaml.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
+using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.AppCenter.Analytics;
@@ -177,7 +178,7 @@ await container.GetRequiredService()
var rootWindow = ActivatorUtilities.CreateInstance(container);
rootWindow.Show();
- if(_args != null && _args.Length > 0)
+ if(_args is { Length: > 0 })
{
var arg = _args[0].Split(":");
@@ -185,6 +186,13 @@ await container.GetRequiredService()
{
await rootWindow.StartListenTogether(arg[1]);
}
+
+ var playArgIndex = Array.BinarySearch(_args, "--play",
+ StringComparer.OrdinalIgnoreCase);
+ if (playArgIndex >= 0 && playArgIndex + 1 < _args.Length &&
+ JsonSerializer.Deserialize(_args[playArgIndex + 1]) is { } state)
+ await container.GetRequiredService()
+ .RestoreFromStateAsync(state);
}
this.Close();
diff --git a/MusicX/Views/VKMixView.xaml b/MusicX/Views/VKMixView.xaml
index 992bc148..ebf8485b 100644
--- a/MusicX/Views/VKMixView.xaml
+++ b/MusicX/Views/VKMixView.xaml
@@ -65,32 +65,33 @@
HorizontalScrollBarVisibility="Auto"
IsInertiaEnabled="True"
hc:ScrollViewerAttach.AutoHide="True">
-
-
-
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
@@ -101,33 +102,34 @@
HorizontalScrollBarVisibility="Auto"
IsInertiaEnabled="True"
hc:ScrollViewerAttach.AutoHide="True">
-
+
-
-
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
diff --git a/MusicX/packages.lock.json b/MusicX/packages.lock.json
index 400f4a9e..b1198cd4 100644
--- a/MusicX/packages.lock.json
+++ b/MusicX/packages.lock.json
@@ -1,67 +1,58 @@
{
"version": 1,
"dependencies": {
- "net7.0-windows10.0.22000": {
+ "net7.0-windows10.0.22621": {
"AsyncAwaitBestPractices.MVVM": {
"type": "Direct",
- "requested": "[6.0.6, )",
- "resolved": "6.0.6",
- "contentHash": "0K2hFZq3RLeAI9VaxkyQbvS6SDx09fl8S9zsJSOBP01/bljj7Ij5fFUQIvqC+hbJrVCHi4AnvfSLfxXf0kLRIA==",
+ "requested": "[7.0.0, )",
+ "resolved": "7.0.0",
+ "contentHash": "P8sgaDlxmdSge23mQ+JURmFXRYQqq1FjNzV7vmp+95Dcmcmajn4ms1at16+1Qz6i7O1tS8bQ91x5qBs6i+r3bw==",
"dependencies": {
- "AsyncAwaitBestPractices": "6.0.6"
+ "AsyncAwaitBestPractices": "7.0.0"
}
},
- "Clowd.Squirrel": {
+ "FFmpegInteropX": {
"type": "Direct",
- "requested": "[3.0.210-g5f9f594, )",
- "resolved": "3.0.210-g5f9f594",
- "contentHash": "F5rxSqpweN/tSMguLTRNLbc1TuAjSYDCzji6TZpQc2dxzrC+Rwv6cDKFUNThULjs8AFBvVdZxZycyrJZvW9e8Q==",
+ "requested": "[2.0.0-pre3, )",
+ "resolved": "2.0.0-pre3",
+ "contentHash": "FzoYL3r5cqS03049epme9+NiqVJVjq5X2MLkZlZpZYFj1iRbU/IJqM1VxRS5O7Tvhg2WFNHSQVNoKIURh7wTGw==",
"dependencies": {
- "NuGet.Versioning": "6.3.0",
- "SharpCompress": "0.32.2"
- }
- },
- "FFmpegInteropX.Desktop": {
- "type": "Direct",
- "requested": "[2.0.0-pre2, )",
- "resolved": "2.0.0-pre2",
- "contentHash": "7+TF/6sw9XArcIvgukUdEJ9IXHwLwjNbyHouvYXu9ndDz7knPMWxes/XGrU1GOgjQ7DWKBxp4LEt68oeFjdbmA==",
- "dependencies": {
- "FFmpegInteropX.FFmpegUWP": "5.1.100"
+ "FFmpegInteropX.Desktop.FFmpeg": "5.1.100-pre3",
+ "FFmpegInteropX.Desktop.Lib": "2.0.0-pre3"
}
},
"HandyControl": {
"type": "Direct",
- "requested": "[3.4.0, )",
- "resolved": "3.4.0",
- "contentHash": "MgD5lxIm23oCv3+mFWH6EARLCOmR/spFaWeb813QtUDIQeUsbGK02xm1xDQEd0UGBcba6aho1FgCtiU4q7hHDw=="
+ "requested": "[3.5.1, )",
+ "resolved": "3.5.1",
+ "contentHash": "i2i0xrLev7F1MFnhf0DP1CNCdGw8hVJ0HJrI0kxRv02ZJgaAzLzDTgAXDPY8GAoD3aYnBLSgM74lBHZ844KQnQ=="
},
"MdXaml": {
"type": "Direct",
- "requested": "[1.22.0, )",
- "resolved": "1.22.0",
- "contentHash": "wrt+KlEgAA6XoSLPH7KDFk1efcNGCSfZfbI5XcpR14/VpKaDy/vlnELsZrWQjkUahFpt01jleDHclEsXNvQoCQ==",
+ "requested": "[1.27.0, )",
+ "resolved": "1.27.0",
+ "contentHash": "VWhqhCeKVkJe8vkPmXuGZlRX01WDrTugOLeUvJn18jH/8DrGGVBvtgIlJoELHD2f1DiEWqF3lxxjV55vnzE7Tg==",
"dependencies": {
- "AvalonEdit": "6.0.0",
- "MdXaml.Plugins": "1.22.0"
+ "AvalonEdit": "6.3.0.90",
+ "MdXaml.Plugins": "1.27.0"
}
},
"Microsoft.AppCenter.Analytics": {
"type": "Direct",
- "requested": "[5.0.2, )",
- "resolved": "5.0.2",
- "contentHash": "yDLYtsf+nmLdwLp49zyhJ+hibV4E2W1zNkFkSH5sot4kL+o+pnn0UUzr+OHobxJDlAC8qnhXvidtgs0pTndYkw==",
+ "requested": "[5.0.3, )",
+ "resolved": "5.0.3",
+ "contentHash": "SMkyXlmkwImUfqdElJKkc+33bHAswgNOCu8Iu8fSFCgNjxvuKran0QSsIBlWzBxPRlRBvjq08Z/ukg02bXBezg==",
"dependencies": {
- "Microsoft.AppCenter": "5.0.2"
+ "Microsoft.AppCenter": "5.0.3"
}
},
"Microsoft.AppCenter.Crashes": {
"type": "Direct",
- "requested": "[5.0.2, )",
- "resolved": "5.0.2",
- "contentHash": "v20yNCKbP3j6l0BcUjU9C9IZsl5Gq6vv+VXPzMV4W04j/Nsmn7WKKxoYqe5a0Djxm5vKCkVxTftQt/eQ0r3ZSw==",
+ "requested": "[5.0.3, )",
+ "resolved": "5.0.3",
+ "contentHash": "yJfl71WnWIyQN2+MyBZjhZ9LEoYhbax2QZhX3yXzPHy8ZrAIVXCmKFlT5XrA34PcG75spj8kc39nGtYyxlozEQ==",
"dependencies": {
- "Microsoft.AppCenter": "5.0.2"
+ "Microsoft.AppCenter": "5.0.3"
}
},
"Microsoft.VCRTForwarders.140": {
@@ -118,12 +109,9 @@
},
"System.Text.Encoding.CodePages": {
"type": "Direct",
- "requested": "[6.0.0, )",
- "resolved": "6.0.0",
- "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
- "dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
- }
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg=="
},
"System.Text.Json": {
"type": "Direct",
@@ -140,11 +128,21 @@
"resolved": "2.1.0",
"contentHash": "w45LreEPpV/vOfgpYBI1cNqiLvj8yQ8f7kJaMYtzTAsg26ZdHWnh5vPuy2/F9tCKhmgNB0s+zOBwdcEb4qk3Tg=="
},
+ "Velopack": {
+ "type": "Direct",
+ "requested": "[0.0.359, )",
+ "resolved": "0.0.359",
+ "contentHash": "UezXo8WNBC8AMrnkuQ1KeYmlkg829Th1p1+pi+HAPXmbd1XX5chlgHj69P2cLqdhhbfCL7sVdfFbKHp2UbBsJg==",
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.0",
+ "NuGet.Versioning": "6.9.1"
+ }
+ },
"WPF-UI": {
"type": "Direct",
- "requested": "[3.0.0-preview.11, )",
- "resolved": "3.0.0-preview.11",
- "contentHash": "wPTCMHQvjCQT096NzYix5Z+VVvitfmRoDYL+K9V679f7z0eLIts02ehnunjypKkg4w9bO8WCwffktb8+kiRiBQ=="
+ "requested": "[3.0.3, )",
+ "resolved": "3.0.3",
+ "contentHash": "Nm6Q5StLWxs74eFcqTwNJTGpA6xbTF1BXLniy5HMwnowvjiEa6Wa26/eI3e4/KbD56YF4nZidn69G/lY7t+aNg=="
},
"WpfScreenHelper": {
"type": "Direct",
@@ -154,23 +152,34 @@
},
"AsyncAwaitBestPractices": {
"type": "Transitive",
- "resolved": "6.0.6",
- "contentHash": "5JwyXsa0357gUCfrm741ae10LuWRl/V3JmBLL/7ddTeyA0ajWm4Ze+WCzweSzz2Pd8MT6T9PDVNK8pjZ9ezt6A=="
+ "resolved": "7.0.0",
+ "contentHash": "RhUokh7YKNpr+W0xzQWTGPyJtv/jVEalyKyBYAhFNyhUa0Ncsrz5S6PYxNDZ4ye/TK//qrpRInn5hzLBTQpehw=="
},
"AvalonEdit": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "QMbyJrlhOuWzLRPqvW724ly9XbSEkp8Xg2mQY7tvsh1se1pDEJnmDjS6c6OuqDe2Q37uCnXwKdV8tJUx2iLUnw=="
+ "resolved": "6.3.0.90",
+ "contentHash": "WVTb5MxwGqKdeasd3nG5udlV4t6OpvkFanziwI133K0/QJ5FvZmfzRQgpAjGTJhQfIA8GP7AzKQ3sTY9JOFk8Q=="
},
"FFmpeg.AutoGen": {
"type": "Transitive",
"resolved": "5.1.2.3",
"contentHash": "3w3o28VmTEf+qRQSFCQnhwB+ouVmow5dQepbfL0lOEzRlgk66HMsg6O8SeUoxHeUFEqo1bKQaux1h3x5Aj6GaQ=="
},
- "FFmpegInteropX.FFmpegUWP": {
+ "FFmpegInteropX.Desktop.FFmpeg": {
+ "type": "Transitive",
+ "resolved": "5.1.100-pre3",
+ "contentHash": "0s2EqrsMBsR37eP0LL/0iIr5qmY3txHaP20K21QaZbVgve4sEndrRk/d5kkD7Ks/S73jIrIuXY3UvJcOz65aWg==",
+ "dependencies": {
+ "Microsoft.Windows.CsWinRT": "2.0.0"
+ }
+ },
+ "FFmpegInteropX.Desktop.Lib": {
"type": "Transitive",
- "resolved": "5.1.100",
- "contentHash": "Pi+ifqTU3VpkvfTkZIR62+fck22HaA54WJeLyumI/EWgXM+1euSihsTX20oi3aQKQAz8Qh9weNLhntD4g3pmvA=="
+ "resolved": "2.0.0-pre3",
+ "contentHash": "LOInPKHQtH0QEDtqCJx9s9m2c54LT8tsN+rly+W1oQ0rUfElvrmeExvDE0Y6/X/Ju5EwXWm+lN9ZgVS8ng+YPA==",
+ "dependencies": {
+ "Microsoft.Windows.CsWinRT": "2.0.0"
+ }
},
"Fody": {
"type": "Transitive",
@@ -184,89 +193,101 @@
},
"MdXaml.Plugins": {
"type": "Transitive",
- "resolved": "1.22.0",
- "contentHash": "asC2GP5AsGQZboc1DKSfgQpk1pkvGF8brfdQtLEAweRGcTgbbuzuonVTal4Bhmje4IJWeMF8QBai5lOLEUmUVQ=="
+ "resolved": "1.27.0",
+ "contentHash": "We7LtBdoukRg9mqTfa1f5n8z/GQPMKBRj3URk9DiMuqzIHkW1lTgK5njVPSScxsRt4YzW22423tSnLWNm2MJKg=="
},
"Microsoft.AppCenter": {
"type": "Transitive",
- "resolved": "5.0.2",
- "contentHash": "AIxHDJsNpjvF7Gp+JU7N+iiSCKETzNCUD+SLt4LsgUc3NGo9FhPxqqJJxhN9RWSSFEtxKwCniPtBYqvjJLMS3w==",
+ "resolved": "5.0.3",
+ "contentHash": "5WizAr5oDDZtZlpeotbu2kB6UGVXKkk7NtAHMFqOP8kvc8JuMY261einCcgUlsm6kByk3umoS/6YP3n+VMDzAw==",
"dependencies": {
"Newtonsoft.Json": "13.0.2",
- "SQLitePCLRaw.bundle_green": "2.1.0",
+ "SQLitePCLRaw.bundle_green": "2.1.5",
"System.Configuration.ConfigurationManager": "6.0.0",
"System.Management": "6.0.0"
}
},
"Microsoft.AspNetCore.Connections.Abstractions": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "OLep4qKG7cFrS9rkS9uZlPNFeELcfv3IeRLY9fxXfPOZG5cUVcKGuc4MI1NPwBeCtny+wuST5AZRzcZ6OsTX0w==",
+ "resolved": "8.0.3",
+ "contentHash": "lM0+lfgJKv1Fq1z6uoMxIAQtVbhd427Ht5HqomDsebsrnklR9pkYgmdp6r6k+dNw5LoI8Vr0EevtQvdKW9aV7w==",
"dependencies": {
- "Microsoft.Extensions.Features": "6.0.10",
- "System.IO.Pipelines": "6.0.3"
+ "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+ "Microsoft.Extensions.Features": "8.0.3",
+ "System.IO.Pipelines": "8.0.0"
}
},
"Microsoft.AspNetCore.Http.Connections.Client": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "pJgTOxuQOb6U5fQ5UxXWzU0wvxfFo9FKokTX71c5MTea8GBUhBulsG82hD23iaXr7x+CKSGiln073IwWpL9wyA==",
+ "resolved": "8.0.3",
+ "contentHash": "WmKLiBVjOcJ3DsIYweka+jjlL06ad+438QYgg+uyEr0QoJtxFfqsJHQoTjQyKGNVsIZj2cTSEnIK0vdqEiXb7Q==",
"dependencies": {
- "Microsoft.AspNetCore.Http.Connections.Common": "6.0.10",
- "Microsoft.Extensions.Logging.Abstractions": "6.0.2",
- "Microsoft.Extensions.Options": "6.0.0"
+ "Microsoft.AspNetCore.Http.Connections.Common": "8.0.3",
+ "Microsoft.Extensions.Logging.Abstractions": "8.0.1",
+ "Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.AspNetCore.Http.Connections.Common": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "scYFKI33ki8QtOCo6jMC60+dEsstJcTl8rjGbk5zh8AfvLDsOy0YyILFla9CMMVGc//LOTkEfm5AOicVwIFUzw==",
+ "resolved": "8.0.3",
+ "contentHash": "gujPT796RA93n79QYA9g0HNGta+Z1BtGD3mpg1BRmSvfwXljA4oGkw7Fm9bi+LgEwwIiHvVAmLVjTQutK+C3eA==",
"dependencies": {
- "Microsoft.AspNetCore.Connections.Abstractions": "6.0.10"
+ "Microsoft.AspNetCore.Connections.Abstractions": "8.0.3",
+ "System.Text.Json": "8.0.3"
}
},
"Microsoft.AspNetCore.SignalR.Client": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "tmY3RRWZPLE3jZ/wdPblHsqRi02ALCa/Ksz9tA4ZPnoJICTZQbccUOzDwHH8u5HpejurQw5r4eHQCQ6ssbJ83w==",
+ "resolved": "8.0.3",
+ "contentHash": "zjQGIt4s+2zIVy0etNRxcMf79JEI4C3zpPwNu3pPFepe7G/T1lbyg6Q/rqW8GTr7SEVHWlwTNRbeaJ5FTENjrA==",
"dependencies": {
- "Microsoft.AspNetCore.Http.Connections.Client": "6.0.10",
- "Microsoft.AspNetCore.SignalR.Client.Core": "6.0.10"
+ "Microsoft.AspNetCore.Http.Connections.Client": "8.0.3",
+ "Microsoft.AspNetCore.SignalR.Client.Core": "8.0.3"
}
},
"Microsoft.AspNetCore.SignalR.Client.Core": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "II+Gw8+50I3VVGkjNvSNNxk0hnZnt5Slf4ZkR89Qj1kbrzktsPMqjHdF6S6DkJEvrpCSIFWktmdXGT4oIwlAKA==",
+ "resolved": "8.0.3",
+ "contentHash": "NxR0+ABjcCPI11c9+OUoNC2SMuQDn/0cbbBPH7x6U8Q5ebNGNMSTcsPPkPI6D16DlmO2djtbcEWWB139los+dA==",
"dependencies": {
- "Microsoft.AspNetCore.SignalR.Common": "6.0.10",
- "Microsoft.AspNetCore.SignalR.Protocols.Json": "6.0.10",
- "Microsoft.Extensions.DependencyInjection": "6.0.1",
- "Microsoft.Extensions.Logging": "6.0.0",
- "System.Threading.Channels": "6.0.0"
+ "Microsoft.AspNetCore.SignalR.Common": "8.0.3",
+ "Microsoft.AspNetCore.SignalR.Protocols.Json": "8.0.3",
+ "Microsoft.Bcl.TimeProvider": "8.0.1",
+ "Microsoft.Extensions.DependencyInjection": "8.0.0",
+ "Microsoft.Extensions.Logging": "8.0.0",
+ "System.Threading.Channels": "8.0.0"
}
},
"Microsoft.AspNetCore.SignalR.Common": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "G0tumlODCHSKJ8lXBUBU6BNQLagPa0qBgSo7fc0RoS5bxZxE8vjrUwLLRCSmfuI+4tFkrxnGg6TMdYITN7NpWw==",
+ "resolved": "8.0.3",
+ "contentHash": "1MX4XXjCH/+ZZWS2zPElNEzLMwszI95iqqjsdTOvaaK7sQLPn8Y41YCOfHU/2lXSWYP8Yc+umyiikoonRedP5Q==",
"dependencies": {
- "Microsoft.AspNetCore.Connections.Abstractions": "6.0.10",
- "Microsoft.Extensions.Options": "6.0.0"
+ "Microsoft.AspNetCore.Connections.Abstractions": "8.0.3",
+ "Microsoft.Extensions.Options": "8.0.2",
+ "System.Text.Json": "8.0.3"
}
},
"Microsoft.AspNetCore.SignalR.Protocols.Json": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "R0JiDQ0CHZ9KPVlYrQdFKE3cknY6dVPB+L8qKueI1eUeF0zjvaxYJ6DccV1Z59GB5Ek3O0Rj0lqS5WFF1G1yYA==",
+ "resolved": "8.0.3",
+ "contentHash": "L1RRxwlzSA/zYFN1fNH+Sfu6snDEf2yQWH0nc2NNvDmwqOu/xVZq3LghWzwpmSdlWZXWZwpq1XCyxTpNtDLkOQ==",
"dependencies": {
- "Microsoft.AspNetCore.SignalR.Common": "6.0.10"
+ "Microsoft.AspNetCore.SignalR.Common": "8.0.3"
}
},
"Microsoft.Bcl.AsyncInterfaces": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
+ "resolved": "8.0.0",
+ "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw=="
+ },
+ "Microsoft.Bcl.TimeProvider": {
+ "type": "Transitive",
+ "resolved": "8.0.1",
+ "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "8.0.0"
+ }
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
@@ -286,13 +307,13 @@
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
+ "resolved": "8.0.1",
+ "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA=="
},
"Microsoft.Extensions.Features": {
"type": "Transitive",
- "resolved": "6.0.10",
- "contentHash": "CMXf1lG9iip0TkpP/ooT+G5uWE9/jFs8uBC2+Qw3EkTc2I7vuh2e+pGVuPAu3I5jig9/WjR7niHDPKtjsOiA9A=="
+ "resolved": "8.0.3",
+ "contentHash": "U1NcOfFSgkJTpdCgRuklSMI0r2kbQuLuEATSdPhgC+aEhvpc/yAD7RvKZVMoBsx/xSn7qJ+qqHczDNbW/o7gGA=="
},
"Microsoft.Extensions.Http": {
"type": "Transitive",
@@ -318,16 +339,16 @@
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
+ "resolved": "8.0.1",
+ "contentHash": "RIFgaqoaINxkM2KTOw72dmilDmTrYA0ns2KW4lDz4gZ2+o6IQ894CzmdL3StM2oh7QQq44nCWiqKqc4qUI9Jmg==",
"dependencies": {
- "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1"
}
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
- "resolved": "8.0.0",
- "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
+ "resolved": "8.0.2",
+ "contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
@@ -357,6 +378,11 @@
"resolved": "6.0.0",
"contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A=="
},
+ "Microsoft.Windows.CsWinRT": {
+ "type": "Transitive",
+ "resolved": "2.0.0",
+ "contentHash": "Z4NdAGpXQbe8bu1amMfqqwjntQy8VsTWYPhcY8KTQCLcxIKzPZm8gsJPt6qIJIJeZd8cR9ig4DQylXNfiXGbaw=="
+ },
"Microsoft.Windows.SDK.Win32Docs": {
"type": "Transitive",
"resolved": "0.1.42-alpha",
@@ -382,18 +408,18 @@
},
"NLog": {
"type": "Transitive",
- "resolved": "4.7.14",
- "contentHash": "R1Nn+Zi2mSiKf82zII+8ZhGTr66CmCDLlpDq/0N0Y22TDrioYeP6t3XWzJVTi3OXaHJ/jiXId/okXQu8e0g3uw=="
+ "resolved": "5.2.8",
+ "contentHash": "jAIELkWBs1CXFPp986KSGpDFQZHCFccO+LMbKBTTNm42KifaI1mYzFMFQQfuGmGMTrCx0TFPhDjHDE4cLAZWiQ=="
},
"NLog.Schema": {
"type": "Transitive",
- "resolved": "5.0.0-rc2",
- "contentHash": "jfOhjdlu+OMKvs2IyUSxMTn2IKXIdNklwGVtALlGM7edHYbhx0pXMk7CRIFFR4VMqVlkVVhAAUct0+a0S+54rg=="
+ "resolved": "5.2.8",
+ "contentHash": "59DAHYtNolhSpKuzjvHtD11xcPd/BwWyV1y7Tlm6nYEyvj8EsQJdponmLYae3i2Q5k9HHwAfRm/MvIvhZPTLBg=="
},
"NuGet.Versioning": {
"type": "Transitive",
- "resolved": "6.3.0",
- "contentHash": "9cQMmhbwR92SSep1y4MMapjrJdcnzcnSWHec3yrB9EMyIjOhiXkwYX9lZ8fkYUxBskwXw8mAEwvY8b7yGRFbAg=="
+ "resolved": "6.9.1",
+ "contentHash": "ypnSvEtpNGo48bAWn95J1oHChycCXcevFSbn53fqzLxlXFSZP7dawu8p/7mHAfGufZQSV2sBpW80XQGIfXO8kQ=="
},
"protobuf-net": {
"type": "Transitive",
@@ -416,39 +442,34 @@
"System.Drawing.Common": "6.0.0"
}
},
- "SharpCompress": {
- "type": "Transitive",
- "resolved": "0.32.2",
- "contentHash": "MSt6TKAg2c0uFFDtf7oACB4DfvXKq5Cvot2e0wjy5nAiVujpIRQ8laiIqCU/VMRGyytasGzg+nODKTkkDjbr/g=="
- },
"SQLitePCLRaw.bundle_green": {
"type": "Transitive",
- "resolved": "2.1.0",
- "contentHash": "VPLK8FzWwJ1RkAWClYOBmjlzFZbz9ME6pdmz0oCoZw736/oIdKldzTxz8S1YGielxYxGHDdK5FczZTOs8LUbbQ==",
+ "resolved": "2.1.5",
+ "contentHash": "TEWM06zE7it0rKSi9au7o9lE2IsLj+MNF5x5DGpTHPw4Io+0hI6Azfi6tCWR2ijS+nqN6L93Q1p+5Rc+dDxXPQ==",
"dependencies": {
- "SQLitePCLRaw.lib.e_sqlite3": "2.1.0",
- "SQLitePCLRaw.provider.e_sqlite3": "2.1.0"
+ "SQLitePCLRaw.lib.e_sqlite3": "2.1.5",
+ "SQLitePCLRaw.provider.e_sqlite3": "2.1.5"
}
},
"SQLitePCLRaw.core": {
"type": "Transitive",
- "resolved": "2.1.0",
- "contentHash": "6khRuPd6ScBUQzV9bti+OtwqwFsMMgJzibhvbgiAIFVQhrowFOCM/Zq0xm00+nVEgEfPwWEOLhYvxs/5j+zAcg==",
+ "resolved": "2.1.5",
+ "contentHash": "gZQMpefwszRkDjnNGxVmykOIaVAzBsR082ODO2myRFcyeSGiv4B1E4Ol+lijqD/SXK/ofyO2/U7dOwVfnzaVdg==",
"dependencies": {
"System.Memory": "4.5.3"
}
},
"SQLitePCLRaw.lib.e_sqlite3": {
"type": "Transitive",
- "resolved": "2.1.0",
- "contentHash": "7p5ltRKUA/uScNRnd20Rg1c7cC4nWwUQIXRFITIR1G0wNCnJ9bj060VanPh2J352KOHpZ3CEOJKu3dMfyY4YMQ=="
+ "resolved": "2.1.5",
+ "contentHash": "Fqp/FQlb+USnEC2qfWOdsY4fFir3sob9BQMgdT3rcamUAoB7id8V0WknWdsFnE4TXBKDiM79+oPoZoHAuU/dsg=="
},
"SQLitePCLRaw.provider.e_sqlite3": {
"type": "Transitive",
- "resolved": "2.1.0",
- "contentHash": "OwvGO4R0ujrdtTiIAiCN7C+2yVcLHtaF1ykcVe/Fb5KDqm1vfUxWinNYvw0S7NchpfTvR/9V9yugzqYTIvtTyA==",
+ "resolved": "2.1.5",
+ "contentHash": "/HG+f7/NZn+51NItD3LKJCMWL5AI9aEkqIYplr4eHeWmMf+TEn8cYxkhQ1jUA9bSUscCHlZbNt+nyegZxQ1SLw==",
"dependencies": {
- "SQLitePCLRaw.core": "2.1.0"
+ "SQLitePCLRaw.core": "2.1.5"
}
},
"System.CodeDom": {
@@ -475,8 +496,8 @@
},
"System.IO.Pipelines": {
"type": "Transitive",
- "resolved": "6.0.3",
- "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw=="
+ "resolved": "8.0.0",
+ "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA=="
},
"System.Management": {
"type": "Transitive",
@@ -530,8 +551,8 @@
},
"System.Threading.Channels": {
"type": "Transitive",
- "resolved": "6.0.0",
- "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q=="
+ "resolved": "8.0.0",
+ "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA=="
},
"System.Windows.Extensions": {
"type": "Transitive",
@@ -569,12 +590,11 @@
"musicx.core": {
"type": "Project",
"dependencies": {
- "Clowd.Squirrel": "[3.0.210-g5f9f594, )",
"DiscordRPC": "[1.0.0, )",
- "Microsoft.AspNetCore.SignalR.Client": "[6.0.10, )",
+ "Microsoft.AspNetCore.SignalR.Client": "[8.0.3, )",
"MusicX.Shared": "[1.0.0, )",
- "NLog": "[4.7.14, )",
- "NLog.Schema": "[5.0.0-rc2, )",
+ "NLog": "[5.2.8, )",
+ "NLog.Schema": "[5.2.8, )",
"System.Linq.Async": "[6.0.1, )",
"VkNet": "[1.70.0, )",
"VkNet.AudioBypassService": "[1.7.2, )",
@@ -613,16 +633,7 @@
}
}
},
- "net7.0-windows10.0.22000/win10-x64": {
- "FFmpegInteropX.Desktop": {
- "type": "Direct",
- "requested": "[2.0.0-pre2, )",
- "resolved": "2.0.0-pre2",
- "contentHash": "7+TF/6sw9XArcIvgukUdEJ9IXHwLwjNbyHouvYXu9ndDz7knPMWxes/XGrU1GOgjQ7DWKBxp4LEt68oeFjdbmA==",
- "dependencies": {
- "FFmpegInteropX.FFmpegUWP": "5.1.100"
- }
- },
+ "net7.0-windows10.0.22621/win10-x64": {
"Microsoft.VCRTForwarders.140": {
"type": "Direct",
"requested": "[1.0.8-pre, )",
@@ -631,17 +642,25 @@
},
"System.Text.Encoding.CodePages": {
"type": "Direct",
- "requested": "[6.0.0, )",
- "resolved": "6.0.0",
- "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==",
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg=="
+ },
+ "FFmpegInteropX.Desktop.FFmpeg": {
+ "type": "Transitive",
+ "resolved": "5.1.100-pre3",
+ "contentHash": "0s2EqrsMBsR37eP0LL/0iIr5qmY3txHaP20K21QaZbVgve4sEndrRk/d5kkD7Ks/S73jIrIuXY3UvJcOz65aWg==",
"dependencies": {
- "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ "Microsoft.Windows.CsWinRT": "2.0.0"
}
},
- "FFmpegInteropX.FFmpegUWP": {
+ "FFmpegInteropX.Desktop.Lib": {
"type": "Transitive",
- "resolved": "5.1.100",
- "contentHash": "Pi+ifqTU3VpkvfTkZIR62+fck22HaA54WJeLyumI/EWgXM+1euSihsTX20oi3aQKQAz8Qh9weNLhntD4g3pmvA=="
+ "resolved": "2.0.0-pre3",
+ "contentHash": "LOInPKHQtH0QEDtqCJx9s9m2c54LT8tsN+rly+W1oQ0rUfElvrmeExvDE0Y6/X/Ju5EwXWm+lN9ZgVS8ng+YPA==",
+ "dependencies": {
+ "Microsoft.Windows.CsWinRT": "2.0.0"
+ }
},
"Microsoft.Win32.Registry": {
"type": "Transitive",
@@ -659,8 +678,8 @@
},
"SQLitePCLRaw.lib.e_sqlite3": {
"type": "Transitive",
- "resolved": "2.1.0",
- "contentHash": "7p5ltRKUA/uScNRnd20Rg1c7cC4nWwUQIXRFITIR1G0wNCnJ9bj060VanPh2J352KOHpZ3CEOJKu3dMfyY4YMQ=="
+ "resolved": "2.1.5",
+ "contentHash": "Fqp/FQlb+USnEC2qfWOdsY4fFir3sob9BQMgdT3rcamUAoB7id8V0WknWdsFnE4TXBKDiM79+oPoZoHAuU/dsg=="
},
"System.Drawing.Common": {
"type": "Transitive",
diff --git a/VkNet.AudioBypassService/VkNet.AudioBypassService.csproj b/VkNet.AudioBypassService/VkNet.AudioBypassService.csproj
index 18a86531..f79e4a6f 100644
--- a/VkNet.AudioBypassService/VkNet.AudioBypassService.csproj
+++ b/VkNet.AudioBypassService/VkNet.AudioBypassService.csproj
@@ -12,6 +12,7 @@
vknet;vk;api;audio;bypass
MIT
Debug;Release
+ x64