|
|
|
@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
using FFmpeg.AutoGen;
|
|
|
|
|
|
|
|
using System.Windows;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace SmartAquaViewer.Helper.FFHelper
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
public sealed unsafe class StreamDecoder : IDisposable
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
public delegate void ThreadStoppedHandler(object obj);
|
|
|
|
|
|
|
|
public static event ThreadStoppedHandler hThreadStoppedHandler;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private readonly AVFormatContext* _pFormatContext;
|
|
|
|
|
|
|
|
public readonly AVFrame* _pFrame;
|
|
|
|
|
|
|
|
private readonly AVPacket* _pPacket;
|
|
|
|
|
|
|
|
AVCodec* codec;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public string CodecName { get; }
|
|
|
|
|
|
|
|
public Size FrameSize { get; }
|
|
|
|
|
|
|
|
public AVPixelFormat PixelFormat { get; }
|
|
|
|
|
|
|
|
public int videoStreamIndex { get; }
|
|
|
|
|
|
|
|
public AVCodecContext* _vcodecContext { get; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public bool _isContextNotInitialized = false;
|
|
|
|
|
|
|
|
public double _fps;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public StreamDecoder(string url)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
//Application.Current.Dispatcher.Invoke(() =>
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// ProgressWindow.GetInstance().Show();
|
|
|
|
|
|
|
|
//});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lock (this)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
AVFormatContext* pFormatContext = null;
|
|
|
|
|
|
|
|
AVCodecContext* vcodecContext = null;
|
|
|
|
|
|
|
|
AVDictionary* options = null;
|
|
|
|
|
|
|
|
AVStream* videoStream = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_log_set_level(ffmpeg.AV_LOG_DEBUG);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_pFormatContext = ffmpeg.avformat_alloc_context();
|
|
|
|
|
|
|
|
if (_pFormatContext == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
throw new InvalidOperationException("Failed to allocate format context.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
pFormatContext = _pFormatContext;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Get the codec context
|
|
|
|
|
|
|
|
_vcodecContext = ffmpeg.avcodec_alloc_context3(codec);
|
|
|
|
|
|
|
|
if (_vcodecContext == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// Out of memory
|
|
|
|
|
|
|
|
ffmpeg.avformat_close_input(&pFormatContext);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ffmpeg.av_dict_set(&options, "stimeout", "600000000", 0); // 60 seconds timeout
|
|
|
|
|
|
|
|
ffmpeg.av_dict_set(&options, "reconnect", "1", 0);
|
|
|
|
|
|
|
|
ffmpeg.av_dict_set(&options, "reconnect_at_eof", "1", 0);
|
|
|
|
|
|
|
|
ffmpeg.av_dict_set(&options, "reconnect_streamed", "1", 0);
|
|
|
|
|
|
|
|
ffmpeg.av_dict_set(&options, "rtsp_transport", "tcp", 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int ret = ffmpeg.avformat_open_input(&pFormatContext, url, null, &options).ThrowExceptionIfError();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (options != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_dict_free(&options);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ret = ffmpeg.avformat_find_stream_info(_pFormatContext, null).ThrowExceptionIfError();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < pFormatContext->nb_streams; i++)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
videoStream = pFormatContext->streams[i];
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (videoStream == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
Console.WriteLine("Failed to find video stream");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AVRational frameRate = videoStream->avg_frame_rate;
|
|
|
|
|
|
|
|
_fps = (double)frameRate.num / frameRate.den;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
videoStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int context = ffmpeg.avcodec_parameters_to_context(_vcodecContext, _pFormatContext->streams[videoStreamIndex]->codecpar).ThrowExceptionIfError();
|
|
|
|
|
|
|
|
if (context < 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
//Log4NetManager.GetLog().Error("StreamDecoder() : avcodec_parameters_to_context Error");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (videoStreamIndex >= 0)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
AVCodecContext* avctx = OpenStream(_vcodecContext);
|
|
|
|
|
|
|
|
if (avctx == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
throw new InvalidOperationException("Failed to open codec context.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
FrameSize = new Size(avctx->width, avctx->height);
|
|
|
|
|
|
|
|
PixelFormat = avctx->pix_fmt;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
throw new InvalidOperationException("No video stream found.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_pPacket = ffmpeg.av_packet_alloc();
|
|
|
|
|
|
|
|
if (_pPacket == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
throw new InvalidOperationException("Failed to allocate packet.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_pFrame = ffmpeg.av_frame_alloc();
|
|
|
|
|
|
|
|
if (_pFrame == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
throw new InvalidOperationException("Failed to allocate frame.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
_isContextNotInitialized = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
|
|
|
|
|
|
|
|
MessageBox.Show("StreamDecoder() : " + ex.Message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_pFrame != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_frame_unref(_pFrame);
|
|
|
|
|
|
|
|
ffmpeg.av_free(_pFrame);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_pPacket != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_packet_unref(_pPacket);
|
|
|
|
|
|
|
|
ffmpeg.av_free(_pPacket);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_vcodecContext != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
vcodecContext = _vcodecContext;
|
|
|
|
|
|
|
|
ffmpeg.avcodec_close(vcodecContext);
|
|
|
|
|
|
|
|
ffmpeg.avcodec_free_context(&vcodecContext);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_pFormatContext != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.avformat_close_input(&pFormatContext);
|
|
|
|
|
|
|
|
ffmpeg.avformat_free_context(pFormatContext);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
finally
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (options != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_dict_free(&options);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//Application.Current.Dispatcher.Invoke(() =>
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// ProgressWindow.GetInstance().Close();
|
|
|
|
|
|
|
|
//});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private AVCodecContext* OpenStream(AVCodecContext* avctx)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
AVCodec* codec = ffmpeg.avcodec_find_decoder(avctx->codec_id);
|
|
|
|
|
|
|
|
if (codec == null) throw new InvalidOperationException("No codec could be found.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
avctx->codec_id = codec->id;
|
|
|
|
|
|
|
|
avctx->lowres = 0;
|
|
|
|
|
|
|
|
if (avctx->lowres > codec->max_lowres)
|
|
|
|
|
|
|
|
avctx->lowres = codec->max_lowres;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
avctx->idct_algo = ffmpeg.FF_IDCT_AUTO;
|
|
|
|
|
|
|
|
avctx->error_concealment = 3;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ffmpeg.avcodec_open2(avctx, codec, null).ThrowExceptionIfError();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return avctx;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
|
|
|
|
|
|
|
|
//public void Dispose()
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// ffmpeg.av_frame_unref(_pFrame);
|
|
|
|
|
|
|
|
// ffmpeg.av_free(_pFrame);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ffmpeg.av_packet_unref(_pPacket);
|
|
|
|
|
|
|
|
// ffmpeg.av_free(_pPacket);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ffmpeg.avcodec_close(_vcodecContext);
|
|
|
|
|
|
|
|
// //ffmpeg.avcodec_close(acodecContext);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// var pFormatContext = _pFormatContext;
|
|
|
|
|
|
|
|
// ffmpeg.avformat_close_input(&pFormatContext);
|
|
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
lock (this)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
if (_pFrame != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_frame_unref(_pFrame);
|
|
|
|
|
|
|
|
ffmpeg.av_free(_pFrame);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_pPacket != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_packet_unref(_pPacket);
|
|
|
|
|
|
|
|
ffmpeg.av_free(_pPacket);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_vcodecContext != null && !_isContextNotInitialized)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.avcodec_close(_vcodecContext);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
//ffmpeg.avcodec_close(acodecContext);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (_pFormatContext != null && !_isContextNotInitialized)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var pFormatContext = _pFormatContext;
|
|
|
|
|
|
|
|
ffmpeg.avformat_close_input(&pFormatContext);
|
|
|
|
|
|
|
|
ffmpeg.avformat_free_context(pFormatContext);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
|
|
|
|
|
|
|
|
//public bool TryDecodeNextFrame(out AVFrame frame)
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// ffmpeg.av_frame_unref(_pFrame);
|
|
|
|
|
|
|
|
// int error = 0;
|
|
|
|
|
|
|
|
// do
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// try
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// do
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// error = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
|
|
|
|
|
|
|
|
// if (error == ffmpeg.AVERROR_EOF)
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// frame = *_pFrame;
|
|
|
|
|
|
|
|
// return false;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// error.ThrowExceptionIfError();
|
|
|
|
|
|
|
|
// } while (_pPacket->stream_index != videoStreamIndex);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ffmpeg.avcodec_send_packet(_vcodecContext, _pPacket).ThrowExceptionIfError();
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// catch (Exception ex)
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// Log4NetManager.GetLog().Error("TryDecodeNextFrame() : " + ex.Message);
|
|
|
|
|
|
|
|
// MessageBox.Show("TryDecodeNextFrame() : " + ex.Message);
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// finally
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// ffmpeg.av_packet_unref(_pPacket);
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// error = ffmpeg.avcodec_receive_frame(_vcodecContext, _pFrame);
|
|
|
|
|
|
|
|
// } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// error.ThrowExceptionIfError();
|
|
|
|
|
|
|
|
// frame = *_pFrame;
|
|
|
|
|
|
|
|
// return true;
|
|
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public bool TryDecodeNextFrame(out AVFrame frame)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_frame_unref(this._pFrame);
|
|
|
|
|
|
|
|
int error;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// repeated-try avcodec_receive_frame until enough packets haven been send
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// try read frame; maybe last avcodec_send_packet resulted in multiple frames to be read; will respond with EAGAIN if more packets are needed
|
|
|
|
|
|
|
|
error = ffmpeg.avcodec_receive_frame(this._vcodecContext, this._pFrame);
|
|
|
|
|
|
|
|
if (error != ffmpeg.AVERROR(ffmpeg.EAGAIN))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
frame = *this._pFrame;
|
|
|
|
|
|
|
|
if (error == ffmpeg.AVERROR_EOF)
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error.ThrowExceptionIfError();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// feed all stream-matching packets to decoder
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
error = ffmpeg.av_read_frame(this._pFormatContext, this._pPacket);
|
|
|
|
|
|
|
|
if (error == ffmpeg.AVERROR_EOF)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// no more packets to read -> trigger draining/flushing of remaining frames
|
|
|
|
|
|
|
|
ffmpeg.avcodec_send_packet(this._vcodecContext, null).ThrowExceptionIfError();
|
|
|
|
|
|
|
|
break; // don't throw error, just exit packet-feed-loop
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error.ThrowExceptionIfError();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this._pPacket->stream_index == this.videoStreamIndex)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var sendPacketResult = ffmpeg.avcodec_send_packet(this._vcodecContext, this._pPacket);
|
|
|
|
|
|
|
|
if (sendPacketResult == 0)
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// no reason to abort/crash (it was just an invalid packet from demuxer) -> just retrieve and feed next packet; most of time the codec will resume without problems
|
|
|
|
|
|
|
|
var errorMsg = Helper.av_strerror(sendPacketResult);
|
|
|
|
|
|
|
|
Trace.TraceError(errorMsg);
|
|
|
|
|
|
|
|
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : av_strerror 오류 " + errorMsg);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// unref all packets, no matter if sent to avcodec or ignored
|
|
|
|
|
|
|
|
ffmpeg.av_packet_unref(this._pPacket);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
Debug.WriteLine("TryDecodeNextFrame()" + ex.Message);
|
|
|
|
|
|
|
|
if (ex.Message.Equals("End of file"))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : " + ex.Message);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
//쓰레드가 망함,
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
finally
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
ffmpeg.av_packet_unref(this._pPacket);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public IReadOnlyDictionary<string, string> GetContextInfo()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
AVDictionaryEntry* tag = null;
|
|
|
|
|
|
|
|
var result = new Dictionary<string, string>();
|
|
|
|
|
|
|
|
while ((tag = ffmpeg.av_dict_get(_pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
|
|
|
|
|
|
|
|
var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
|
|
|
|
|
|
|
|
result.Add(key, value);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|