From da5a4b14df291e7b540bc6e0adb0f62f83417798 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:05:43 +0300 Subject: [PATCH] [bink] synced host movie playback to the guest audio clock --- src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs | 4 +- src/SharpEmu.Libs/Bink/BinkFramePlayback.cs | 136 ++++- .../Bink/FfmpegNativeBinkFrameSource.cs | 515 +++++++++++++++--- 3 files changed, 582 insertions(+), 73 deletions(-) diff --git a/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs b/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs index 36124b25..907c2f08 100644 --- a/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs +++ b/src/SharpEmu.Libs/Bink/Bink2MovieBridge.cs @@ -133,10 +133,12 @@ internal static class Bink2MovieBridge if (_playback.IsFinished) { var completedPath = _activePath; + var progress = _playback.PlaybackProgress; CloseActiveLocked(); Console.Error.WriteLine( "[LOADER][INFO] Bink2 bridge completed: " + - Path.GetFileName(completedPath)); + $"{Path.GetFileName(completedPath)} after " + + $"{progress.Seconds:F2}s at frame {progress.FrameIndex}"); AttachNextQueuedMovieLocked(); } return false; diff --git a/src/SharpEmu.Libs/Bink/BinkFramePlayback.cs b/src/SharpEmu.Libs/Bink/BinkFramePlayback.cs index aa0600f4..1f392e6e 100644 --- a/src/SharpEmu.Libs/Bink/BinkFramePlayback.cs +++ b/src/SharpEmu.Libs/Bink/BinkFramePlayback.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later using System.Diagnostics; +using SharpEmu.HLE.Host; namespace SharpEmu.Libs.Bink; @@ -36,6 +37,8 @@ internal sealed class BinkFramePlayback : IDisposable private long _currentFrameIndex = -1; private long _nextDecodedFrameIndex; private long _playbackStartTimestamp; + private double _audioStartSeconds; + private long _lastSkewTraceTimestamp; private bool _playbackClockStarted; private bool _decoderCompleted; private bool _stopRequested; @@ -83,6 +86,26 @@ internal sealed class BinkFramePlayback : IDisposable } } + /// + /// Wall-clock seconds since the first frame was presented, and the index of + /// the last frame shown. Playback is on its own time base when these agree + /// with the movie's frame rate. + /// + internal (double Seconds, long FrameIndex) PlaybackProgress + { + get + { + lock (_gate) + { + return ( + _playbackClockStarted + ? Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds + : 0, + _currentFrameIndex); + } + } + } + internal bool TryGetFrame( bool advanceClock, out byte[] pixels, @@ -118,14 +141,13 @@ internal sealed class BinkFramePlayback : IDisposable if (advanceClock && !_playbackClockStarted) { _playbackStartTimestamp = Stopwatch.GetTimestamp(); + _audioStartSeconds = GuestAudioClock.PlayedSeconds; _playbackClockStarted = true; } - var elapsedSeconds = _playbackClockStarted - ? Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds - : 0; - var targetFrameIndex = (long)Math.Floor( - elapsedSeconds * FramesPerSecondNumerator / FramesPerSecondDenominator); + var elapsedSeconds = CurrentPlaybackSecondsLocked(); + TraceClockSkewLocked(); + var targetFrameIndex = CurrentTargetFrameIndexLocked(); DecodedFrame? replacement = null; while (_decodedFrames.Count > 0 && _decodedFrames.Peek().Index <= targetFrameIndex) @@ -166,6 +188,86 @@ internal sealed class BinkFramePlayback : IDisposable } } + /// + /// Time base for playback. A host-decoded movie runs on whatever clock it is + /// given, but the audio that belongs to it comes from the guest, which does + /// not advance at wall-clock rate on a slow frame. Following the audio keeps + /// the two together; SHARPEMU_MOVIE_CLOCK=wall restores the old behaviour. + /// + private static readonly bool _followGuestAudioClock = !string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_MOVIE_CLOCK"), + "wall", + StringComparison.OrdinalIgnoreCase); + + /// + /// Seconds of playback elapsed on the movie's time base. Falls back to wall + /// clock whenever guest audio is not flowing: a movie whose audio never + /// starts — or stops early — must still finish rather than hang on a clock + /// that will never advance again. + /// + private double CurrentPlaybackSecondsLocked() + { + if (!_playbackClockStarted) + { + return 0; + } + + var wallSeconds = Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds; + if (!_followGuestAudioClock || !GuestAudioClock.IsRunning) + { + return wallSeconds; + } + + return Math.Clamp(GuestAudioClock.PlayedSeconds - _audioStartSeconds, 0, wallSeconds); + } + + private static readonly bool _traceClockSkew = string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_MOVIE_SYNC"), + "1", + StringComparison.Ordinal); + + /// + /// Logs how far the movie's wall clock has drifted from the guest audio the + /// movie is supposed to be in step with. A skew that is flat across playback + /// is a late audio start; one that grows is a rate mismatch, and the two need + /// different fixes. Caller holds . + /// + private void TraceClockSkewLocked() + { + if (!_traceClockSkew || !_playbackClockStarted) + { + return; + } + + var now = Stopwatch.GetTimestamp(); + if (_lastSkewTraceTimestamp != 0 && + Stopwatch.GetElapsedTime(_lastSkewTraceTimestamp) < TimeSpan.FromSeconds(1)) + { + return; + } + + _lastSkewTraceTimestamp = now; + var wallSeconds = Stopwatch.GetElapsedTime(_playbackStartTimestamp).TotalSeconds; + var audioSeconds = GuestAudioClock.PlayedSeconds - _audioStartSeconds; + Console.Error.WriteLine( + $"[PERF][MOVIE] wall_s={wallSeconds:F2} audio_s={audioSeconds:F2} " + + $"playback_s={CurrentPlaybackSecondsLocked():F2} " + + $"skew_s={wallSeconds - audioSeconds:F2} frame={_currentFrameIndex} " + + $"audio_running={GuestAudioClock.IsRunning}"); + } + + /// + /// The frame the movie's own time base says should be on screen right now. + /// Returns -1 until the first frame is presented, so the queue prefills + /// instead of instantly declaring everything late. + /// + private long CurrentTargetFrameIndexLocked() => + _playbackClockStarted + ? (long)Math.Floor( + CurrentPlaybackSecondsLocked() * + FramesPerSecondNumerator / FramesPerSecondDenominator) + : -1; + private void DecodeLoop() { try @@ -199,8 +301,28 @@ internal sealed class BinkFramePlayback : IDisposable lock (_gate) { - _decodedFrames.Enqueue(new DecodedFrame( - _nextDecodedFrameIndex++, destination)); + var frameIndex = _nextDecodedFrameIndex++; + + // Frames are pulled once per guest flip, so a title running + // well under the movie's frame rate cannot drain a queue + // this shallow fast enough and the movie stretches past its + // real duration — audio finishes while the last picture sits + // on screen and the next movie starts late. Once the clock + // has passed a queued frame it can never be shown, so retire + // it in favour of this newer one. Only superseded frames are + // dropped, never the newest, so a decoder that cannot keep + // up still advances the picture instead of freezing it. + var targetFrameIndex = CurrentTargetFrameIndexLocked(); + if (frameIndex <= targetFrameIndex) + { + while (_decodedFrames.Count > 0 && + _decodedFrames.Peek().Index <= targetFrameIndex) + { + _freeBuffers.Enqueue(_decodedFrames.Dequeue().Pixels); + } + } + + _decodedFrames.Enqueue(new DecodedFrame(frameIndex, destination)); Monitor.PulseAll(_gate); } } diff --git a/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs b/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs index ab0bd581..0551ad70 100644 --- a/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs +++ b/src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs @@ -1,7 +1,9 @@ // Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +using System.Buffers; using FFmpeg.AutoGen; +using SharpEmu.HLE.Host; namespace SharpEmu.Libs.Bink; @@ -13,13 +15,29 @@ namespace SharpEmu.Libs.Bink; /// internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder { + private const int OutputAudioChannels = 2; + private const int OutputAudioBytesPerSample = sizeof(short); + + private readonly object _decodeGate = new(); private AVFormatContext* _formatContext; private AVCodecContext* _codecContext; + private AVCodecContext* _audioCodecContext; private SwsContext* _swsContext; + private SwrContext* _swrContext; private AVFrame* _frame; + private AVFrame* _audioFrame; private AVPacket* _packet; + private IHostAudioStream? _audioStream; private readonly int _videoStreamIndex; + private readonly int _audioStreamIndex; + private readonly int _audioOutputSampleRate; + private AVChannelLayout _swrInputLayout; + private AVSampleFormat _swrInputFormat = AVSampleFormat.AV_SAMPLE_FMT_NONE; + private int _swrInputSampleRate; + private bool _swrInputLayoutValid; private bool _draining; + private bool _audioDraining; + private bool _audioFailed; private int _disposed; public uint Width { get; } @@ -34,6 +52,10 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder AVFormatContext* formatContext, AVCodecContext* codecContext, int videoStreamIndex, + AVCodecContext* audioCodecContext, + int audioStreamIndex, + IHostAudioStream? audioStream, + int audioOutputSampleRate, uint width, uint height, uint framesPerSecondNumerator, @@ -42,11 +64,16 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder _formatContext = formatContext; _codecContext = codecContext; _videoStreamIndex = videoStreamIndex; + _audioCodecContext = audioCodecContext; + _audioStreamIndex = audioStreamIndex; + _audioStream = audioStream; + _audioOutputSampleRate = audioOutputSampleRate; Width = width; Height = height; FramesPerSecondNumerator = framesPerSecondNumerator; FramesPerSecondDenominator = framesPerSecondDenominator; _frame = ffmpeg.av_frame_alloc(); + _audioFrame = audioCodecContext is null ? null : ffmpeg.av_frame_alloc(); _packet = ffmpeg.av_packet_alloc(); } @@ -93,6 +120,8 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder AVFormatContext* formatContext = null; AVCodecContext* codecContext = null; + AVCodecContext* audioCodecContext = null; + IHostAudioStream? audioStream = null; try { if (ffmpeg.avformat_open_input(&formatContext, path, null, null) < 0) @@ -151,6 +180,28 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder frameRate = new AVRational { num = 30, den = 1 }; } + var audioStreamIndex = TryOpenAudioDecoder( + formatContext, + out audioCodecContext, + out var audioOutputSampleRate); + if (audioStreamIndex >= 0 && audioCodecContext is not null) + { + try + { + audioStream = HostPlatform.Current.Audio.OpenStereoPcm16Stream( + checked((uint)audioOutputSampleRate)); + } + catch (Exception exception) when (exception is InvalidOperationException or + ArgumentOutOfRangeException) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Bink audio output unavailable: {exception.Message}"); + ffmpeg.avcodec_free_context(&audioCodecContext); + audioStreamIndex = -1; + audioOutputSampleRate = 0; + } + } + var outputWidth = (uint)codecContext->width; var outputHeight = (uint)codecContext->height; if (maximumWidth > 0 && maximumHeight > 0 && @@ -175,12 +226,18 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder formatContext, codecContext, videoStreamIndex, + audioCodecContext, + audioStreamIndex, + audioStream, + audioOutputSampleRate, outputWidth, outputHeight, (uint)frameRate.num, (uint)frameRate.den); formatContext = null; codecContext = null; + audioCodecContext = null; + audioStream = null; return true; } catch (DllNotFoundException) @@ -194,6 +251,13 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder ffmpeg.avcodec_free_context(&codecContext); } + if (audioCodecContext is not null) + { + ffmpeg.avcodec_free_context(&audioCodecContext); + } + + audioStream?.Dispose(); + if (formatContext is not null) { ffmpeg.avformat_close_input(&formatContext); @@ -201,52 +265,102 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder } } + private static int TryOpenAudioDecoder( + AVFormatContext* formatContext, + out AVCodecContext* codecContext, + out int outputSampleRate) + { + codecContext = null; + outputSampleRate = 0; + + AVCodec* decoder = null; + var streamIndex = ffmpeg.av_find_best_stream( + formatContext, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, -1, &decoder, 0); + if (streamIndex < 0 || decoder is null) + { + return -1; + } + + var candidate = ffmpeg.avcodec_alloc_context3(decoder); + if (candidate is null) + { + return -1; + } + + var stream = formatContext->streams[streamIndex]; + if (ffmpeg.avcodec_parameters_to_context(candidate, stream->codecpar) < 0) + { + ffmpeg.avcodec_free_context(&candidate); + return -1; + } + + candidate->thread_count = 0; + candidate->thread_type = ffmpeg.FF_THREAD_FRAME | ffmpeg.FF_THREAD_SLICE; + if (ffmpeg.avcodec_open2(candidate, decoder, null) < 0) + { + ffmpeg.avcodec_free_context(&candidate); + return -1; + } + + outputSampleRate = candidate->sample_rate > 0 ? candidate->sample_rate : 48_000; + codecContext = candidate; + return streamIndex; + } + public bool TryDecodeNextFrame(Span destination) { - var stride = checked((int)(Width * 4)); - var required = (long)stride * Height; - if (destination.Length < required) + lock (_decodeGate) { - return false; - } + if (Volatile.Read(ref _disposed) != 0) + { + return false; + } - if (!TryReceiveFrame()) - { - return false; - } + var stride = checked((int)(Width * 4)); + var required = (long)stride * Height; + if (destination.Length < required) + { + return false; + } - _swsContext = ffmpeg.sws_getCachedContext( - _swsContext, - _frame->width, - _frame->height, - (AVPixelFormat)_frame->format, - (int)Width, - (int)Height, - AVPixelFormat.AV_PIX_FMT_BGRA, - ffmpeg.SWS_FAST_BILINEAR, - null, - null, - null); - if (_swsContext is null) - { - ffmpeg.av_frame_unref(_frame); - return false; - } + if (!TryReceiveFrame()) + { + return false; + } - fixed (byte* destinationPointer = destination) - { - var destinationPlanes = new byte*[4] { destinationPointer, null, null, null }; - var destinationStrides = new int[4] { stride, 0, 0, 0 }; - var convertedRows = ffmpeg.sws_scale( + _swsContext = ffmpeg.sws_getCachedContext( _swsContext, - _frame->data, - _frame->linesize, - 0, + _frame->width, _frame->height, - destinationPlanes, - destinationStrides); - ffmpeg.av_frame_unref(_frame); - return convertedRows == (int)Height; + (AVPixelFormat)_frame->format, + (int)Width, + (int)Height, + AVPixelFormat.AV_PIX_FMT_BGRA, + ffmpeg.SWS_FAST_BILINEAR, + null, + null, + null); + if (_swsContext is null) + { + ffmpeg.av_frame_unref(_frame); + return false; + } + + fixed (byte* destinationPointer = destination) + { + var destinationPlanes = new byte*[4] { destinationPointer, null, null, null }; + var destinationStrides = new int[4] { stride, 0, 0, 0 }; + var convertedRows = ffmpeg.sws_scale( + _swsContext, + _frame->data, + _frame->linesize, + 0, + _frame->height, + destinationPlanes, + destinationStrides); + ffmpeg.av_frame_unref(_frame); + return convertedRows == (int)Height; + } } } @@ -291,9 +405,17 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder { _draining = true; ffmpeg.avcodec_send_packet(_codecContext, null); + DrainAudioDecoder(); return true; } + if (_packet->stream_index == _audioStreamIndex) + { + DecodeAudioPacket(_packet); + ffmpeg.av_packet_unref(_packet); + continue; + } + if (_packet->stream_index != _videoStreamIndex) { ffmpeg.av_packet_unref(_packet); @@ -311,6 +433,248 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder } } + private void DecodeAudioPacket(AVPacket* packet) + { + if (_audioCodecContext is null || _audioFrame is null || _audioFailed) + { + return; + } + + var sendResult = ffmpeg.avcodec_send_packet(_audioCodecContext, packet); + if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + DrainAvailableAudioFrames(); + sendResult = ffmpeg.avcodec_send_packet(_audioCodecContext, packet); + } + + if (sendResult < 0) + { + DisableAudio("packet decode failed"); + return; + } + + DrainAvailableAudioFrames(); + } + + private void DrainAudioDecoder() + { + if (_audioCodecContext is null || _audioFrame is null || + _audioDraining || _audioFailed) + { + return; + } + + _audioDraining = true; + var sendResult = ffmpeg.avcodec_send_packet(_audioCodecContext, null); + if (sendResult >= 0 || sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + DrainAvailableAudioFrames(); + } + } + + private void DrainAvailableAudioFrames() + { + while (_audioCodecContext is not null && _audioFrame is not null) + { + var receiveResult = ffmpeg.avcodec_receive_frame(_audioCodecContext, _audioFrame); + if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || + receiveResult == ffmpeg.AVERROR_EOF) + { + return; + } + + if (receiveResult < 0) + { + DisableAudio("frame decode failed"); + return; + } + + if (!SubmitAudioFrame()) + { + ffmpeg.av_frame_unref(_audioFrame); + DisableAudio("host submission failed"); + return; + } + + ffmpeg.av_frame_unref(_audioFrame); + } + } + + private bool SubmitAudioFrame() + { + if (_audioStream is null || _audioFrame is null || + _audioFrame->nb_samples <= 0 || _audioFrame->extended_data is null) + { + return true; + } + + var sampleRate = _audioFrame->sample_rate > 0 + ? _audioFrame->sample_rate + : _audioCodecContext->sample_rate; + if (sampleRate <= 0) + { + return false; + } + + var inputLayout = _audioFrame->ch_layout; + var ownsInputLayout = false; + if (ffmpeg.av_channel_layout_check(&inputLayout) == 0) + { + inputLayout = _audioCodecContext->ch_layout; + } + if (ffmpeg.av_channel_layout_check(&inputLayout) == 0) + { + ffmpeg.av_channel_layout_default( + &inputLayout, + Math.Max(1, _audioFrame->ch_layout.nb_channels)); + ownsInputLayout = true; + } + + try + { + if (!EnsureAudioResampler( + &inputLayout, + (AVSampleFormat)_audioFrame->format, + sampleRate)) + { + return false; + } + + var maximumSamples = ffmpeg.swr_get_out_samples( + _swrContext, _audioFrame->nb_samples); + if (maximumSamples <= 0) + { + return true; + } + + var outputBytes = checked( + maximumSamples * OutputAudioChannels * OutputAudioBytesPerSample); + var buffer = ArrayPool.Shared.Rent(outputBytes); + try + { + fixed (byte* output = buffer) + { + var outputPlanes = stackalloc byte*[1]; + outputPlanes[0] = output; + var convertedSamples = ffmpeg.swr_convert( + _swrContext, + outputPlanes, + maximumSamples, + _audioFrame->extended_data, + _audioFrame->nb_samples); + if (convertedSamples < 0) + { + return false; + } + + var convertedBytes = checked( + convertedSamples * OutputAudioChannels * OutputAudioBytesPerSample); + return _audioStream.Submit(buffer.AsSpan(0, convertedBytes)); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + if (ownsInputLayout) + { + ffmpeg.av_channel_layout_uninit(&inputLayout); + } + } + } + + private bool EnsureAudioResampler( + AVChannelLayout* inputLayout, + AVSampleFormat inputFormat, + int inputSampleRate) + { + var storedInputLayout = _swrInputLayout; + if (_swrContext is not null && + _swrInputFormat == inputFormat && + _swrInputSampleRate == inputSampleRate && + ffmpeg.av_channel_layout_compare(&storedInputLayout, inputLayout) == 0) + { + return true; + } + + FreeAudioResampler(); + + AVChannelLayout copiedInputLayout = default; + if (ffmpeg.av_channel_layout_copy(&copiedInputLayout, inputLayout) < 0) + { + return false; + } + + AVChannelLayout outputLayout = default; + ffmpeg.av_channel_layout_default(&outputLayout, OutputAudioChannels); + SwrContext* context = null; + var allocateResult = ffmpeg.swr_alloc_set_opts2( + &context, + &outputLayout, + AVSampleFormat.AV_SAMPLE_FMT_S16, + _audioOutputSampleRate, + &copiedInputLayout, + inputFormat, + inputSampleRate, + 0, + null); + ffmpeg.av_channel_layout_uninit(&outputLayout); + if (allocateResult < 0 || context is null || ffmpeg.swr_init(context) < 0) + { + if (context is not null) + { + ffmpeg.swr_free(&context); + } + ffmpeg.av_channel_layout_uninit(&copiedInputLayout); + return false; + } + + _swrContext = context; + _swrInputLayout = copiedInputLayout; + _swrInputLayoutValid = true; + _swrInputFormat = inputFormat; + _swrInputSampleRate = inputSampleRate; + return true; + } + + private void DisableAudio(string reason) + { + if (_audioFailed) + { + return; + } + + _audioFailed = true; + Console.Error.WriteLine($"[LOADER][WARN] Bink audio disabled: {reason}."); + FreeAudioResampler(); + _audioStream?.Dispose(); + _audioStream = null; + } + + private void FreeAudioResampler() + { + if (_swrContext is not null) + { + var context = _swrContext; + ffmpeg.swr_free(&context); + _swrContext = null; + } + + if (_swrInputLayoutValid) + { + var inputLayout = _swrInputLayout; + ffmpeg.av_channel_layout_uninit(&inputLayout); + _swrInputLayout = default; + _swrInputLayoutValid = false; + } + + _swrInputFormat = AVSampleFormat.AV_SAMPLE_FMT_NONE; + _swrInputSampleRate = 0; + } + public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) != 0) @@ -318,38 +682,59 @@ internal sealed unsafe class FfmpegNativeBinkFrameSource : IBinkFrameDecoder return; } - if (_swsContext is not null) + lock (_decodeGate) { - ffmpeg.sws_freeContext(_swsContext); - _swsContext = null; - } + FreeAudioResampler(); + _audioStream?.Dispose(); + _audioStream = null; - if (_packet is not null) - { - var packet = _packet; - ffmpeg.av_packet_free(&packet); - _packet = null; - } + if (_swsContext is not null) + { + ffmpeg.sws_freeContext(_swsContext); + _swsContext = null; + } - if (_frame is not null) - { - var frame = _frame; - ffmpeg.av_frame_free(&frame); - _frame = null; - } + if (_packet is not null) + { + var packet = _packet; + ffmpeg.av_packet_free(&packet); + _packet = null; + } - if (_codecContext is not null) - { - var codecContext = _codecContext; - ffmpeg.avcodec_free_context(&codecContext); - _codecContext = null; - } + if (_frame is not null) + { + var frame = _frame; + ffmpeg.av_frame_free(&frame); + _frame = null; + } - if (_formatContext is not null) - { - var formatContext = _formatContext; - ffmpeg.avformat_close_input(&formatContext); - _formatContext = null; + if (_audioFrame is not null) + { + var frame = _audioFrame; + ffmpeg.av_frame_free(&frame); + _audioFrame = null; + } + + if (_codecContext is not null) + { + var codecContext = _codecContext; + ffmpeg.avcodec_free_context(&codecContext); + _codecContext = null; + } + + if (_audioCodecContext is not null) + { + var codecContext = _audioCodecContext; + ffmpeg.avcodec_free_context(&codecContext); + _audioCodecContext = null; + } + + if (_formatContext is not null) + { + var formatContext = _formatContext; + ffmpeg.avformat_close_input(&formatContext); + _formatContext = null; + } } } }