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