Files
ADOFAIRenderer/Util/FFmpeg/FFmpegNativeDecoder.cs
T

863 lines
33 KiB
C#

using System;
using FFmpeg.AutoGen;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
namespace ADOFAIRenderer.Util.FFmpeg
{
/// <summary>
/// FFmpeg의 C 네이티브 API를 사용하여 비디오 파일을 스트리밍 방식으로 디코딩하는 클래스입니다.
/// 플레이어 형태가 아니며, 필요할 때 프레임을 순차적으로 읽거나 앞으로 탐색(Seek/Skip)할 수 있습니다.
/// </summary>
public sealed unsafe class FFmpegNativeDecoder : IDisposable
{
private AVFormatContext* _pFormatContext = null;
private AVCodecContext* _pCodecContext = null;
private AVFrame* _pDecodedFrame = null;
private AVFrame* _pConvertedFrame = null;
private SwsContext* _pSwsContext = null;
private AVPacket* _pPacket = null;
private int _videoStreamIndex = -1;
private long _currentFrameIndex;
private AVRational _timeBase;
private bool _isOpened;
private bool _disposed;
private bool _isDraining;
private readonly object _lock = new();
// 탐색(Seek) 후 매칭되는 프레임을 보관하기 위한 캐시 프레임
private AVFrame* _pCachedFrame = null;
private bool _hasCachedFrame;
// SwsContext 갱신 감지용 필드
private AVPixelFormat _srcPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_NONE;
private int _srcWidth;
private int _srcHeight;
/// <summary>비디오 가로 해상도 (픽셀).</summary>
public int Width { get; private set; }
/// <summary>비디오 세로 해상도 (픽셀).</summary>
public int Height { get; private set; }
/// <summary>초당 프레임 수.</summary>
public double FPS { get; private set; }
/// <summary>전체 재생 시간 (초).</summary>
public double Duration { get; private set; }
/// <summary>전체 프레임 수 (추정치).</summary>
public long TotalFrames { get; private set; }
/// <summary>현재 읽은 프레임의 인덱스.</summary>
public long CurrentFrameIndex => _currentFrameIndex;
/// <summary>현재 재생 시간 (초).</summary>
public double CurrentTime => _currentFrameIndex / FPS;
/// <summary>비디오 원본 코덱의 픽셀 포맷.</summary>
public AVPixelFormat PixelFormat => _pCodecContext != null ? _pCodecContext->pix_fmt : AVPixelFormat.AV_PIX_FMT_NONE;
/// <summary>비디오 원본 코덱의 색상 범위.</summary>
public AVColorRange ColorRange => _pCodecContext != null ? _pCodecContext->color_range : AVColorRange.AVCOL_RANGE_UNSPECIFIED;
public FFmpegNativeDecoder(string videoPath)
{
Open(videoPath);
}
private void Open(string videoPath)
{
lock (_lock)
{
if (_isOpened || _disposed) return;
try
{
fixed (AVFormatContext** ppFormatCtx = &_pFormatContext)
{
int ret = ffmpeg.avformat_open_input(ppFormatCtx, videoPath, null, null);
if (ret < 0)
throw new Exception($"비디오 파일을 열 수 없습니다: {videoPath} ({GetFFmpegErrorString(ret)})");
}
if (ffmpeg.avformat_find_stream_info(_pFormatContext, null) < 0)
throw new Exception("스트림 정보를 읽을 수 없습니다.");
_videoStreamIndex = -1;
for (int i = 0; i < _pFormatContext->nb_streams; i++)
{
if (_pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
_videoStreamIndex = i;
break;
}
}
if (_videoStreamIndex == -1)
throw new Exception("비디오 스트림을 찾을 수 없습니다.");
var stream = _pFormatContext->streams[_videoStreamIndex];
_timeBase = stream->time_base;
// FPS 계산
if (stream->r_frame_rate.num > 0 && stream->r_frame_rate.den > 0)
{
FPS = ffmpeg.av_q2d(stream->r_frame_rate);
}
else if (stream->avg_frame_rate.num > 0 && stream->avg_frame_rate.den > 0)
{
FPS = ffmpeg.av_q2d(stream->avg_frame_rate);
}
else
{
FPS = 30.0; // 기본값 폴백
}
var codecId = stream->codecpar->codec_id;
var decoder = ffmpeg.avcodec_find_decoder(codecId);
if (decoder == null)
throw new Exception($"지원하지 않는 비디오 코덱입니다: {codecId}");
_pCodecContext = ffmpeg.avcodec_alloc_context3(decoder);
if (_pCodecContext == null)
throw new Exception("코덱 컨텍스트를 할당할 수 없습니다.");
if (ffmpeg.avcodec_parameters_to_context(_pCodecContext, stream->codecpar) < 0)
throw new Exception("코덱 파라미터를 복사할 수 없습니다.");
// 디코딩 성능 향상을 위한 멀티스레딩 설정 (자동)
_pCodecContext->thread_count = 0;
_pCodecContext->thread_type = ffmpeg.FF_THREAD_FRAME;
if (ffmpeg.avcodec_open2(_pCodecContext, decoder, null) < 0)
throw new Exception("코덱을 열 수 없습니다.");
Width = _pCodecContext->width;
Height = _pCodecContext->height;
// 총 프레임 수 및 재생 시간 계산
if (stream->nb_frames > 0)
{
TotalFrames = stream->nb_frames;
}
else if (stream->duration > 0)
{
TotalFrames = (long)Math.Round(stream->duration * ffmpeg.av_q2d(stream->time_base) * FPS);
}
else if (_pFormatContext->duration > 0)
{
TotalFrames = (long)Math.Round((_pFormatContext->duration / (double)ffmpeg.AV_TIME_BASE) * FPS);
}
else
{
TotalFrames = 0;
}
Duration = _pFormatContext->duration > 0
? _pFormatContext->duration / (double)ffmpeg.AV_TIME_BASE
: (stream->duration > 0 ? stream->duration * ffmpeg.av_q2d(stream->time_base) : 0);
_pDecodedFrame = ffmpeg.av_frame_alloc();
_pConvertedFrame = ffmpeg.av_frame_alloc();
_pCachedFrame = ffmpeg.av_frame_alloc();
_pPacket = ffmpeg.av_packet_alloc();
_currentFrameIndex = 0;
_isOpened = true;
_isDraining = false;
}
catch (Exception)
{
Dispose();
throw;
}
}
}
private bool ReadAndSendPacket()
{
while (true)
{
ffmpeg.av_packet_unref(_pPacket);
int ret = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
if (ret < 0) return false; // EOF 또는 에러
if (_pPacket->stream_index == _videoStreamIndex)
{
ret = ffmpeg.avcodec_send_packet(_pCodecContext, _pPacket);
ffmpeg.av_packet_unref(_pPacket);
if (ret < 0)
{
throw new Exception($"디코더에 패킷을 전송하는 중 오류 발생: {GetFFmpegErrorString(ret)}");
}
return true;
}
}
}
private void InitOrUpdateSwsContext(AVPixelFormat srcFormat, int srcWidth, int srcHeight, AVPixelFormat dstFormat)
{
if (_pSwsContext != null && _srcPixFmt == srcFormat && _srcWidth == srcWidth && _srcHeight == srcHeight && _dstPixFmt == dstFormat)
{
return;
}
if (_pSwsContext != null)
{
ffmpeg.sws_freeContext(_pSwsContext);
_pSwsContext = null;
}
_srcPixFmt = srcFormat;
_srcWidth = srcWidth;
_srcHeight = srcHeight;
_dstPixFmt = dstFormat;
_pSwsContext = ffmpeg.sws_getContext(
srcWidth, srcHeight, srcFormat,
Width, Height, dstFormat,
(int)SwsFlags.SWS_BILINEAR, null, null, null
);
if (_pSwsContext == null)
{
throw new Exception("SwsContext를 초기화할 수 없습니다.");
}
}
private double GetTimestampAndUpdateIndex(AVFrame* frame)
{
long ptsValue = frame->best_effort_timestamp;
if (ptsValue == ffmpeg.AV_NOPTS_VALUE)
{
ptsValue = frame->pts;
}
double timestamp;
if (ptsValue == ffmpeg.AV_NOPTS_VALUE)
{
timestamp = _currentFrameIndex / FPS;
_currentFrameIndex++;
}
else
{
timestamp = ptsValue * ffmpeg.av_q2d(_timeBase);
_currentFrameIndex = (long)Math.Round(timestamp * FPS);
}
return timestamp;
}
/// <summary>
/// 다음 비디오 프레임을 읽고 RGBA 형태로 복사합니다.
/// </summary>
/// <param name="outBuffer">RGBA 데이터를 저장할 버퍼의 포인터</param>
/// <param name="outStride">출력 버퍼의 가로 한 행의 바이트 크기 (일반적으로 Width * 4)</param>
/// <param name="vflip">Unity 텍스처 시스템에 맞게 상하 flip을 적용할지 여부</param>
/// <param name="timestamp">출력된 프레임의 타임스탬프 (초)</param>
/// <returns>프레임을 성공적으로 디코딩하여 복사했으면 true, 재생이 끝났거나 실패했으면 false</returns>
private bool TryReadFrame(byte* outBuffer, int outStride, bool vflip, out double timestamp)
{
timestamp = 0;
lock (_lock)
{
if (!_isOpened || _disposed) return false;
if (_hasCachedFrame)
{
timestamp = GetTimestampAndUpdateIndex(_pCachedFrame);
InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pCachedFrame->width, _pCachedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA);
if (vflip)
{
_pConvertedFrame->data[0] = outBuffer + outStride * (Height - 1);
_pConvertedFrame->linesize[0] = -outStride;
}
else
{
_pConvertedFrame->data[0] = outBuffer;
_pConvertedFrame->linesize[0] = outStride;
}
ffmpeg.sws_scale(
_pSwsContext,
_pCachedFrame->data, _pCachedFrame->linesize,
0, _pCachedFrame->height,
_pConvertedFrame->data, _pConvertedFrame->linesize
);
ffmpeg.av_frame_unref(_pCachedFrame);
_hasCachedFrame = false;
return true;
}
while (true)
{
int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
if (ret == 0)
{
timestamp = GetTimestampAndUpdateIndex(_pDecodedFrame);
InitOrUpdateSwsContext(_pCodecContext->pix_fmt, _pDecodedFrame->width, _pDecodedFrame->height, AVPixelFormat.AV_PIX_FMT_RGBA);
if (vflip)
{
_pConvertedFrame->data[0] = outBuffer + outStride * (Height - 1);
_pConvertedFrame->linesize[0] = -outStride;
}
else
{
_pConvertedFrame->data[0] = outBuffer;
_pConvertedFrame->linesize[0] = outStride;
}
ffmpeg.sws_scale(
_pSwsContext,
_pDecodedFrame->data, _pDecodedFrame->linesize,
0, _pDecodedFrame->height,
_pConvertedFrame->data, _pConvertedFrame->linesize
);
ffmpeg.av_frame_unref(_pDecodedFrame);
return true;
}
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
if (_isDraining) return false;
if (!ReadAndSendPacket())
{
_isDraining = true;
ffmpeg.avcodec_send_packet(_pCodecContext, null);
}
}
else if (ret == ffmpeg.AVERROR_EOF)
{
return false;
}
else
{
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
}
}
}
}
public bool TryReadFrame(IntPtr outBuffer, int outStride, bool vflip, out double timestamp)
{
return TryReadFrame((byte*)outBuffer.ToPointer(), outStride, vflip, out timestamp);
}
public bool TryReadFrame(NativeArray<byte> outBuffer, bool vflip, out double timestamp)
{
if (outBuffer.Length < Width * Height * 4)
{
throw new ArgumentException("출력 버퍼의 크기가 비디오 해상도보다 작습니다.", nameof(outBuffer));
}
int stride = Width * 4;
return TryReadFrame((byte*)outBuffer.GetUnsafePtr(), stride, vflip, out timestamp);
}
/// <summary>
/// 다음 비디오 프레임을 읽어 Y, U, V 채널 데이터를 각각 복사합니다.
/// 비디오 원본이 YUV420p 계열(YUV420P, YUVJ420P)이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
/// 그 외 포맷일 경우 자동으로 YUV420p로 변환 후 복사합니다.
/// </summary>
// ReSharper disable once InconsistentNaming
private bool TryReadYUV420PFrame(
byte* yBuffer, int yStride,
byte* uBuffer, int uStride,
byte* vBuffer, int vStride,
out double timestamp)
{
timestamp = 0;
lock (_lock)
{
if (!_isOpened || _disposed) return false;
AVFrame* frame;
if (_hasCachedFrame)
{
frame = _pCachedFrame;
_hasCachedFrame = false;
}
else
{
while (true)
{
int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
if (ret == 0)
{
frame = _pDecodedFrame;
break;
}
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
if (_isDraining) return false;
if (!ReadAndSendPacket())
{
_isDraining = true;
ffmpeg.avcodec_send_packet(_pCodecContext, null);
}
}
else if (ret == ffmpeg.AVERROR_EOF)
{
return false;
}
else
{
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
}
}
}
if (frame == null) return false;
timestamp = GetTimestampAndUpdateIndex(frame);
if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUV420P && frame->format != (int)AVPixelFormat.AV_PIX_FMT_YUVJ420P)
{
InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_YUV420P);
_pConvertedFrame->data[0] = yBuffer;
_pConvertedFrame->data[1] = uBuffer;
_pConvertedFrame->data[2] = vBuffer;
_pConvertedFrame->linesize[0] = yStride;
_pConvertedFrame->linesize[1] = uStride;
_pConvertedFrame->linesize[2] = vStride;
ffmpeg.sws_scale(
_pSwsContext,
frame->data, frame->linesize,
0, frame->height,
_pConvertedFrame->data, _pConvertedFrame->linesize
);
}
else
{
int w = frame->width;
int h = frame->height;
int uvW = w / 2;
int uvH = h / 2;
// Y plane 복사
for (int y = 0; y < h; y++)
{
Buffer.MemoryCopy(
frame->data[0] + y * frame->linesize[0],
yBuffer + y * yStride,
w, w
);
}
// U plane 복사
for (int y = 0; y < uvH; y++)
{
Buffer.MemoryCopy(
frame->data[1] + y * frame->linesize[1],
uBuffer + y * uStride,
uvW, uvW
);
}
// V plane 복사
for (int y = 0; y < uvH; y++)
{
Buffer.MemoryCopy(
frame->data[2] + y * frame->linesize[2],
vBuffer + y * vStride,
uvW, uvW
);
}
}
ffmpeg.av_frame_unref(frame);
return true;
}
}
// ReSharper disable once InconsistentNaming
public bool TryReadYUV420PFrame(
NativeArray<byte> yBuffer,
NativeArray<byte> uBuffer,
NativeArray<byte> vBuffer,
out double timestamp)
{
int w = Width;
int h = Height;
int uvW = w / 2;
int uvH = h / 2;
if (yBuffer.Length < w * h || uBuffer.Length < uvW * uvH || vBuffer.Length < uvW * uvH)
{
throw new ArgumentException("버퍼 크기가 비디오 YUV 평면 해상도보다 작습니다.");
}
return TryReadYUV420PFrame(
(byte*)yBuffer.GetUnsafePtr(), w,
(byte*)uBuffer.GetUnsafePtr(), uvW,
(byte*)vBuffer.GetUnsafePtr(), uvW,
out timestamp
);
}
/// <summary>
/// 다음 비디오 프레임을 읽어 NV12 형식의 Y, UV 채널 데이터를 각각 복사합니다.
/// 비디오 원본이 NV12 계열이면 CPU 컬러 변환(sws_scale)을 우회하고 메모리를 다이렉트로 복사하여 속도가 매우 빠릅니다.
/// 그 외 포맷일 경우 자동으로 NV12로 변환 후 복사합니다.
/// </summary>
// ReSharper disable once InconsistentNaming
private bool TryReadNV12Frame(
byte* yBuffer, int yStride,
byte* uvBuffer, int uvStride,
out double timestamp)
{
timestamp = 0;
lock (_lock)
{
if (!_isOpened || _disposed) return false;
AVFrame* frame;
if (_hasCachedFrame)
{
frame = _pCachedFrame;
_hasCachedFrame = false;
}
else
{
while (true)
{
int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
if (ret == 0)
{
frame = _pDecodedFrame;
break;
}
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
if (_isDraining) return false;
if (!ReadAndSendPacket())
{
_isDraining = true;
ffmpeg.avcodec_send_packet(_pCodecContext, null);
}
}
else if (ret == ffmpeg.AVERROR_EOF)
{
return false;
}
else
{
throw new Exception($"디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
}
}
}
if (frame == null) return false;
timestamp = GetTimestampAndUpdateIndex(frame);
if (frame->format != (int)AVPixelFormat.AV_PIX_FMT_NV12)
{
InitOrUpdateSwsContext((AVPixelFormat)frame->format, frame->width, frame->height, AVPixelFormat.AV_PIX_FMT_NV12);
_pConvertedFrame->data[0] = yBuffer;
_pConvertedFrame->data[1] = uvBuffer;
_pConvertedFrame->linesize[0] = yStride;
_pConvertedFrame->linesize[1] = uvStride;
ffmpeg.sws_scale(
_pSwsContext,
frame->data, frame->linesize,
0, frame->height,
_pConvertedFrame->data, _pConvertedFrame->linesize
);
}
else
{
int w = frame->width;
int h = frame->height;
int uvH = h / 2;
// Y plane 복사
for (int y = 0; y < h; y++)
{
Buffer.MemoryCopy(
frame->data[0] + y * frame->linesize[0],
yBuffer + y * yStride,
w, w
);
}
// UV plane 복사
for (int y = 0; y < uvH; y++)
{
Buffer.MemoryCopy(
frame->data[1] + y * frame->linesize[1],
uvBuffer + y * uvStride,
w, w
);
}
}
ffmpeg.av_frame_unref(frame);
return true;
}
}
// ReSharper disable once InconsistentNaming
public bool TryReadNV12Frame(
NativeArray<byte> yBuffer,
NativeArray<byte> uvBuffer,
out double timestamp)
{
int w = Width;
int h = Height;
int uvH = h / 2;
if (yBuffer.Length < w * h || uvBuffer.Length < w * uvH)
{
throw new ArgumentException("버퍼 크기가 비디오 NV12 평면 해상도보다 작습니다.");
}
return TryReadNV12Frame(
(byte*)yBuffer.GetUnsafePtr(), w,
(byte*)uvBuffer.GetUnsafePtr(), w,
out timestamp
);
}
/// <summary>
/// 지정한 개수만큼 프레임을 디코딩 후 바로 버립니다. (빠른 전방 탐색용)
/// </summary>
/// <param name="count">버릴 프레임 개수</param>
/// <returns>실제로 건너뛴 프레임 개수</returns>
private void SkipFrames(int count)
{
lock (_lock)
{
if (!_isOpened || _disposed) return;
if (count <= 0) return;
int skipped = 0;
if (_hasCachedFrame)
{
ffmpeg.av_frame_unref(_pCachedFrame);
_hasCachedFrame = false;
skipped++;
}
while (skipped < count)
{
int ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
if (ret == 0)
{
GetTimestampAndUpdateIndex(_pDecodedFrame);
ffmpeg.av_frame_unref(_pDecodedFrame);
skipped++;
continue;
}
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
if (_isDraining) break;
if (!ReadAndSendPacket())
{
_isDraining = true;
ffmpeg.avcodec_send_packet(_pCodecContext, null);
}
}
else if (ret == ffmpeg.AVERROR_EOF)
{
break;
}
else
{
throw new Exception($"프레임 건너뛰기 중 오류 발생: {GetFFmpegErrorString(ret)}");
}
}
}
}
/// <summary>
/// 특정 재생 시간(초)으로 탐색(Seek)합니다.
/// </summary>
/// <param name="timeSeconds">탐색할 시간 (초)</param>
public void SeekToTime(double timeSeconds)
{
lock (_lock)
{
if (!_isOpened || _disposed) return;
if (timeSeconds < 0) timeSeconds = 0;
double targetFrameIndex = timeSeconds * FPS;
// 이미 해당 인덱스이고 매우 인접한 미래라면 seek 대신 skip 처리 (I-프레임 찾기 오버헤드 방지)
bool useSkip = targetFrameIndex >= _currentFrameIndex && targetFrameIndex - _currentFrameIndex < 60;
if (useSkip)
{
SkipFrames((int)(targetFrameIndex - _currentFrameIndex));
return;
}
if (_hasCachedFrame)
{
ffmpeg.av_frame_unref(_pCachedFrame);
_hasCachedFrame = false;
}
long targetPts = (long)Math.Round(timeSeconds / ffmpeg.av_q2d(_timeBase));
int ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, targetPts, ffmpeg.AVSEEK_FLAG_BACKWARD);
if (ret < 0)
{
// 실패 시 파일 처음부터 탐색 시도
ret = ffmpeg.av_seek_frame(_pFormatContext, _videoStreamIndex, 0, ffmpeg.AVSEEK_FLAG_BACKWARD);
if (ret < 0)
{
throw new Exception($"Seek 실패: {GetFFmpegErrorString(ret)}");
}
}
// 디코더 내부 캐시 초기화
ffmpeg.avcodec_flush_buffers(_pCodecContext);
_isDraining = false;
// 지정된 시간 이후의 첫 번째 프레임을 찾을 때까지 디코딩을 진행하며 버립니다.
while (true)
{
ret = ffmpeg.avcodec_receive_frame(_pCodecContext, _pDecodedFrame);
if (ret == 0)
{
double currentPts = GetTimestampAndUpdateIndex(_pDecodedFrame);
// 플로팅 포인트 오차 감안하여 하프 프레임 제외한 시간부터 체크
if (currentPts >= timeSeconds - (0.5 / FPS))
{
// 매칭되는 첫 프레임을 캐시에 임시 저장하여 다음 TryReadFrame에서 소모하도록 유도
ffmpeg.av_frame_move_ref(_pCachedFrame, _pDecodedFrame);
_hasCachedFrame = true;
break;
}
ffmpeg.av_frame_unref(_pDecodedFrame);
continue;
}
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
if (_isDraining) break;
if (!ReadAndSendPacket())
{
_isDraining = true;
ffmpeg.avcodec_send_packet(_pCodecContext, null);
}
}
else if (ret == ffmpeg.AVERROR_EOF)
{
break;
}
else
{
throw new Exception($"Seek 디코딩 중 오류 발생: {GetFFmpegErrorString(ret)}");
}
}
}
}
/// <summary>
/// 특정 프레임 인덱스로 탐색(Seek)합니다.
/// </summary>
/// <param name="frameIndex">탐색할 프레임 인덱스</param>
public void SeekToFrame(long frameIndex)
{
SeekToTime(frameIndex / FPS);
}
private string GetFFmpegErrorString(int errnum)
{
const ulong bufferSize = 1024;
byte* buffer = stackalloc byte[(int)bufferSize];
if (ffmpeg.av_strerror(errnum, buffer, bufferSize) >= 0)
{
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)buffer) ?? $"알 수 없는 에러 (Code: {errnum})";
}
return $"알 수 없는 에러 (Code: {errnum})";
}
public void Dispose()
{
lock (_lock)
{
if (_disposed) return;
_disposed = true;
if (_pSwsContext != null)
{
ffmpeg.sws_freeContext(_pSwsContext);
_pSwsContext = null;
}
if (_pDecodedFrame != null)
{
var pFrame = _pDecodedFrame;
ffmpeg.av_frame_free(&pFrame);
_pDecodedFrame = null;
}
if (_pCachedFrame != null)
{
var pFrame = _pCachedFrame;
ffmpeg.av_frame_free(&pFrame);
_pCachedFrame = null;
}
if (_pConvertedFrame != null)
{
var pFrame = _pConvertedFrame;
ffmpeg.av_frame_free(&pFrame);
_pConvertedFrame = null;
}
if (_pPacket != null)
{
var pPacket = _pPacket;
ffmpeg.av_packet_free(&pPacket);
_pPacket = null;
}
if (_pCodecContext != null)
{
var pCodecCtx = _pCodecContext;
ffmpeg.avcodec_free_context(&pCodecCtx);
_pCodecContext = null;
}
if (_pFormatContext != null)
{
var pFormatCtx = _pFormatContext;
ffmpeg.avformat_close_input(&pFormatCtx);
_pFormatContext = null;
}
_isOpened = false;
GC.SuppressFinalize(this);
}
}
~FFmpegNativeDecoder()
{
Dispose();
}
}
}