FFmpeg 库的 .NET 封装





5.00/5 (28投票s)
本文介绍创建的 .NET 封装库
目录
- 引言
- 目标
- 如何阅读文档
- 支持的 FFmpeg 版本
- 架构
- 核心类
- AVPacket
- AVPicture
- AVFrame
- AVCodec
- AVCodecContext
- AVCodecParser
- AVCodecParserContext
- AVRational
- AVFormatContext
- AVIOContext
- AVOutputFormat
- AVInputFormat
- AVStream
- AVDictionary
- AVOptions
- AVBitStreamFilter
- AVBSFContext
- AVChannelLayout
- AVPixelFormat
- AVSampleFormat
- AVSamples
- AVMath
- SwrContext
- SwsContext
- AVFilter
- AVFilterContext
- AVFilterGraph
- AVDevices
- AVDeviceInfoList
- AVDeviceInfo
- 高级
- 示例
- 源代码
- 编译后的库
- 构建项目
- 许可和分发
- 历史
引言
已经有很多尝试来创建 FFmpeg 库的 .NET 封装。我看到过不同的实现,大多数只是执行带有准备好的参数的 ffmpeg.exe 调用并解析其输出。更有成效的实现有一些基础类,它们只能执行 ffmpeg 的少数任务之一,并且它们的类暴露为 COM 对象或常规 C# 类,但它们的功能非常有限,并且在 .NET 中无法完全实现使用 C++ 完全访问 ffmpeg 库所能实现的功能。我还见过完全是 .NET 的“不安全”代码实现封装,因此 C# 代码也必须标记为“不安全”,并且此类封装的函数有很多指针,如“void *”和其他类似类型。因此,此类代码的使用也很成问题,而我只谈论 C# 而不考虑 VB.NET 等其他语言。
当前的 .NET 封装实现是 ffmpeg 库中所有导出的结构的表示,以及可以在托管代码中操作这些对象的 API,无论是来自 C#、VB.NET 还是 Delphi.NET。它不仅仅是调用几个预定义对象的特定 API——所有 API 都作为方法公开。您可能会问一个好问题:“这是如何实现的?为什么之前没有这样做?”,您可以在当前文档中找到答案。
目标
本文介绍创建的 FFmpeg 库的 .NET 封装。它涵盖了基本的架构方面以及信息:如何实现以及在实现过程中解决了哪些问题。它包含大量用于不同 FFmpeg 库用途的代码示例。每段代码都是可运行的,并附有输出结果的描述。对于那些想开始学习如何使用 ffmpeg 库的人,或者那些计划在生产中使用它的人来说,都会很有趣。即使是那些确信自己从 C++ 角度了解 ffmpeg 库的人,这些信息也会很有趣。如果您正在寻找一种解决方案,可以将整个 ffmpeg 库的强大功能直接集成到您的 .NET 应用程序中——那么您走在了正确的道路上。
如何阅读文档
第一部分描述了封装库的架构、主要功能、类的基本设计、实现过程中出现的各种问题以及如何解决。这里还介绍了库的基础类,并描述了它们的字段和用途。它包含 C++/CLI 或 C# 的代码片段和示例。在第二部分,您可以找到大多数 FFmpeg 库结构的核心封装类。每个类的文档都包含基本描述和一个或几个使用示例代码。这部分代码示例是 C#。第三部分介绍了一些库的高级主题。此外,文档还包括 .NET 封装与原生 C++ FFmpeg 示例的示例 C# 项目的描述。还有几个我自己的示例项目,我认为对于 FFmpeg 用户来说会很有趣。最后一部分包含有关如何构建项目、许可信息和分发说明的描述。
支持的 FFmpeg 版本
当前实现支持 4.2.2 及以上版本的 FFmpeg 库。当我说是“以上”时,我的意思是它通过一些定义和/或动态库链接来控制新版本的某些方面。例如,原生 FFmpeg 库定义了哪些 API 是导出的,以及每个结构包含哪些字段,并且当前结构正确地处理了这些。
架构
说当前 .NET 封装的实现并不简单或不容易,这简直是轻描淡写。该库能够存在并正常工作的结果在于其架构。该项目是用 C++/CLI 编写的,并编译为独立的 .NET DLL,可以作为引用添加到用任何语言编写的 .NET 应用程序中。有关为什么选择 C++/CLI 的决策的答案,您可以在本文档中找到。
库包含几个 h/cpp 文件,它们主要代表底层导入的 FFmpeg dll 库
AVCore.h/cpp | 包含核心类、枚举和结构。还包含从 AVUtil 库导入的类和枚举 |
AVCodec.h/cpp | 包含来自 AVCodec 库的封装类和枚举 |
AVFormat.h/cpp | 包含来自 AVFormat 库的封装类和枚举 |
AVFilter.h/cpp | 包含来自 AVFilter 库的封装类和枚举 |
AVDevice.h/cpp | 包含来自 AVDevice 库的封装类和枚举 |
SWResample.h/cpp | 包含来自 SWResample 库的封装类和枚举 |
SWScale.h/cpp | 包含来自 SWScale 库的封装类和枚举 |
Postproc.h/cpp | 包含来自 Postproc 库的封装类和枚举 |
核心架构
本主题包含大多数库架构的优点和设计指南,从基础类设计到核心方面和已解决的问题。本部分以及 C# 还包含来自封装库实现的 C++/CLI 代码片段。
类
封装库实现中的大多数类都依赖于 FFmpeg 库中的结构或枚举。类名与其底层原生结构同名。例如:库中的托管类 AVPacket
代表 libavcodec
的 AVPacket
结构:它包含结构的所有字段以及导出的相关方法。字段实现为托管属性,而不是常规结构字段
public ref class AVPacket : public AVBase
, public ICloneable
{
private:
Object^ m_pOpaque;
AVBufferFreeCB^ m_pFreeCB;
internal:
AVPacket(void * _pointer,AVBase^ _parent);
public:
/// Allocate an AVPacket and set its fields to default values.
AVPacket();
// Allocate the payload of a packet and initialize its fields with default values.
AVPacket(int _size);
/// Initialize a reference-counted packet from allocated data.
/// Data Must be freed separately
AVPacket(IntPtr _data,int _size);
/// Initialize a reference-counted packet from allocated data.
/// With callback For data Free
AVPacket(IntPtr _data,int _size,AVBufferFreeCB^ free_cb,Object^ opaque);
/// Initialize a reference-counted packet with given buffer
AVPacket(AVBufferRef^ buf);
// Create a new packet that references the same data as src.
AVPacket(AVPacket^ _packet);
~AVPacket();
public:
property int _StructureSize { virtual int get() override; }
public:
///A reference to the reference-counted buffer where the packet data is
///stored.
///May be NULL, then the packet data is not reference-counted.
property AVBufferRef^ buf { AVBufferRef^ get(); }
///Presentation timestamp in AVStream->time_base units; the time at which
///the decompressed packet will be presented to the user.
///Can be AV_NOPTS_VALUE if it is not stored in the file.
///pts MUST be larger or equal to dts as presentation cannot happen before
///decompression, unless one wants to view hex dumps. Some formats misuse
///the terms dts and pts/cts to mean something different. Such timestamps
///must be converted to true pts/dts before they are stored in AVPacket.
property Int64 pts { Int64 get(); void set(Int64); }
///Decompression timestamp in AVStream->time_base units; the time at which
///the packet is decompressed.
///Can be AV_NOPTS_VALUE if it is not stored in the file.
property Int64 dts { Int64 get(); void set(Int64); }
...
属性
依赖于原生结构的类继承自 AVBase
类。这些类代表指向底层结构的指针。将结构字段公开为属性能够隐藏所有内部细节,这允许通过封装实现来支持不同版本的 ffmpeg。例如:在某些 ffmpeg
版本中,某些字段可能隐藏在结构中,只能通过 Options API 访问,例如 AVCodecContext
的“b_sensitivity
”字段,通过设置 FF_API_PRIVATE_OPT
定义可能会在构建时删除它
#if FF_API_PRIVATE_OPT
/** @deprecated use encoder private options instead */
attribute_deprecated
int b_sensitivity;
#endif
在封装库中,您只能看到属性,但内部会选择访问它的方式
int FFmpeg::AVCodecContext::b_sensitivity::get()
{
__int64 val = 0;
if (AVERROR_OPTION_NOT_FOUND == av_opt_get_int(m_pPointer, "b_sensitivity", 0, &val))
{
#if FF_API_PRIVATE_OPT
val = ((::AVCodecContext*)m_pPointer)->b_sensitivity;
#endif
}
return (int)val;
}
void FFmpeg::AVCodecContext::b_sensitivity::set(int value)
{
if (AVERROR_OPTION_NOT_FOUND ==
av_opt_set_int(m_pPointer, "b_sensitivity", (int64_t)value, 0))
{
#if FF_API_PRIVATE_OPT
((::AVCodecContext*)m_pPointer)->b_sensitivity = (int)value;
#endif
}
}
同时在 C# 代码中
或者,在某些 FFmpeg
版本中,字段可能根本不存在。例如,“refcounted_frames
”的 AVCodecContext
int FFmpeg::AVCodecContext::refcounted_frames::get()
{
__int64 val = 0;
if (AVERROR_OPTION_NOT_FOUND ==
av_opt_get_int(m_pPointer, "refcounted_frames", 0, &val))
{
#if (LIBAVCODEC_VERSION_MAJOR < 59)
val = ((::AVCodecContext*)m_pPointer)->refcounted_frames;
#endif
}
return (int)val;
}
void FFmpeg::AVCodecContext::refcounted_frames::set(int value)
{
if (AVERROR_OPTION_NOT_FOUND ==
av_opt_set_int(m_pPointer, "refcounted_frames", (int64_t)value, 0))
{
#if (LIBAVCODEC_VERSION_MAJOR < 59)
((::AVCodecContext*)m_pPointer)->refcounted_frames = (int)value;
#endif
}
}
我相信您已经认为这非常酷,而且无需担心后台。这种实现被称为外观模式。
构造函数
如果您了解 FFmpeg
库 API,您会同意它包含用于分配结构的 API。为此目的,每个结构都可以有不同的 API。在封装类中,所有对象都有一个构造函数——所以用户无需考虑使用哪个 API;甚至更多:构造函数可以封装一系列 FFmpeg
API 调用来执行完整的对象初始化。不同的 FFmpeg
API 以及不同的对象初始化作为具有不同参数的单独构造函数完成。例如 AVPacket
对象的构造函数
public:
/// Allocate an AVPacket and set its fields to default values.
AVPacket();
// Allocate the payload of a packet and initialize its fields with default values.
AVPacket(int _size);
/// Initialize a reference-counted packet from allocated data.
/// Data Must be freed separately
AVPacket(IntPtr _data,int _size);
/// Initialize a reference-counted packet from allocated data.
/// With callback For data Free
AVPacket(IntPtr _data,int _size,AVBufferFreeCB^ free_cb,Object^ opaque);
/// Initialize a reference-counted packet with given buffer
AVPacket(AVBufferRef^ buf);
// Create a new packet that references the same data as src.
AVPacket(AVPacket^ _packet);
有一个特殊的内部构造函数,它不对库外部公开。大多数对象都包含它。这是为了内部对象管理,我将在本文档后面介绍
internal:
AVPacket(void * _pointer,AVBase^ _parent);
析构函数
另外,用户无需担心释放内存和释放对象数据。这一切都在对象的析构函数和内部的终结器中完成。在 C# 中,这作为对象的 IDisposable 接口自动实现。因此,一旦对象不再需要,最好调用 Dispose 方法——以清除所有依赖项并释放分配的对象内存。用户也无需考虑调用哪个 FFmpeg
API 来释放数据和取消分配对象资源,因为这一切都由封装库处理。如果用户忘记调用 Dispose
方法:对象及其内存将由终结器释放,但最好在代码中直接调用 Dispose
。
// a wrapper around a single output AVStream
public class OutputStream
{
public AVStream st;
public AVCodecContext enc;
/* pts of the next frame that will be generated */
public long next_pts;
public int samples_count;
public AVFrame frame;
public AVFrame tmp_frame;
public float t, tincr, tincr2;
public SwsContext sws_ctx;
public SwrContext swr_ctx;
}
static void close_stream(AVFormatContext oc, OutputStream ost)
{
if (ost.enc != null) ost.enc.Dispose();
if (ost.frame != null) ost.frame.Dispose();
if (ost.tmp_frame != null) ost.tmp_frame.Dispose();
if (ost.sws_ctx != null) ost.sws_ctx.Dispose();
if (ost.swr_ctx != null) ost.swr_ctx.Dispose();
}
此外,必须记住,像 AVPacket
或 AVFrame
这样的对象可能包含由 ffmpeg
API 内部分配的缓冲区,这些缓冲区应该单独释放,请参阅 av_packet_unref
和 av_frame_unref
API。为此目的,这些对象公开名为 Free
的方法,所以,不要忘记调用它以避免内存泄漏。
枚举
某些 FFmpeg
库定义或枚举被实现为 .NET enum
类——这样可以轻松找到在结构的特定字段中可以设置的值。例如 AV_CODEC_FLAG_*
、AV_CODEC_FLAG2_*
或 AV_CODEC_CAP_*
定义
#define AV_CODEC_CAP_DR1 (1 << 1)
#define AV_CODEC_CAP_TRUNCATED (1 << 3)
设计为 .NET enum
以便从代码中更好地访问
// AV_CODEC_CAP_*
[Flags]
public enum class AVCodecCap : UInt32
{
///< Decoder can use draw_horiz_band callback.
DRAW_HORIZ_BAND = (1 << 0),
///<summary>
/// Codec uses get_buffer() for allocating buffers and supports custom allocators.
/// If not set, it might not use get_buffer() at all or use operations that
/// assume the buffer was allocated by avcodec_default_get_buffer.
///</summary>
DR1 = (1 << 1),
TRUNCATED = (1 << 3),
...
在 C# 中,它看起来像
一些来自 FFmpeg
的枚举或定义被实现为常规 .NET 类。在这种情况下,类还公开一些依赖于指定枚举类型的有用方法
/// AV_SAMPLE_FMT_*
public value class AVSampleFormat
{
public:
static const AVSampleFormat NONE = ::AV_SAMPLE_FMT_NONE;
/// unsigned 8 bits
static const AVSampleFormat U8 = ::AV_SAMPLE_FMT_U8;
/// signed 16 bits
static const AVSampleFormat S16 = ::AV_SAMPLE_FMT_S16;
/// signed 32 bits
static const AVSampleFormat S32 = ::AV_SAMPLE_FMT_S32;
/// float
static const AVSampleFormat FLT = ::AV_SAMPLE_FMT_FLT;
/// double
static const AVSampleFormat DBL = ::AV_SAMPLE_FMT_DBL;
/// unsigned 8 bits, planar
static const AVSampleFormat U8P = ::AV_SAMPLE_FMT_U8P;
/// signed 16 bits, planar
static const AVSampleFormat S16P = ::AV_SAMPLE_FMT_S16P;
/// signed 32 bits, planar
static const AVSampleFormat S32P = ::AV_SAMPLE_FMT_S32P;
/// float, planar
static const AVSampleFormat FLTP = ::AV_SAMPLE_FMT_FLTP;
/// double, planar
static const AVSampleFormat DBLP = ::AV_SAMPLE_FMT_DBLP;
protected:
/// Sample Format
int m_nValue;
public:
AVSampleFormat(int value);
explicit AVSampleFormat(unsigned int value);
protected:
//property int value { int get() { return m_nValue; } }
public:
/// Return the name of sample_fmt, or NULL if sample_fmt is not
/// recognized.
property String^ name { String^ get() { return ToString(); } }
/// Return number of bytes per sample.
///
/// @param sample_fmt the sample format
/// @return number of bytes per sample or zero if unknown for the given
/// sample format
property int bytes_per_sample { int get(); }
/// Check if the sample format is planar.
///
/// @param sample_fmt the sample format to inspect
/// @return 1 if the sample format is planar, 0 if it is interleaved
property bool is_planar { bool get(); }
这些类公开了所有必需的东西,以便将它们作为值类型处理,并支持隐式类型转换
public:
static operator int(AVSampleFormat a) { return a.m_nValue; }
static explicit operator unsigned int(AVSampleFormat a)
{ return (unsigned int)a.m_nValue; }
static operator AVSampleFormat(int a) { return AVSampleFormat(a); }
static explicit operator AVSampleFormat(unsigned int a)
{ return AVSampleFormat((int)a); }
internal:
static operator ::AVSampleFormat(AVSampleFormat a)
{ return (::AVSampleFormat)a.m_nValue; }
static operator AVSampleFormat(::AVSampleFormat a)
{ return AVSampleFormat((int)a); }
库
库包含特殊的 static
类,可以识别使用的库——它们的版本、构建配置和许可证
// LibAVCodec
public ref class LibAVCodec
{
private:
static bool s_bRegistered = false;
private:
LibAVCodec();
public:
// Return the LIBAVCODEC_VERSION_INT constant.
static property UInt32 Version { UInt32 get(); }
// Return the libavcodec build-time configuration.
static property String^ Configuration { String^ get(); }
// Return the libavcodec license.
static property String^ License { String^ get(); }
public:
// Register the codec codec and initialize libavcodec.
static void Register(AVCodec^ _codec);
// Register all the codecs, parsers and bitstream filters which were enabled at
// configuration time. If you do not call this function you can select exactly
// which formats you want to support, by using the individual registration
// functions.
static void RegisterAll();
};
每个 FFmpeg
导入的库都有一个类似的类,其中包含描述的字段。
这些库类可以包含 static
初始化方法,这些方法会调用诸如 avcodec_register_all
、av_register_all
等 API。这些方法可以由用户手动调用,但库会在需要时自动调用它们。已弃用的 API 可能不会从 FFmpeg
库导出——内部设计为动态链接。这如何完成将在本文档后面介绍。
ToString() 实现
某些类或枚举已重写 string
转换方法:“ToString()”。在这种情况下,它返回相关 FFmpeg
API 输出的 string
值,以获得类型或结构内容的易读描述
String^ FFmpeg::AVSampleFormat::ToString()
{
auto p = av_get_sample_fmt_name((::AVSampleFormat)m_nValue);
return p != nullptr ? gcnew String(p) : nullptr;
}
例如,它调用该方法以获取样本格式值的默认 string
表示
var fmt = AVSampleFormat.FLT;
Console.WriteLine("AVSampleFormat: {0} {1}",(int)fmt,fmt);
AVRESULT
正如您可能知道的,大多数 FFmpeg
API 返回整数值,它可能表示错误、处理过的数据量或任何其他含义。为了更好地理解返回值,封装库有一个名为 AVRESULT
的类。它与 FFmpeg
中的 AVERROR
宏相同,只是设计为一个类对象,公开了有用的字段,并具有 .NET 的优点。该类可以作为整数值使用,无需直接类型转换,因为它在其基础上作为值类型实现。通过使用该类,可以从返回的 API 值获取错误描述 string
,因为该类已重写 ToString()
方法,因此可以轻松地用于 string
转换
/* Write the stream header, if any. */
ret = oc.WriteHeader();
if (ret < 0) {
Console.WriteLine("Error occurred when opening output file: {0}\n",(AVRESULT)ret);
Environment.Exit(1);
}
给出的错误描述 string
有助于理解方法执行的结果
Console.WriteLine(AVRESULT.ENOMEM.ToString());
如果代码中使用 AVRESULT
类型,那么在调试时可以在变量值视图中看到结果 string
如果我们有整数结果类型,并且知道它是一个错误,那么只需将其转换为 AVRESULT
类型,即可在调试时查看变量值中的错误描述
此类还可以通过一些预定义的错误值进行访问,这些值可以在代码中使用
static AVRESULT CheckPointer(IntPtr p)
{
if (p == IntPtr.Zero) return AVRESULT.ENOMEM;
return AVRESULT.OK;
}
有些方法已经设计为返回 AVRESULT
对象,其他类型可以轻松转换为它。
FFmpeg::AVRESULT FFmpeg::AVCodecParameters::FromContext(AVCodecContext^ codec)
{
return avcodec_parameters_from_context(
((::AVCodecParameters*)m_pPointer),(::AVCodecContext*)codec->_Pointer.ToPointer());
}
FFmpeg::AVRESULT FFmpeg::AVCodecParameters::CopyTo(AVCodecContext^ context)
{
return avcodec_parameters_to_context(
(::AVCodecContext*)context->_Pointer.ToPointer(),
(::AVCodecParameters*)m_pPointer);
}
FFmpeg::AVRESULT FFmpeg::AVCodecParameters::CopyTo(AVCodecParameters^ dst)
{
return avcodec_parameters_copy(
(::AVCodecParameters*)dst->_Pointer.ToPointer(),
((::AVCodecParameters*)m_pPointer));
}
AVLog
通知应用程序从您的组件或从 FFmpeg
库内部发送信息的方式是使用 av_log
API。通过静态 AVLog
类可以访问相同的日志记录功能。
if ((ret = AVFormatContext.OpenInput(out ifmt_ctx, in_filename)) < 0) {
AVLog.log(AVLogLevel.Error, string.Format("Could not open input file {0}", in_filename));
goto end;
}
它还可以设置您自己的回调来接收日志消息
static void LogCallback(AVBase avcl, int level, string fmt, IntPtr vl)
{
Console.WriteLine(fmt);
}
static void Main(string[] args)
{
AVLog.Callback = LogCallback;
...
AVOptions
大多数原生 FFmpeg
结构对象都有隐藏的属性,只能通过 av_opt_
* API 访问。当前封装库的实现也具有这些能力。为此,库中包含 AVOptions
类。最初,它被设计为一个 static
对象,但后来被实现为一个常规类。它可以与 AVBase
的构造函数初始化,或仅与指针类型参数一起初始化。
/* Set the filter options through the AVOptions API. */
AVOptions options = new AVOptions(abuffer_ctx);
options.set("channel_layout", INPUT_CHANNEL_LAYOUT.ToString(), AVOptSearch.CHILDREN);
options.set("sample_fmt", INPUT_FORMAT.name, AVOptSearch.CHILDREN);
options.set_q("time_base", new AVRational( 1, INPUT_SAMPLERATE ), AVOptSearch.CHILDREN);
options.set_int("sample_rate", INPUT_SAMPLERATE, AVOptSearch.CHILDREN);
该类公开了操作选项 API 的所有有用方法。此外,还可以枚举选项
AVOptions options = new AVOptions(ifmt_ctx);
foreach (AVOption o in options)
{
Console.WriteLine(o.name);
}
AVDictionary
FFmpeg
API 中另一个最常用的结构是 AVDictionary
。它是一个键值集合类,可以是某些函数的输入或输出。在这些函数中,您将一些初始化选项作为 dictionary
对象传递,并返回所有可用选项或函数未使用的选项。在此类 API 调用时,该结构指针的管理是就地完成的。因此,用户只需以常规方式释放返回的 dictionary
对象,如前所述,并且内部结构指针会被替换。例如,我们有一个 FFmpeg
API
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
以及封装库中的相关实现
AVRESULT Open(AVCodec^ codec, AVDictionary^ options);
AVRESULT Open(AVCodec^ codec);
因此,根据 FFmpeg
API 的实现细节,结构设计用于设置选项,并在返回时,有一个新的 dictionary
对象包含未找到的选项。但在实现中,仅使用作为参数传递的结构对象。因此,这是常规工作方式,用户只有一个结构。即使内部 API 调用失败,用户也只需要释放最初创建的目标结构对象
AVDictionary dct = new AVDictionary();
dct.SetValue("preset", "veryfast");
s.id = (int)_output.nb_streams - 1;
bFailed = !(s.codec.Open(_codec, dct));
dct.Dispose();
扩展功能
如前所述,结构类可以包含相关结构的字段以及由 FFmpeg
API 生成的、与该结构功能相关的方法。但类还可以包含辅助方法,允许扩展 FFmpeg
的功能。例如,AVFrame
类具有将 .NET Bitmap
对象转换为 AVFrame
结构(反之亦然)的 static
方法
public:
System::Drawing::Bitmap^ ToBitmap();
public:
static String^ GetColorspaceName(AVColorSpace val);
public:
// Create Frame From Image
static AVFrame^ FromImage(System::Drawing::Bitmap^ _image,AVPixelFormat _format);
static AVFrame^ FromImage(System::Drawing::Bitmap^ _image);
static AVFrame^ FromImage(System::Drawing::Bitmap^ _image,
AVPixelFormat _format,bool bDeleteBitmap);
static AVFrame^ FromImage(System::Drawing::Bitmap^ _image, bool bDeleteBitmap);
// Convert Frame To Different Colorspace
static AVFrame^ ConvertVideoFrame(AVFrame^ _source,AVPixelFormat _format);
// Convert Frame To Bitmap
static System::Drawing::Bitmap^ ToBitmap(AVFrame^ _frame);
这些方法封装了 colorspace
转换以及任何其他 API 调用的全部功能。更重要的是,可以包装现有 Bitmap
对象而不进行任何复制或分配新图像数据。在这种情况下,会创建一个内部辅助对象,该对象与父 AVFrame
类一起被释放。这如何实现将在本文档后面介绍。
类 AVBase
所有 FFmpeg
结构类都继承自 AVBase
类。它包含通用字段和辅助方法。AVBase
类是每个 FFmpeg
对象的根类。AVBase
对象的主要任务是处理对象管理和内存管理。
字段
如前所述,AVBase
类是 FFmpeg
库每个导出结构的基类,它包含所有对象共有的字段。这些字段是:结构指针和结构大小。Size
字段可能未初始化,除非结构不是直接分配的,因此用户无需依赖该字段。指针可以转换为原始结构类型,以直接访问任何附加字段或原始数据
AVBase avbase = new AVPacket();
Console.WriteLine("0x{0:x}",avbase._Pointer);
如果结构可以使用通用分配方法进行分配,它可能包含非零的结构大小值,并且如果结构已分配,则相关属性会设置为 true
AVBase avbase = new AVRational(1,1);
Console.WriteLine("0x{0:x} StructureSize: {1} Allocated {2}",
avbase._Pointer,
avbase._StructureSize,
avbase._IsAllocated
);
如果类对象是从任何方法返回的,为了确定此类对象是否有效,我们可以检查指针字段并将其与非零值进行比较。但是 AVBase
类中还有一个 _IsValid
属性,它执行相同的操作
/* allocate and init a re-usable frame */
ost.frame = alloc_picture(c.pix_fmt, c.width, c.height);
if (!ost.frame._IsValid) {
Console.WriteLine("Could not allocate video frame\n");
Environment.Exit(1);
}
决定在这些内部且与 FFmpeg
结构无关的属性前添加“_
”前缀,以便用户知道这不是结构字段。
方法
用于测试对象及其字段的一个可用方法是 TraceValues()
。此方法使用 .NET 反射并枚举对象可用的属性及其值。如果您的代码对 .NET 反射的使用至关重要,则应将该代码排除在程序集之外。尽管 TraceValues
代码仅在 **Debug** 构建配置中工作。执行该方法的示例是
var r = new AVRational(25, 1);
r.TraceValues();
r.Dispose();
类还有几个 protected
方法,可以帮助构建具有您自己需求的继承类。我只指出其中几个可能感兴趣的
// Validate object not disposed
void ValidateObject();
该方法检查对象是否已释放,如果已释放,则抛出 ObjectDisposedException 异常。
// Check that object is available to be accessed
// throw exception if not
bool _EnsurePointer();
bool _EnsurePointer(bool bThrow);
这两个方法检查结构指针是否为零,包括对象是否已释放。此方法比前面描述的 _IsValid
属性更适合内部使用。
// Object Memory Allocation
void AllocPointer(int _size);
这是分配结构指针的辅助方法。通常,此方法的参数用于 _StructureSize
字段,但这并非强制要求。该方法执行结构指针字段的内部内存分配:_Pointer
,设置析构函数并设置对象的分配标志:_IsAllocated
。
if (!base._EnsurePointer(false))
{
base.AllocPointer(_StructureSize);
}
类中的其他 protected
方法在对象管理或内存管理主题中进行了描述。
析构函数
如前所述,AVBase
类中的结构指针是指向实际 FFmpeg
结构的指针。但是,任何结构都可以使用不同的 FFmpeg
API 进行分配和释放。更重要的是,在创建和调用任何结构的任何初始化方法之后,都可以调用额外的销毁方法或在执行实际对象释放之前的特定取消初始化方法。例如,AVCodecContext
的分配和释放 API。
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
/**
* Free the codec context and everything associated with it and write NULL to
* the provided pointer.
*/
void avcodec_free_context(AVCodecContext **avctx);
同时,AVPacket
对象,除了分配和释放相关 API 外,还具有底层缓冲区解引用的 API,在释放结构之前应该调用它。
void av_packet_unref(AVPacket *pkt);
根据这一点,AVBase
类有两个指向 API 的指针,用于执行结构销毁和释放实际结构对象内存
typedef void TFreeFN(void *);
typedef void TFreeFNP(void **);
// Function to free object
TFreeFN * m_pFree;
// Object Destructor Function (may be set along with any free function)
TFreeFN * m_pDescructor;
// Free Pointer Function
TFreeFNP * m_pFreep;
有两种类型的释放 API 指针,因为不同的 FFmpeg
结构可以有不同的 API 参数来释放底层对象并正确销毁它。如果设置了一个 API,则不使用另一个 API——这两个指针是互斥的。如果设置了析构函数 API,则在释放结构之前的任何 API 之前都会调用它。例如,AVPacket
类内部如何初始化这些指针
FFmpeg::AVPacket::AVPacket()
: AVBase(nullptr,nullptr)
, m_pOpaque(nullptr)
, m_pFreeCB(nullptr)
{
m_pPointer = av_packet_alloc();
m_pFreep = (TFreeFNP*)av_packet_free;
m_pDescructor = (TFreeFN*)av_packet_unref;
av_init_packet((::AVPacket*)m_pPointer);
((::AVPacket*)m_pPointer)->data = nullptr;
((::AVPacket*)m_pPointer)->size = 0;
}
这些指针可以在代码内部根据调用哪个对象方法而更改。因此,在某些情况下,一个对象可以设置为不同的释放函数指针或不同的析构函数,这取决于对象的状态。这允许对象正确销毁分配的数据并清理使用的内存。
对象管理
每个封装库对象都可以属于另一个对象,底层结构字段还可以包含指向另一个 FFmpeg
结构的指针。例如:结构 AVCodecContext
包含指向 AVCodec
结构的 codec 字段;av_class
字段——AVClass
结构等;AVFormatContext
具有类似的字段:iformat
、oformat
、pb
等。更重要的是,结构字段可能不是只读的,并且可以由用户更改,因此在代码中,我们需要处理它。在相关属性中,它是这样设计的
public ref class AVFormatContext : public AVBase
{
public:
/// Get the AVClass for AVFormatContext. It can be used in combination with
/// AV_OPT_SEARCH_FAKE_OBJ for examining options.
static property AVClass^ Class { AVClass^ get(); }
protected:
// Callback
AVIOInterruptDesc^ m_pInterruptCB;
internal:
AVFormatContext(void * _pointer,AVBase^ _parent);
public:
AVFormatContext();
public:
/// A class for logging and @ref avoptions. Set by avformat_alloc_context().
/// Exports (de)muxer private options if they exist.
property AVClass^ av_class { AVClass^ get(); }
/// The input container format.
/// Demuxing only, set by avformat_open_input().
property AVInputFormat^ iformat { AVInputFormat^ get(); void set(AVInputFormat^); }
/// The output container format.
/// Muxing only, must be set by the caller before avformat_write_header().
property AVOutputFormat^ oformat { AVOutputFormat^ get(); void set(AVOutputFormat^); }
内部,此结构子对象的创建没有引用分配的数据——因为最初,该对象是父对象的一部分。因此,处理该对象没有意义。但是,如果此对象由用户设置,这意味着它最初是作为一个独立对象分配的,那么父对象将持有对子对象的引用,直到它通过属性被替换或主对象被释放。在这种情况下,调用子对象的释放没有释放实际对象——它只是递减内部引用计数器。通过这种方式:一个对象可以被创建,并且可以通过属性设置为不同的父对象,在所有情况下,该单个对象将被使用而无需复制,并且一旦所有父对象都被释放,它将被自动处理。换句话说,子对象的释放将在它不属于任何其他父对象时执行。因此,一个对象可以手动创建,对象可以从指针创建作为父对象的一部分,或者对象可以设置为另一个对象的指针属性,并且所有这些对象都可以被正确释放。这是通过两个引用计数器来实现的。一个用于对象内部访问,另一个用于外部对象调用,一旦所有引用变为零——对象被释放。
大多数子对象是在程序第一次访问该属性时创建的。然后将该对象放入子对象集合中,如果程序进行新的相同属性调用,则将使用之前创建的对象,因此不会创建新实例。AVBase
类中存在用于子实例创建的辅助模板
internal:
// Create or Access Child objects
template <class T>
static T^ _CreateChildObject(const void * p, AVBase^ _parent)
{ return _CreateChildObject((void*)p,_parent); }
template <class T>
static T^ _CreateChildObject(void * p,AVBase^ _parent) {
if (p == nullptr) return nullptr;
T^ o = (_parent != nullptr ? (T^)_parent->GetObject((IntPtr)p) : nullptr);
if (o == nullptr) o = gcnew T(p,_parent); return o;
}
template <class T>
T^ _CreateObject(const void * p) { return _CreateChildObject<T>(p,this); }
template <class T>
T^ _CreateObject(void * p) { return _CreateChildObject<T>(p,this); }
子对象集合在主对象释放时被释放。该类包含用于访问集合中子对象的辅助方法
protected:
// Accessing children
AVBase^ GetObject(IntPtr p);
bool AddObject(IntPtr p,AVBase^ _object);
bool AddObject(AVBase^ _object);
void RemoveObject(IntPtr p);
这里最有趣的方法是 AddObject
,它允许将子 AVBase
对象与指定的指针关联起来。如果该指针已与另一个对象关联,则会替换它,并释放前一个对象。
内存管理
每个 AVBase
对象都可以包含为特定属性、内部数据或其他需求分配的内存。该内存存储在与该对象相关的命名集合中。如果重新创建内存指针,则会在集合中替换它。一旦对象被释放——那么所有分配的内存也会被释放。
Generic::SortedList<String^,IntPtr>^ m_ObjectMemory;
类中有几个内存操作方法
IntPtr GetMemory(String^ _key);
void SetMemory(String^ _key,IntPtr _pointer);
IntPtr AllocMemory(String^ _key,int _size);
IntPtr AllocString(String^ _key,String^ _value);
IntPtr AllocString(String^ _key,String^ _value,bool bUnicode);
void FreeMemory(String^ _key);
bool IsAllocated(String^ _key);
这些方法有助于分配和释放内存,并检查其是否已分配。命名访问用于控制指定属性的分配。
void FFmpeg::AVCodecContext::subtitle_header::set(array<byte>^ value)
{
if (value != nullptr && value->Length > 0)
{
((::AVCodecContext*)m_pPointer)->subtitle_header_size = value->Length;
((::AVCodecContext*)m_pPointer)->subtitle_header =
(uint8_t *)AllocMemory("subtitle_header",value->Length).ToPointer();
Marshal::Copy(value,0,
(IntPtr)((::AVCodecContext*)m_pPointer)->subtitle_header,value->Length);
}
else
{
FreeMemory("subtitle_header");
((::AVCodecContext*)m_pPointer)->subtitle_header_size = 0;
((::AVCodecContext*)m_pPointer)->subtitle_header = nullptr;
}
}
</byte>
内存分配使用 FFmpeg 库的 av_maloc
API 完成。还有一个 static
分配内存集合。该集合用于调用 AVBase
类提供的 static
内存分配方法。
IntPtr AllocMemory(String^ _key,int _size);
IntPtr AllocString(String^ _key,String^ _value);
IntPtr AllocString(String^ _key,String^ _value,bool bUnicode);
void FreeMemory(String^ _key);
bool IsAllocated(String^ _key);
Static
内存集合位于一个特殊的 AVMemory
类中。由于该类是 AVMemPtr
的基础——它代表一个已分配指针的特殊对象,也是 AVBase
的基础——因为该类是所有导入结构的基础,并且正如前面提到的,可能需要内存分配。Static
内存会在所有可能使用它的对象被处理后自动释放。
数组
在 FFmpeg
结构中,有很多字段代表不同类型的数据数组。这些数组可以是固定大小的,以指定数据值结尾的数组,数组的数组,以及其他结构的数组。每种类型都可以单独设计,但也有一些通用实现。例如,格式上下文中的流数组实现为其自己的类,带枚举器和索引属性。
ref class AVStreams
: public System::Collections::IEnumerable
, public System::Collections::Generic::IEnumerable<AVStream^>
{
private:
ref class AVStreamsEnumerator
: public System::Collections::IEnumerator
, public System::Collections::Generic::IEnumerator<AVStream^>
{
protected:
AVStreams^ m_pParent;
int m_nIndex;
public:
AVStreamsEnumerator(AVStreams^ streams);
~AVStreamsEnumerator();
public:
// IEnumerator
virtual bool MoveNext();
virtual property AVStream^ Current { AVStream^ get (); }
virtual void Reset();
virtual property Object^ CurrentObject
{ virtual Object^ get() sealed = IEnumerator::Current::get; }
};
protected:
AVFormatContext^ m_pParent;
internal:
AVStreams(AVFormatContext^ _parent) : m_pParent(_parent) {}
public:
property AVStream^ default[int] { AVStream^ get(int index)
{ return m_pParent->GetStream(index); } }
property int Count { int get() { return m_pParent->nb_streams; } }
public:
// IEnumerable
virtual System::Collections::IEnumerator^ GetEnumerator() sealed =
System::Collections::IEnumerable::GetEnumerator
{ return gcnew AVStreamsEnumerator(this); }
public:
// IEnumerable<AVStream^>
virtual System::Collections::Generic::IEnumerator<AVStream^>^
GetEnumeratorGeneric() sealed =
System::Collections::Generic::IEnumerable<AVStream^>::GetEnumerator
{
return gcnew AVStreamsEnumerator(this);
}
};
在这种情况下,我们有一个指向其他结构类 AVStream
的指针数组,并且每个索引属性调用——执行创建父 AVFormatContext
对象的子对象,如对象管理主题中所述。
FFmpeg::AVStream^ FFmpeg::AVFormatContext::GetStream(int idx)
{
if (((::AVFormatContext*)m_pPointer)->nb_streams <=
(unsigned int)idx || idx < 0) return nullptr;
auto p = ((::AVFormatContext*)m_pPointer)->streams[idx];
return _CreateObject<AVStream>((void*)p);
}
在 .NET 中,这看起来就像常规属性和数组访问
var _input = AVFormatContext.OpenInputFile(@"test.avi");
if (_input.FindStreamInfo() == 0)
{
for (int i = 0; i < _input.streams.Count; i++)
{
Console.WriteLine("Stream: {0} {1}",i,_input.streams[i].codecpar.codec_type);
}
}
也可能出现将 AVBase
对象数组设置为属性的情况——在这种情况下,每个对象都被放入父对象的集合中,因此可以安全地处理该对象的释放。除了主结构数组外,与该数组中对象计数相关的属性也可以在内部修改。此外,先前分配的对象和数组内存本身也会被释放。下面的代码显示了如何完成
void FFmpeg::AVFormatContext::chapters::set(array<avchapter^>^ value)
{
{
int nCount = ((::AVFormatContext*)m_pPointer)->nb_chapters;
::AVChapter ** p = (::AVChapter **)((::AVFormatContext*)m_pPointer)->chapters;
if (p)
{
while (nCount-- > 0)
{
RemoveObject((IntPtr)*p++);
}
}
((::AVFormatContext*)m_pPointer)->nb_chapters = 0;
((::AVFormatContext*)m_pPointer)->chapters = nullptr;
FreeMemory("chapters");
}
if (value != nullptr && value->Length > 0)
{
::AVChapter ** p = (::AVChapter **)AllocMemory
("chapters",value->Length * (sizeof(::AVChapter*))).ToPointer();
if (p)
{
((::AVFormatContext*)m_pPointer)->chapters = p;
((::AVFormatContext*)m_pPointer)->nb_chapters = value->Length;
for (int i = 0; i < value->Length; i++)
{
AddObject((IntPtr)*p,value[i]);
*p++ = (::AVChapter*)value[i]->_Pointer.ToPointer();
}
}
}
}
在大多数只读数组的情况下,其中数组值不需要更改,属性会返回一个简单的 .NET 数组
array<FFmpeg::AVSampleFormat>^ FFmpeg::AVCodec::sample_fmts::get()
{
List<AVSampleFormat>^ _array = nullptr;
const ::AVSampleFormat * _pointer = ((::AVCodec*)m_pPointer)->sample_fmts;
if (_pointer)
{
_array = gcnew List<AVSampleFormat>();
while (*_pointer != -1)
{
_array->Add((AVSampleFormat)*_pointer++);
}
}
return _array != nullptr ? _array->ToArray() : nullptr;
}
这些数组的使用方式与 .NET 中的常规方式相同
var codec = AVCodec.FindDecoder(AVCodecID.MP3);
Console.Write("{0} Formats [ ",codec.long_name);
foreach (var fmt in codec.sample_fmts)
{
Console.Write("{0} ",fmt);
}
Console.WriteLine("]");
对于数组访问的其他情况,封装库实现了基类 AVArrayBase
。它继承自 AVBase
类,并以指定的数据大小访问内存块。该类还包含该数组中的元素数量。
public ref class AVArrayBase : public AVBase
{
protected:
bool m_bValidate;
int m_nItemSize;
int m_nCount;
protected:
AVArrayBase(void * _pointer,AVBase^ _parent,int nItemSize,int nCount)
: AVBase(_pointer,_parent) , m_nCount(nCount),
m_nItemSize(nItemSize), m_bValidate(true) { }
AVArrayBase(void * _pointer,AVBase^ _parent,
int nItemSize,int nCount,bool bValidate)
: AVBase(_pointer,_parent) , m_nCount(nCount),
m_nItemSize(nItemSize), m_bValidate(bValidate) { }
protected:
void ValidateIndex(int index) { if (index < 0 || index >= m_nCount)
throw gcnew ArgumentOutOfRangeException(); }
void * GetValue(int index)
{
if (m_bValidate) ValidateIndex(index);
return (((LPBYTE)m_pPointer) + m_nItemSize * index);
}
void SetValue(int index,void * value)
{
if (m_bValidate) ValidateIndex(index);
memcpy(((LPBYTE)m_pPointer) + m_nItemSize * index,value,m_nItemSize);
}
public:
property int Count { int get() { return m_nCount; } }
};
该类是无类型指定的基类。并且类型化数组的主要模板是 AVArray
类。它继承自 AVArrayBase
并已经可以访问数组元素的索引属性。此外,该类支持枚举器。该类对某些类型(如 AVMemPtr
和 IntPtr
)进行了特殊修改,以实现对数组元素的正确访问。模板类在库中大多数情况下都使用,因为它们可以直接访问底层数组内存——没有任何 .NET 封送和复制。例如,类 AVFrame
/AVPicture
具有属性
/// pointers to the image data planes
property AVArray<AVMemPtr^>^ data { AVArray<AVMemPtr^>^ get(); }
/// number of bytes per line
property AVArray<int>^ linesize { AVArray<int>^ get(); }
在同一位置,AVMemPtr
类具有直接访问内存的属性,以及不同类型的数组,这有助于修改或读取数据。下一个示例显示如何访问帧数据
var frame = AVFrame.FromImage((Bitmap)Bitmap.FromFile(@"image.jpg"),
AVPixelFormat.YUV420P);
for (int j = 0; j < frame.height; j++)
{
for (int i = 0; i < frame.linesize[0]; i++)
{
Console.Write("{0:X2}",frame.data[0][i + j * frame.linesize[0]]);
}
Console.WriteLine();
}
frame.Dispose();
类 AVMemPtr
最初,在封装库中,所有操作指针的属性都被设计为 IntPtr 类型。但在 .NET 中直接使用 IntPtr
操作内存并不容易。因为应用程序可能需要更改图片数据、生成图像、执行一些音频和视频处理。为了简化这一点,引入了 AVMemPtr
内存辅助类。但是,它包含一个隐式转换运算符,允许它在接受 IntPtr
作为参数的相同方法中使用。此类的基本用法作为常规 IntPtr
值在下面的 C# 示例中演示
[DllImport("msvcrt.dll", EntryPoint = "strcpy")]
public static extern IntPtr strcpy(IntPtr dest, IntPtr src);
static void Main(string[] args)
{
// Allocate pointer from string
var p = Marshal.StringToCoTaskMemAnsi("some text");
// Create AVMemPtr object and allocate memory buffer of 256 bytes
AVMemPtr s = new AVMemPtr(256);
// Copy zero ending string see exter API above
strcpy(s, p);
// Free Source memory
Marshal.FreeCoTaskMem(p);
// Shows what we have in memory
Console.WriteLine("AVMemPtr string: \"{0}\"", Marshal.PtrToStringAnsi(s));
s.Dispose();
}
在此示例中,我们从给定文本中分配内存。该内存指针和数据的结果可以在下图所示
在执行 C++ strcpy API .NET 封装后,它执行零结尾字符串复制,您可以看到文本已复制到 AVMemPtr
对象的分配指针中
然后,我们显示我们拥有的 string
值
类中的内存分配和释放是通过 FFmpeg API av_alloc
和 av_free
完成的。类具有与不同类型的比较运算符。AVMemPtr
也可以直接从 IntPtr
结构进行转换。此外,该类还可以作为常规字节数组访问数据
var p = Marshal.StringToCoTaskMemAnsi("some text");
AVMemPtr s = p;
int idx = 0;
Console.Write("AVMemPtr data: ");
while (s[idx] != 0) Console.Write(" {0}",(char)s[idx++]);
Console.Write("\n");
Marshal.FreeCoTaskMem(p);
上述代码的执行结果
因此,我们分配了作为 IntPtr
类型分配的指针数据,并轻松地直接访问了它。AVMemPtr
还可以处理加法和减法运算符,它们会生成另一个 AVMemPtr
对象,该对象指向具有结果偏移的另一个地址
var p = Marshal.StringToCoTaskMemAnsi("some text");
AVMemPtr s = p;
Console.Write("AVMemPtr data: ");
while ((byte)s != 0) { Console.Write(" {0}", (char)((byte)s)); s += 1; }
Console.Write("\n");
Marshal.FreeCoTaskMem(p);
每次加法或减法都会创建一个新的 AVMemPtr
实例,但内部实现是,即使数据被分配并且主实例被 DIsposed
,基础数据指针也会保持不变。这也可以通过计算主实例的引用来完成,例如,这段代码将正常工作
var p = Marshal.StringToCoTaskMemAnsi("some text");
AVMemPtr s = new AVMemPtr(256);
strcpy(s, p);
Marshal.FreeCoTaskMem(p);
AVMemPtr s1 = s + 3;
s.Dispose();
Console.WriteLine("AVMemPtr string: \"{0}\"",Marshal.PtrToStringAnsi(s1));
s1.Dispose();
在下一个代码中,我们可以看到 s
、s0
和 s1
对象的实例是不同的,但比较运算符决定了数据是否相等
var p = Marshal.StringToCoTaskMemAnsi("some text");
AVMemPtr s = p;
AVMemPtr s1 = s + 1;
AVMemPtr s0 = s1 - 1;
Console.WriteLine(" s {0} s1 {1} s0 {2}", s,s1,s0);
Console.WriteLine(" s == s1 {0}, s == s0 {1},s0 == s1 {2}",
(s == s1),(s == s0),(s0 == s1));
Console.WriteLine(" {0}, {1}, {2}",
object.ReferenceEquals(s,s1), object.ReferenceEquals(s,s0),
object.ReferenceEquals(s0,s1));
Marshal.FreeCoTaskMem(p);
AVMemPtr
类还包含可能有用的辅助方法和属性,其中之一是确定数据是否已分配以及缓冲区的字节大小,以及用于将指针数据转储到文件或 Stream 的调试辅助方法。
该类的主要特点是能够将数据表示为不同类型的数组。这是通过 AVArray
模板的不同属性实现的
property AVArray<byte>^ bytes { AVArray<byte>^ get(); }
property AVArray<short>^ shorts { AVArray<short>^ get(); }
property AVArray<int>^ integers { AVArray<int>^ get(); }
property AVArray<float>^ floats { AVArray<float>^ get(); }
property AVArray<double>^ doubles { AVArray<double>^ get(); }
property AVArray<IntPtrv^ pointers { AVArray<IntPtr>^ get(); }
property AVArray<unsigned int>^ uints { AVArray<unsigned int>^ get(); }
property AVArray<unsigned short>^ ushorts { AVArray<unsigned short>^ get(); }
property AVArray<RGB^>^ rgb { AVArray<RGB^>^ get(); }
property AVArray<RGBA^>^ rgba { AVArray<RGBA^>^ get(); }
property AVArray<AYUV^>^ ayuv { AVArray<AYUV^>^ get(); }
property AVArray<YUY2^>^ yuy2 { AVArray<YUY2^>^ get(); }
property AVArray<UYVY^>^ uyvy { AVArray<UYVY^>^ get(); }
因此,例如,很容易用 IEEE float 类型或 short 有符号类型填充 AVFrame
对象的音频数据
var samples = frame.data[0].shorts;
for (int j = 0; j < c.frame_size; j++)
{
samples[2 * j] = (short)(int)(Math.Sin(t) * 10000);
for (int k = 1; k < c.channels; k++)
samples[2 * j + k] = samples[2 * j];
t += tincr;
}
此外,正如您所看到的,有一些具有指定像素格式结构的数组:RGB
、RGBA
、AYUV
、YUY2
和 UYVY
。使用这些属性是访问这些格式像素的设计方式,稍后将对此进行描述。
枚举器
访问资源列表(例如枚举现有输入或输出格式)的 FFmpeg
API 具有特殊类型的 API——迭代器。新 API 具有带有不透明参数的 iterate
,旧 API 使用前一个元素作为引用
const AVCodec *av_codec_iterate(void **opaque);
而旧的
AVCodec *av_codec_next(const AVCodec *c);
最初,封装库被设计为以与 FFmpeg
中相同的方式使用这些 API,但后来的实现使用了 IEnumerable 和 IEnumerator 接口。这使我们可以使用 C# 的 foreach 运算符进行枚举,而不是直接调用 API
foreach (AVCodec codec in AVCodec.Codecs)
{
if (codec.IsDecoder()) Console.WriteLine(codec);
}
上面的代码可以通过几种其他方式修改。大多数枚举器对象都公开 Count
属性和索引器
var codecs = AVCodec.Codecs;
for (int i = 0; i < codecs.Count; i++)
{
if (codecs[i].IsDecoder()) Console.WriteLine(codecs[i]);
}
也可以使用旧方法调用 API 包装器方法
AVCodec codec = null;
while (null != (codec = AVCodec.Next(codec)))
{
if (codec.IsDecoder()) Console.WriteLine(codec);
}
无需担心 FFmpeg
库是否仅公开一个 API:av_codec_iterate
或 av_codec_next
。在封装库中,所有标记为已弃用的 FFmpeg
API 都被动态链接,因此它会在运行时检查要使用哪个 API。这如何实现将在后面的独立主题中介绍。
库中实现的 AVArray
类也公开了一个枚举器接口
var p = Marshal.StringToCoTaskMemAnsi("some text");
AVMemPtr s = new AVMemPtr(256);
strcpy(s, p);
Marshal.FreeCoTaskMem(p);
var array = s.bytes;
foreach (byte c in array)
{
if (c == 0) break;
Console.Write("{0} ",(char)c);
}
Console.Write("\n");
s.Dispose();
在上面的示例中,我们在创建 AVMemPtr
对象时分配了内存长度。但是,枚举可能不起作用,因为数组可以动态创建或不带大小初始化——因此枚举器无法确定元素的总数。我限制了此功能,以避免越界崩溃和其他异常。例如,以下代码将不起作用,因为 AVMemPtr
对象的数据大小为 0
,这是由于从 IntPtr
指针转换为 AVMemPtr
导致的,因此您可以找到与前面示例的区别
var p = Marshal.StringToCoTaskMemAnsi("some text");
AVMemPtr s = p;
var array = s.bytes;
foreach (byte c in array)
{
if (c == 0) break;
Console.Write("{0} ",(char)c);
}
Console.Write("\n");
s.Dispose();
在下面的屏幕截图中,可以看到 AVMemPtr
对象的数据正确地指向了文本行,但由于大小未知,因此枚举不可用。
封装库正确处理 AVPicture
/AVFrame
类的数据字段枚举。这是通过一个内部函数完成的,该函数处理检测对象字段大小。因此,以下代码示例可以正常工作
AVFrame frame = new AVFrame();
frame.Alloc(AVPixelFormat.BGR24, 320, 240);
frame.MakeWritable();
var rgb = frame.data[0].rgb;
foreach (RGB c in rgb)
{
c.Color = Color.AliceBlue;
}
var bmp = frame.ToBitmap();
bmp.Save(@"test.bmp");
bmp.Dispose();
frame.Dispose();
库中的许多类都支持枚举器。除了上面示例中枚举编解码器之外,枚举器类型还用于枚举输入输出格式、解析器、过滤器、设备和其他库资源。
类 AVColor
如前所述,AVMemPtr
类可以将数据访问生成为某些颜色空间类型的数组,如 RGB24
、RGBA
、AYUV
、YUY2
或 UYVY
。这些颜色空间设计为具有访问颜色分量的结构。这些结构的基础类是 AVColor
。它包含基本运算符、辅助属性和方法。例如,它支持分配常规颜色值并重写 string
表示
AVFrame frame = new AVFrame();
frame.Alloc(AVPixelFormat.BGRA, 320, 240);
frame.MakeWritable();
var rgb = frame.data[0].rgba;
rgb[0].Color = Color.DarkGoldenrod;
Console.Write("{0} {1}",rgb[0].ToString(),rgb[0].Color);
frame.Dispose();
此外,该类还具有颜色分量访问能力,并隐式转换为直接 IntPtr
类型。另外,内部该类支持颜色空间转换,如 RGB
到 YUV
和 YUV
到 RGB
。
var ayuv = frame.data[0].ayuv;
ayuv[0].Color = Color.DarkGoldenrod;
Console.Write("{0} {1}",ayuv[0].ToString(),ayuv[0].Color);
在上面的代码中,颜色与上一个示例中的相同,被设置为 AYUV
颜色空间。因此,内部将其从 ARGB
转换为 AYUV
,产生了以下输出
转换结果与原始值略有不同,但这可以接受。默认情况下,使用了 BT.601 转换矩阵。可以通过调用 AVColor
类的 SetColorspaceMatrices
static
方法来更改矩阵系数。此外,该类还包含用于颜色空间组件转换的 static
方法
Color c = Color.DarkGoldenrod;
int y = 0, u = 0, v = 0;
int r = 0, g = 0, b = 0;
AVColor.RGBToYUV(c.R,c.G,c.B, ref y, ref u, ref v);
AVColor.YUVToRGB(y,u,v, ref r, ref g, ref b);
Console.Write("src: [R: {0} G: {1} B: {2}] dst: [R: {3} G: {4} B: {5}]",c.R,c.G,c.B,r,g,b);
必须记住,YUY2
和 UYVY
控制两个像素。因此,设置 Color
属性会影响这两个像素。要控制每个像素,您应该使用“Y0
”和“Y1
”类属性来更改亮度值。
核心类
封装库包含许多类,如前所述,它们代表 FFmpeg
的每个结构,将数据字段公开为属性以及相关的用法方法。本主题描述了其中大多数类,但不是全部。描述还包含这些类的基本用法示例,不描述类字段,因为它们与原始 FFmpeg
库中的相同。所有代码示例都是可运行的,但在代码部分跳过了对某些全局对象销毁调用 Dispose()
方法。另外,需要检查方法调用的错误结果,您应该在代码中这样做,但在示例中,为了减少代码大小也跳过了。
AVPacket
该类是 FFmpeg libavcodec
库的 AVPacket
结构上的封装。它具有不同的构造函数,可以传入自己的缓冲区,并带有检测缓冲区何时将被释放的回调。在每次成功从任何解码或编码操作返回后,都需要调用 Free
方法来取消引用底层缓冲区。该类包含 static
方法,用于从指定的 .NET 数组创建 AVPacket 对象,数组类型不同。
Dump
方法允许您将数据包数据保存到文件或流中。
// Create frame object from the image
var f = AVFrame.FromImage((Bitmap)Bitmap.FromFile(@"image.jpg"),AVPixelFormat.YUV420P);
// Create encoder context
var c = new AVCodecContext(AVCodec.FindEncoder(AVCodecID.MPEG2VIDEO));
// Setting up encoder parameters
c.bit_rate = 400000;
c.width = f.width;
c.height = f.height;
c.time_base = new AVRational(1, 25);
c.framerate = new AVRational(25, 1);
c.pix_fmt = f.format;
// Open context
c.Open(null);
// Create packet object
AVPacket pkt = new AVPacket();
bool got_packet = false;
while (!got_packet)
{
// Encode frame until we got packet
c.EncodeVideo(pkt,f,ref got_packet);
}
// Save packet int a file
pkt.Dump(@"pkt.bin");
// unref packet buffer
pkt.Free();
pkt.Dispose();
AVPicture
该类封装了 FFmpeg libavcodec
库旧版本中的相关结构。在封装库中,它支持当前实现的所有 FFmpeg 版本。该类包含用于 FFmpeg
库的 av_image_
* API 的方法,以及指向数据和平面大小的指针。AVFrame
继承自此类,因此建议直接使用 AVFrame
。
AVFrame
用于 FFmpeg libavutil
库的 AVFrame
结构的类。该类用作编码音频或视频数据的输入,或作为解码过程的输出。它可以处理音频和视频数据。在每次成功从任何解码或编码操作返回后,都需要调用 Free
方法来取消引用底层缓冲区。
// Open Input File Context
var _input = AVFormatContext.OpenInputFile(@"test.mp4");
// Check for the streams
if (_input.FindStreamInfo() == 0) {
// Get Video Stream Index
int idx = _input.FindBestStream(AVMediaType.VIDEO, -1, -1);
// Initialize Decoder For That Stream
AVCodecContext decoder = _input.streams[idx].codec;
var codec = AVCodec.FindDecoder(decoder.codec_id);
// Open Decoder Context
if (decoder.Open(codec) == 0) {
// Create frame and packet objects
AVPacket pkt = new AVPacket();
AVFrame frame = new AVFrame();
int index = 0;
// Reading Packets from the input file
while ((bool)_input.ReadFrame(pkt)) {
// Check the stream index
if (pkt.stream_index == idx) {
bool got_frame = false;
// Decode Video
int ret = decoder.DecodeVideo(frame, ref got_frame, pkt);
if (got_frame) {
// Once we got frame convert it into Bitmap object
// and saves into a file
var bmp = AVFrame.ToBitmap(frame);
bmp.Save(string.Format(@"image{0}.png",++index));
bmp.Dispose();
// Free frame data
frame.Free();
}
}
// Free packet data
pkt.Free();
}
}
}
Frame
对象可以从现有的 .NET Bitmap 对象初始化。在下面的代码中,帧是从图像创建并转换为 YUV 420
平面颜色空间格式。
var frame = AVFrame.FromImage((Bitmap)Bitmap.FromFile(@"image.jpg"),
AVPixelFormat.YUV420P);
也可以包装现有位图数据,在这种情况下,无需指定目标像素格式作为第二个参数。此类帧数据的像素格式将为 BGRA
,并且图像数据不会复制到新分配的缓冲区中。但是,内部会创建一个包装图像数据的子类对象,该对象将在实际帧被释放时被释放。
还有一些可以用于转换视频帧的辅助方法
var src = AVFrame.FromImage((Bitmap)Bitmap.FromFile(@"image.jpg"));
var frame = AVFrame.ConvertVideoFrame(src,AVPixelFormat.YUV420P);
以及转换回 .NET Bitmap
对象
var bmp = AVFrame.ToBitmap(frame);
bmp.Save(@"image.png");
AVCodec
实现了对 FFmpeg libavcodec
库的 AVCodec
结构的托管访问的类。如建议的,它只包含底层结构的 public
字段。该类描述了库中注册的 codec
。
var codec = AVCodec.FindEncoder("libx264");
Console.WriteLine("{0}",codec);
该类能够枚举现有编解码器
foreach (AVCodec codec in AVCodec.Codecs)
{
Console.WriteLine(codec);
}
AVCodecContext
实现了对 FFmpeg libavcodec
库的 AVCodecContext
结构的托管访问的类。该对象用于编码或解码视频和音频数据。
// Create Encoder Context
var c = new AVCodecContext(AVCodec.FindEncoder("libx264"));
// Initialize Encoder Parameters
c.bit_rate = 400000;
c.width = 352;
c.height = 288;
c.time_base = new AVRational(1, 25);
c.framerate = new AVRational(25, 1);
c.gop_size = 10;
c.max_b_frames = 1;
c.pix_fmt = AVPixelFormat.YUV420P;
// Open Context
c.Open(null);
Console.WriteLine(c);
在上面的示例中,我们创建了一个上下文对象,并初始化了视频的 **H264** 编码器,分辨率为 352x288,帧率为 25 帧/秒,比特率为 400 kbps。
下一个示例演示了打开文件并初始化视频流的解码器
AVFormatContext fmt_ctx;
// Open Input File Context
AVFormatContext.OpenInput(out fmt_ctx, @"test.mp4");
// Check streams
fmt_ctx.FindStreamInfo();
// Get the Video Stream Object
var s = fmt_ctx.streams[fmt_ctx.FindBestStream(AVMediaType.VIDEO)];
// Create Decoder Context object for that stream
var c = new AVCodecContext(AVCodec.FindDecoder(s.codecpar.codec_id));
// Copy decoder parameters from the stream
s.codecpar.CopyTo(c);
// Open Codec Context
c.Open(null);
Console.WriteLine(c);
该类包含初始化结构以及解码或编码音频和视频的方法。
AVCodecParser
用于从托管代码访问 FFmpeg libavcodec
库的 AVCodecParser
结构的类。结构描述了注册的 codec
解析器。该类主要用于枚举库中注册的解析器
foreach (AVCodecParser parser in AVCodecParser.Parsers)
{
for (int i = 0; i < parser.codec_ids.Length; i++)
{
Console.Write("{0} ", parser.codec_ids[i]);
}
}
AVCodecParserContext
用于执行比特流解析操作的类。它封装了 FFmpeg libavcodec
库的 AVCodecParserContext
结构。它使用指定的 codec
创建,并包含不同变体的 parse
方法。
// Create Decoder Context for MP3 audio
var decoder = new AVCodecContext(AVCodec.FindDecoder(AVCodecID.MP3));
// Specify desired decoder sample format
decoder.sample_fmt = AVSampleFormat.FLTP;
// Open the context
if (decoder.Open(null) == 0) {
// Create parset context for our codec
var parser = new AVCodecParserContext(decoder.codec_id);
AVPacket pkt = new AVPacket();
AVFrame frame = new AVFrame();
// Opens the mp3 file
var f = fopen(@"test.mp3", "rb");
const int buffer_size = 1024;
IntPtr data = Marshal.AllocCoTaskMem(buffer_size);
// Reading the file data
int data_size = fread(data, 1, buffer_size, f);
while (data_size > 0) {
// Parse readed data
int ret = parser.Parse(decoder, pkt, data, data_size);
if (ret < 0) break;
data_size -= ret;
// Once we have packet data
if (pkt.size > 0) {
bool got_frame = false;
// Decode it
decoder.DecodeAudio(frame,ref got_frame,pkt);
if (got_frame) {
// Output IEEE floats wrap into characters
var p = frame.data[0].floats;
for (int i = 0; i < frame.nb_samples; i++) {
Console.Write((char)(((p[i] + 1.0) * 54) + 32));
}
// Free frame buffer
frame.Free();
}
// Free packet buffer
pkt.Free();
}
// Shift outstanding data and continue file reading
memmove(data, data + ret, data_size);
data_size += fread(data + data_size, 1, buffer_size - data_size, f);
}
// Free allocated buffer and close file
Marshal.FreeCoTaskMem(data);
fclose(f);
}
示例演示了从原始 mp3 文件读取数据,按块解析,然后解码,并通过简单的打印将数据夹断到输出。示例使用了一些非托管 API 的封装:fopen、fread 和 fclose。这些函数声明可以在本文档的其他示例中找到。
AVRational
实现了对 FFmpeg libavutil
库的 AVRational
结构的托管访问的类。它包含与该结构相关的所有方法。
AVRational r = new AVRational(25,1);
Console.WriteLine("{0}, {1}",r.ToString(),r.inv_q().ToString());
AVFormatContext
实现了对 FFmpeg libavformat
库的 AVFormatContext
结构的托管访问的类。该类包含用于操作输出或输入媒体数据的函数。
var ic = AVFormatContext.OpenInputFile(@"test.mp4");
ic.DumpFormat(0,null,false);
该类最常见的用法是从输入中获取音频或视频数据包,或将这些数据包写入输出。下一个示例演示了从 AVI 文件读取 mp3 音频数据包并将其保存到单独的文件中。
// Open input file context
var ic = AVFormatContext.OpenInputFile(@"1.avi");
// Check streams
if (0 == ic.FindStreamInfo()) {
// Get the audio stream index
int idx = ic.FindBestStream(AVMediaType.AUDIO);
if (idx >= 0
&& ic.streams[idx].codecpar.codec_id == AVCodecID.MP3) {
// Start reading packets in case if we have MP3 audio
AVPacket pkt = new AVPacket();
bool append = false;
while (ic.ReadFrame(pkt) == 0) {
if (pkt.stream_index == idx) {
// Saves packets into a file
pkt.Dump(@"out.mp3",append);
append = true;
}
pkt.Free();
}
pkt.Dispose();
}
}
ic.Dispose();
如前所述,另一个主要任务是输出数据包。下一个示例演示了将静态图像输出到 mp4 文件,使用 **H264** 编码 10 秒钟。
string filename = @"test.mp4";
// Open output file context
var oc = AVFormatContext.OpenOutput(filename);
// Create frame from image file
var frame = AVFrame.FromImage((Bitmap)Bitmap.FromFile(@"image.jpg"),
AVPixelFormat.YUV420P);
// Create and initialize video encoder context
var codec = AVCodec.FindEncoder(AVCodecID.H264);
var ctx = new AVCodecContext(codec);
// Add new stream
var st = oc.AddStream(codec);
// Set stream settings
st.id = oc.nb_streams-1;
st.time_base = new AVRational( 1, 25 );
// Set encoder parameters
ctx.bit_rate = 400000;
ctx.width = frame.width;
ctx.height = frame.height;
ctx.time_base = st.time_base;
ctx.framerate = ctx.time_base.inv_q();
ctx.pix_fmt = frame.format;
// Open encoder
ctx.Open(null);
// Copy parameters into stream
st.codecpar.FromContext(ctx);
// Open underlaying file IO context
if ((int)(oc.oformat.flags & AVfmt.NOFILE) == 0) {
oc.pb = new AVIOContext(filename,AvioFlag.WRITE);
}
// Display format information
oc.DumpFormat(0, filename, true);
// Start writing into the file
oc.WriteHeader();
AVPacket pkt = new AVPacket();
int idx = 0;
bool flush = false;
while (true) {
// Output for 10 seconds long
flush = (idx > ctx.framerate.num * 10 / ctx.framerate.den);
frame.pts = idx++;
bool got_packet = false;
// Encode video frame
if (0 < ctx.EncodeVideo(pkt, flush ? null : frame, ref got_packet)) break;
if (got_packet) {
// Configure timestamps
pkt.RescaleTS(ctx.time_base, st.time_base);
// Write resulted packet into the file
oc.WriteFrame(pkt);
pkt.Free();
continue;
}
if (flush) break;
}
// End writing
oc.WriteTrailer();
pkt.Dispose();
frame.Dispose();
ctx.Dispose();
oc.Dispose();
以及应用程序的输出结果
AVIOContext
实现了对 FFmpeg libavformat
库的 AVIOContext
结构的托管访问的类。该类包含用于从字节流写入/读取数据的辅助方法。该类最有趣的用法是能够设置自定义读取或写入回调。为了演示如何使用这些功能,让我们修改前面的代码示例,替换 AVIOContext
创建代码
AVMemPtr ptr = new AVMemPtr(20 * 1024);
OutputFile file = new OutputFile(filename);
if ((int)(oc.oformat.flags & AVfmt.NOFILE) == 0) {
oc.pb = new AVIOContext(ptr,1,file,null,OutputFile.WritePacket,OutputFile.Seek);
}
现在,我们通过定义输出和搜索回调来创建一个上下文。这些回调在示例的 OutputFile
类中定义
class OutputFile
{
// File Handle
IntPtr stream = IntPtr.Zero;
public OutputFile(string filename) {
// Opens the file for writing
stream = fopen(filename, "w+b");
}
~OutputFile() {
// Close the file
IntPtr s = Interlocked.Exchange(ref stream, IntPtr.Zero);
if (s != IntPtr.Zero) fclose(s);
}
// Write data callback
public static int WritePacket(object opaque, IntPtr buf, int buf_size) {
return fwrite(buf, 1, buf_size, (opaque as OutputFile).stream);
}
// File seek callback
public static int Seek(object opaque, long offset, AVSeek whence) {
return fseek((opaque as OutputFile).stream, (int)offset, (int)whence);
}
[DllImport("msvcrt.dll", EntryPoint = "fopen")]
public static extern IntPtr fopen(
[MarshalAs(UnmanagedType.LPStr)] string filename,
[MarshalAs(UnmanagedType.LPStr)] string mode);
[DllImport("msvcrt.dll")]
public static extern int fwrite(IntPtr buffer, int size, int count, IntPtr stream);
[DllImport("msvcrt.dll")]
public static extern int fclose(IntPtr stream);
[DllImport("msvcrt.dll")]
public static extern int fseek(IntPtr stream, long offset, int origin);
}
通过运行示例,您将获得与上一个主题相同的输出文件和结果,但现在您可以控制每个写入和搜索数据操作。
该类还包含一些静态辅助方法。它可以检查 **URL** 并枚举支持的协议
Console.Write("Output Protocols: \n");
var protocols = AVIOContext.EnumProtocols(true);
foreach (var s in protocols) {
Console.Write("{0} ",s);
}
AVOutputFormat
实现了对 FFmpeg libavformat
库的 AVOutputFormat
结构的托管访问的类。该结构描述了 libavformat
库支持的输出格式的参数。该类包含一个枚举器,用于查看所有可用的格式
foreach (AVOutputFormat format in AVOutputFormat.Formats)
{
Console.WriteLine(format.ToString());
}
该类没有 public
构造函数,但可以通过枚举器或 static
方法访问
var fmt = AVOutputFormat.GuessFormat(null,"test.mp4",null);
Console.WriteLine(fmt.long_name);
AVInputFormat
实现了对 FFmpeg libavformat
库的 AVInputFormat
结构的托管访问的类。该结构描述了 libavformat
库支持的输入格式的参数。该类还包含一个枚举器,用于查看所有可用的格式
foreach (AVInputFormat format in AVInputFormat.Formats)
{
Console.WriteLine(format.ToString());
}
该类没有 public
构造函数。可以通过 static
方法访问
var fmt = AVInputFormat.FindInputFormat("avi");
Console.WriteLine(fmt.long_name);
也可以检测给定缓冲区的格式,也使用 static
方法
// Opens the file
var f = fopen(@"test.mp4", "rb");
// Setting up probe data and allocate buffer
AVProbeData data = new AVProbeData();
data.buf_size = 1024;
data.buf = Marshal.AllocCoTaskMem(data.buf_size);
// Read data from the file
data.buf_size = fread(data.buf,1,data.buf_size,f);
// Check the data
var fmt = AVInputFormat.ProbeInputFormat(data,true);
Console.WriteLine(fmt.ToString());
// Free memory and close file
Marshal.FreeCoTaskMem(data.buf);
data.Dispose();
fclose(f);
AVStream
实现了对 FFmpeg libavformat
库的 AVStream
结构的托管访问的类。该类没有 public
构造函数,并且可以通过 AVFormatContext
的 streams 属性访问。
var input = AVFormatContext.OpenInputFile(@"test.mp4");
if (input.FindStreamInfo() == 0){
for (int idx = 0; idx < input.streams.Count; idx++)
{
Console.WriteLine("Stream [{0}]: {1}",idx,
input.streams[idx].codecpar.codec_type);
}
}
input.DumpFormat(0,null,false);
可以使用 AVFormatContext AddStream
方法创建流。
// Open Output Context
var oc = AVFormatContext.OpenOutput(@"test.mp4");
// Find Encoder
var codec = AVCodec.FindEncoder(AVCodecID.H264);
// Create Encoder Context
var c = new AVCodecContext(codec);
// Add Video Stream to Output
var st = oc.AddStream(codec);
st.id = oc.nb_streams-1;
st.time_base = new AVRational( 1, 25 );
// Initialize codec parameters
c.codec_id = codec.id;
c.bit_rate = 400000;
c.width = 352;
c.height = 288;
c.time_base = st.time_base;
c.gop_size = 12;
c.pix_fmt = AVPixelFormat.YUV420P;
// Open encoder context
c.Open(codec);
// Copy codec parameters to stream
st.codecpar.FromContext(c);
oc.DumpFormat(0, null, true);
AVDictionary
实现了对 FFmpeg libavutil
库的 AVDictionary
集合类型的托管访问的类。此类是 string
值的名称-值集合,并包含用于访问它们的函数、枚举器和属性。FFmpeg
API 中无法从集合中删除特殊条目,因此我们在封装库中具有相同的功能。AVDictionary
类支持键值条目的枚举。该类还支持使用 ICloneable 接口复制数据。
AVDictionary dict = new AVDictionary();
dict.SetValue("Key1","Value1");
dict.SetValue("Key2","Value2");
dict.SetValue("Key3","Value3");
Console.Write("Number of elements: \"{0}\"\nIndex of \"Key2\": \"{1}\"\n",
dict.Count,dict.IndexOf("Key2"));
Console.Write("Keys:\t");
foreach (string key in dict.Keys) {
Console.Write(" \"" + key + "\"");
}
Console.Write("\nValues:\t");
for (int i = 0; i < dict.Values.Count; i++) {
Console.Write(" \"" + dict.Values[i] + "\"");
}
Console.Write("\nValue[\"Key1\"]:\t\"{0}\"\nValue[3]:\t\"{1}\"\n",
dict["Key1"],dict[2]);
AVDictionary cloned = (AVDictionary)dict.Clone();
Console.Write("Cloned Entries: \n");
foreach (AVDictionaryEntry ent in cloned) {
Console.Write("\"{0}\"=\"{1}\"\n", ent.key, ent.value);
}
cloned.Dispose();
dict.Dispose();
上述代码的执行结果是
AVOptions
辅助类,用于访问库对象的选项。对象可以从指针或从 AVBase
类创建,如前所述。枚举现有对象选项的示例
// Create Encoder Context
var ctx = new AVCodecContext(AVCodec.FindEncoder(AVCodecID.H264));
Console.WriteLine("{0} Options:",ctx);
// Create options object for encoder context
var options = new AVOptions(ctx);
for (int i = 0; i < options.Count; i++)
{
string value = "";
if (0 == options.get(options[i].name, AVOptSearch.None, out value))
{
Console.WriteLine("{0}=\"{1}\"\t'{2}'",options[i].name,value,options[i].help);
}
}
该类能够枚举选项名称和选项参数,还可以获取或设置对象的选项值。
var ctx = new AVCodecContext(AVCodec.FindEncoder(AVCodecID.H264));
var options = new AVOptions(ctx);
string initial = "";
options.get("b", AVOptSearch.None, out initial);
options.set_int("b", 100000, AVOptSearch.None);
string updated = "";
options.get("b", AVOptSearch.None, out updated);
Console.WriteLine("Initial=\"{0}\", Updated\"{1}\"",initial,updated);
为了兼容旧的封装库版本,库类的一些属性会在内部设置对象选项,而不是直接使用字段。下面给出了 AVFilterGraph scale_sws_opts
字段中如何实现该操作的示例
void FFmpeg::AVFilterGraph::scale_sws_opts::set(String^ value)
{
auto p = _CreateObject<AVOptions>(m_pPointer);
p->set("scale_sws_opts",value,AVOptSearch::None);
}
还可以通过库的 AVClass
对象枚举可用选项
// Get available options for AVCodecContext
var options = AVClass.GetCodecClass().option;
for (int i = 0; i < options.Length; i++)
{
Console.WriteLine("\"{0}\"",options[i]);
}
在 FFmpeg
中,每个上下文结构都有 AVClass
对象访问权限,因此下面的代码会产生相同的结果
var ctx = new AVCodecContext(AVCodec.FindEncoder(AVCodecID.H264));
var options = ctx.av_class.option;
for (int i = 0; i < options.Length; i++)
{
Console.WriteLine("\"{0}\"",options[i]);
}
AVBitStreamFilter
实现了对 FFmpeg libavcodec
库的 AVBitStreamFilter
类型的托管访问的类。它包含名称和可应用于过滤器的 codec
ID 数组。所有过滤器都可以被枚举
foreach (AVBitStreamFilter f in AVBitStreamFilter.Filters)
{
Console.WriteLine("{0}",f);
}
可以通过名称搜索过滤器
var f = AVBitStreamFilter.GetByName("h264_mp4toannexb");
Console.Write("{0} Codecs: ",f);
foreach (AVCodecID id in f.codec_ids)
{
Console.Write("{0} ",id);
}
AVBSFContext
实现了对 FFmpeg libavcodec
库的 AVBSFContext
结构类型的托管访问的类。它是比特流过滤器的一个实例,并包含用于过滤操作的函数。
类使用示例如下
// Open Input File Context
var input = AVFormatContext.OpenInputFile(@"test.mp4");
if (input.FindStreamInfo() == 0) {
// Get video stream index
int idx = input.FindBestStream(AVMediaType.VIDEO);
// Create Bit Stream Filter Context
var ctx = new AVBSFContext(
AVBitStreamFilter.GetByName("h264_mp4toannexb"));
// Set context parameters from the stream
input.streams[idx].codecpar.CopyTo(ctx.par_in);
ctx.time_base_in = input.streams[idx].time_base;
// Initialize Context
if (0 == ctx.Init()) {
bool append = false;
AVPacket pkt = new AVPacket();
// Read Packets from the file
while (0 == input.ReadFrame(pkt)) {
// Process video packets
if (pkt.stream_index == idx) {
ctx.SendPacket(pkt);
}
pkt.Free();
if (0 == ctx.ReceivePacket(pkt)) {
// Save resulted packet data
pkt.Dump(@"out.h264",append);
pkt.Free();
append = true;
}
}
pkt.Dispose();
}
ctx.Dispose();
}
上面的代码执行了从打开的 mp4 媒体文件到 annexb 格式(带起始码的格式)的 **H264** 视频流的转换,并将生成的比特流保存到文件中。生成的文件可以用 **VLC** 媒体播放器、graphedit
工具或 ffplay
播放。
在文件的十六进制转储中,可以看到数据是 annexb 格式
AVChannelLayout
来自 libavutil
库的音频通道掩码定义的辅助值类 AV_CH_*
。该类以 public static class
字段的形式包含所有可用定义
该类表示 64 位整数值并涵盖隐式转换运算符。它还包含辅助方法,作为 FFmpeg
库 API 的封装,并重写了值的 string
表示。
AVChannelLayout ch = AVChannelLayout.LAYOUT_5POINT1;
Console.WriteLine("{0} Channels: \"{1}\" Description: \"{2}\"",
ch,ch.channels,ch.description);
AVChannelLayout ex = ch.extract_channel(2);
Console.WriteLine("Extracted 2 Channel: {0} Index: \"{1}\" Name: \"{2}\"",
ex,ch.get_channel_index(ex),ex.name);
ch = AVChannelLayout.get_default_channel_layout(7);
Console.WriteLine("Default layout for 7 channels {0} "+
"Channels: \"{1}\" Description: \"{2}\"",
ch,ch.channels,ch.description);
上面的代码显示了与该类的基本操作。所有类方法都基于 FFmpeg
库 API,因此很容易理解如何使用它们。上述代码的执行结果
封装库中还有一个 AVChannels
类,它是一个独立的 static
类,包含 AVChannelLayout
的导出 API。
AVPixelFormat
另一个同名枚举的辅助值类(来自 libavutil
库)。它描述了视频数据的像素格式。该类包含原始枚举公开的所有格式。此外,该类还扩展了基于 FFmpeg
API 的方法和属性。它处理从整数类型到整数类型的隐式转换。
AVPixelFormat fmt = AVPixelFormat.YUV420P;
byte[] bytes = BitConverter.GetBytes(fmt.codec_tag);
string tag = "";
for (int i = 0; i < bytes.Length; i++) { tag += (char)bytes[i]; }
Console.WriteLine("Format: {0} Name: {1} Tag: {2} Planes: {3} " +
"Components: {4} Bits Per Pixel: {5}",(int)fmt,
fmt.name,tag,fmt.planes, fmt.format.nb_components,
fmt.format.bits_per_pixel);
fmt = AVPixelFormat.get_pix_fmt("rgb32");
Console.WriteLine("{0} {1}",fmt.name, (int)fmt);
FFLoss loss = FFLoss.NONE;
fmt = AVPixelFormat.find_best_pix_fmt_of_2(AVPixelFormat.RGB24,
AVPixelFormat.RGB32,AVPixelFormat.RGB444BE,false,ref loss);
Console.WriteLine("{0} {1}",fmt.name, (int)fmt);
上述代码的执行结果是
AVSampleFormat
也是一个同名 libavutil
枚举的辅助类。它管理音频数据的格式描述。与其他类一样,它处理基本操作并公开作为 FFmpeg
API 实现的方法和属性。
AVSampleFormat fmt = AVSampleFormat.FLTP;
Console.WriteLine("Format: {0}, Name: {1}, Planar: {2}, Bytes Per Sample: {3}",
(int)fmt,fmt.name,fmt.is_planar,fmt.bytes_per_sample);
fmt = AVSampleFormat.get_sample_fmt("s16");
var alt = fmt.get_alt_sample_fmt(false);
var pln = fmt.get_planar_sample_fmt();
Console.WriteLine("Format: {0} Alt: {1} Planar: {2}",fmt, alt, pln);
操作 AVSampleFormat
的 FFmpeg
API 也导出一个独立的 static
类 AVSampleFmt
。
AVSamples
暴露 libavutil
API 的一些有用方法的辅助 static
类,并操作音频数据样本。它能够分配存储音频数据的缓冲区(具有不同格式),并将数据从扩展缓冲区复制到 AVFrame
/AVPicture
类。
接下来的代码显示了如何为音频数据分配缓冲区,用静音填充该缓冲区,并将解码后的音频帧复制到该缓冲区。
// Open input file
var input = AVFormatContext.OpenInputFile(@"test.mp4");
if (input.FindStreamInfo() == 0) {
// Open Decoder Context For the Audio Stream
int idx = input.FindBestStream(AVMediaType.AUDIO);
AVCodecContext decoder = input.streams[idx].codec;
var codec = AVCodec.FindDecoder(decoder.codec_id);
if (decoder.Open(codec) == 0) {
AVPacket pkt = new AVPacket();
AVFrame frame = new AVFrame();
bool got_frame = false;
// Read packets from file
while (input.ReadFrame(pkt) == 0 && !got_frame) {
if (pkt.stream_index == idx) {
// Decode audio data
decoder.DecodeAudio(frame,ref got_frame,pkt);
if (got_frame) {
// Gets the size of the buffer
int size = AVSamples.get_buffer_size(frame.channels,
frame.nb_samples, frame.format);
// Allocate the buffer
AVMemPtr ptr = new AVMemPtr(size);
IntPtr[] data = new IntPtr[frame.channels];
// Setup channels pointers
AVSamples.fill_arrays(ref data, ptr,
frame.channels, frame.nb_samples, frame.format);
// Set silence to created buffer
AVSamples.set_silence(data, 0,
frame.nb_samples, frame.channels, frame.format);
ptr.Dump(@"silence.bin");
// Copy decode data into that buffer
AVSamples.copy(data, frame, 0, 0, frame.nb_samples);
ptr.Dump(@"data.bin");
ptr.Dispose();
frame.Free();
}
}
pkt.Free();
}
frame.Dispose();
pkt.Dispose();
}
}
AVMath
暴露 libavutil
库的一些有用数学 API 的辅助 static
类。当然,不一定非要使用它们,因为我们有 .NET 的数学库,但在某些情况下,它们可能会很有帮助。例如,时间戳从一个基数转换为另一个基数,以及时间戳比较。
AVCodecContext c = new AVCodecContext(null);
//... Initialize encoder context
AVFrame frame = new AVFrame();
//... Prepare frame data
frame.pts = AVMath.rescale_q(frame.nb_samples,
new AVRational(1, c.sample_rate), c.time_base);
//... Pass frame to encoder
在上面的代码片段中,执行了从采样率基数到上下文时间基时间戳的转换,后者将传递给编码器。
SwrContext
libswresample
库的重采样上下文结构的托管封装。该类包含与底层库结构协同工作的方法和属性。它允许设置属性、初始化上下文并执行音频数据的重采样。
下一个代码示例演示了如何初始化重采样上下文并执行转换。它打开一个带有音频流的文件。解码音频数据。将重采样后的数据转换为 S16 立体声格式,采样率与输入时相同,并将结果保存到二进制文件中。fopen
、fwrite
和 fclose
函数的定义可以在 AVIOContext
代码示例中找到。
// Open input file
var input = AVFormatContext.OpenInputFile(@"test.mp4");
if (input.FindStreamInfo() == 0) {
// Initialize and open decoder context for audio stream
int idx = input.FindBestStream(AVMediaType.AUDIO);
AVCodecContext decoder = input.streams[idx].codec;
var codec = AVCodec.FindDecoder(decoder.codec_id);
decoder.sample_fmt = codec.sample_fmts != null ?
codec.sample_fmts[0] : AVSampleFormat.FLTP;
if (decoder.Open(codec) == 0) {
AVPacket pkt = new AVPacket();
AVFrame frame = new AVFrame();
AVMemPtr ptr = null;
SwrContext swr = null;
// Open destination file
IntPtr file = fopen(@"out.bin","w+b");
// Reading packets from input
while (input.ReadFrame(pkt) == 0) {
if (pkt.stream_index == idx) {
bool got_frame = false;
// Decode audio
decoder.DecodeAudio(frame,ref got_frame,pkt);
if (got_frame) {
int size = AVSamples.get_buffer_size(frame.channels,
frame.nb_samples, frame.format);
if (swr == null) {
// Create resampling context
swr = new SwrContext(AVChannelLayout.LAYOUT_STEREO,
AVSampleFormat.S16, frame.sample_rate,
frame.channel_layout, frame.format, frame.sample_rate);
swr.Init();
}
if (ptr != null && ptr.size < size) {
ptr.Dispose();
ptr = null;
}
if (ptr == null) {
// Allocate output buffer
ptr = new AVMemPtr(size);
}
// Perform resampling
int count = swr.Convert(new IntPtr[] { ptr },
frame.nb_samples,frame.data,frame.nb_samples);
if (count > 0) {
int bps = AVSampleFmt.get_bytes_per_sample(AVSampleFormat.S16)
* frame.channels;
// Saving all data into output file
fwrite(ptr,bps,count,file);
}
frame.Free();
}
}
pkt.Free();
}
fclose(file);
if (swr != null) swr.Dispose();
if (ptr != null) ptr.Dispose();
frame.Dispose();
pkt.Dispose();
}
}
结果文件可以使用 ffplay
配合格式参数播放。如果您的音频数据采样率为 44100 Hz,否则请在下一个命令行参数中替换速率。
SwsContext
libswscale
库的缩放上下文结构的托管封装。该类支持视频数据的缩放和颜色空间转换。
下一个示例代码演示了如何初始化缩放上下文并执行视频帧到图像文件的转换。它打开一个带有视频流的文件。解码视频数据。初始化上下文和临时数据指针,创建一个与分配的数据指针关联的 .NET Bitmap
对象,并将每个帧保存到文件中。
// Open input file
var input = AVFormatContext.OpenInputFile(@"test.mp4");
if (input.FindStreamInfo() == 0) {
// Open decoder context for the video stream
int idx = input.FindBestStream(AVMediaType.VIDEO);
AVCodecContext decoder = input.streams[idx].codec;
var codec = AVCodec.FindDecoder(decoder.codec_id);
if (decoder.Open(codec) == 0) {
AVPacket pkt = new AVPacket();
AVFrame frame = new AVFrame();
AVMemPtr ptr = null;
SwsContext sws = null;
int image = 0;
Bitmap bmp = null;
// Reading packets
while (input.ReadFrame(pkt) == 0) {
if (pkt.stream_index == idx) {
bool got_frame = false;
// Decode video
decoder.DecodeVideo(frame,ref got_frame,pkt);
if (got_frame) {
if (sws == null) {
// Create scaling context object
sws = new SwsContext(frame.width, frame.height, frame.format,
frame.width, frame.height, AVPixelFormat.BGRA,
SwsFlags.FastBilinear);
}
if (ptr == null) {
// Allocate picture buffer
int size = AVPicture.GetSize(AVPixelFormat.BGRA,
frame.width, frame.height);
ptr = new AVMemPtr(size);
}
// Convert input frame into RGBA image
sws.Scale(frame,0,frame.height,new IntPtr[] { ptr },
new int[] { frame.width << 2 });
if (bmp == null)
{
// Create Bitmap object for given buffer
bmp = new Bitmap(frame.width, frame.height,
frame.width << 2, PixelFormat.Format32bppRgb, ptr);
}
// Saves bitmap into a file
bmp.Save(string.Format(@"image{0}.png",image++));
frame.Free();
}
}
pkt.Free();
}
if (sws != null) sws.Dispose();
if (ptr != null) ptr.Dispose();
if (bmp != null) bmp.Dispose();
frame.Dispose();
pkt.Dispose();
}
}
这种帧转换由库通过 AVFrame.ToBitmap()
方法实现。您可以在 AVFrame
示例代码中看到它是如何完成的。当前示例的区别在于数据缓冲区和位图对象仅分配一次,这提供了更好的性能。
AVFilter
libavfilter
库的 AVFilter
结构的托管封装。该类描述了过滤平台中的每个过滤器项。它定义了对象字段和方法。该类没有 public
构造函数,并且可以从 static
方法访问。
var f = AVFilter.GetByName("vflip");
Console.WriteLine("Name: \"{0}\"\nDescription: \"{1}\"\nFlags: {2}, ",
f.name, f.description,f.flags);
for (int i = 0; i < f.inputs.Count; i++)
Console.WriteLine("Input[{0}] Name: \"{1}\" Type: \"{2}\"",
i + 1, f.inputs[i].name, f.inputs[i].type);
for (int i = 0; i < f.outputs.Count; i++)
Console.WriteLine("Output[{0}] Name: \"{1}\" Type: \"{2}\"",
i + 1, f.outputs[i].name, f.outputs[i].type);
该类还能够枚举库中可用的现有过滤器。
foreach (var f in AVFilter.Filters)
{
Console.WriteLine(f);
}
AVFilterContext
托管类表示 libavfilter
库的 AVFilterContext
结构。它用于描述过滤器图中的过滤器实例。它没有 public
构造函数,只能通过调用 AVFilterGraph 类的方法来创建。
// Create filter graph object
AVFilterGraph graph = new AVFilterGraph();
// Create vertical flip filter context instance
AVFilterContext ctx = graph.CreateFilter(AVFilter.GetByName("vflip"),"My Filter");
Console.WriteLine("Name: \"{0}\" Filter: \"{1}\" Ready: \"{2}\"",
ctx.name, ctx.filter, ctx.ready);
每个过滤器上下文的输出都可以链接到另一个过滤器的上下文输入。有几个特殊过滤器:sink 和 source。Source 能够接收数据,应首先插入到图链中。Sink 是链的终点。Sink 提供来自过滤器图的输出。
源过滤器的名称分别为视频的“buffer
”和音频的“abuffer
”,创建此类过滤器始终需要初始化参数
AVFilterGraph graph = new AVFilterGraph();
// Create video source filter context
AVFilterContext src = graph.CreateFilter(AVFilter.GetByName("buffer"),"in",
"video_size=640x480:pix_fmt=0:time_base=1/25:pixel_aspect=1/1",IntPtr.Zero);
目标过滤器的名称分别为视频和音频的“buffersink
”和“abuffersink
”。扩展之前的代码,添加 sink 过滤器创建并将其与之前创建的视频源连接。
// Create video sink filter context
var sink = graph.CreateFilter(AVFilter.GetByName("buffersink"),"out");
// Connect sink with source directly
src.Link(sink);
// Configure filter graph
graph.Config();
var input = sink.inputs[0];
Console.WriteLine("Type: \"{0}\" W: \"{1}\" H: \"{2}\" Format: \"{3}\"",
input.type, input.w, input.h,((AVPixelFormat)input.format).name);
一旦我们调用图的配置方法,它就会设置链参数,因此我们在 sink 输入端具有格式设置。
有一些特殊类有助于操作 sink 和 source 过滤器上下文。AVBufferSrc
- 具有初始化过滤器的方法,并将输入帧提供给底层源过滤器上下文。AVBufferSink
- 包含获取图端点属性和接收结果帧的能力。
下一个完整的示例代码执行了对一个图像文件的图片进行垂直翻转滤波效果,并将结果保存到另一个文件中
// Create frame from image file
var frame = AVFrame.FromImage((Bitmap)Bitmap.FromFile(@"image.jpg"),
AVPixelFormat.YUV420P);
string fmt = string.Format(
"video_size={0}x{1}:pix_fmt={2}:time_base=1/1:pixel_aspect1/1",
frame.width, frame.height, (int)frame.format);
// Create filter graph
AVFilterGraph graph = new AVFilterGraph();
// Create source context
AVFilterContext src = graph.CreateFilter(AVFilter.GetByName("buffer"),
"in",fmt, IntPtr.Zero);
// Create vertical flip filter context
var flip = graph.CreateFilter(AVFilter.GetByName("vflip"),"My Filter");
// Create sink context
var sink = graph.CreateFilter(AVFilter.GetByName("buffersink"),"out");
// Connect filters
src.Link(flip);
flip.Link(sink);
graph.Config();
// Create sink and source helper objects
AVBufferSink _sink = new AVBufferSink(sink);
AVBufferSrc _source = new AVBufferSrc(src);
// Add frame for processing
_source.add_frame(frame);
frame.Free();
// Get resulted frame
_sink.get_frame(frame);
// Save Frame into a file
frame.ToBitmap().Save(@"out_image.jpg");
AVFilterGraph
libavfilter
库的 AVFilterGraph
结构的托管类。该类管理过滤器链以及它们之间的连接。它包含前面描述的图配置和过滤器上下文创建方法。此外,图还可以枚举链中的所有过滤器
AVFilterGraph graph = new AVFilterGraph();
var src = graph.CreateFilter(AVFilter.GetByName("buffer"),"in",
"video_size=640x480:pix_fmt=0:time_base=1/25:pixel_aspect=1/1",IntPtr.Zero);
var flip = graph.CreateFilter(AVFilter.GetByName("vflip"),"My Filter");
var sink = graph.CreateFilter(AVFilter.GetByName("buffersink"),"out");
foreach (AVFilterContext f in graph.filters)
{
Console.Write("\"" + f + "\" ");
}
以附加方式初始化图,该类允许从参数字符串生成图,该字符串包含过滤器名称及其参数。在这种情况下,源和 sink 过滤器应以常规方式创建,并为中间过滤器链初始化设置输入和输出参数。
// Create filters graph
AVFilterGraph graph = new AVFilterGraph();
// Create sink and source context
var src = graph.CreateFilter(AVFilter.GetByName("buffer"),"in",
"video_size=640x480:pix_fmt=0:time_base=1/25:pixel_aspect=1/1",IntPtr.Zero);
var sink = graph.CreateFilter(AVFilter.GetByName("buffersink"),"out");
// Setup outputs
AVFilterInOut outputs = new AVFilterInOut();
outputs.name = "in";
outputs.filter_ctx = src;
// Setup inputs
AVFilterInOut inputs = new AVFilterInOut();
inputs.name = "out";
inputs.filter_ctx = sink;
// Initialize filters change from configuration string
graph.ParsePtr("vflip,scale=2:3", inputs, outputs);
graph.Config();
foreach (AVFilterContext f in graph.filters)
{
Console.Write("\"" + f + "\" ");
}
上面的代码演示了过滤器链的创建和从初始化参数配置图。样本输出
我们将“in
”和“out
”过滤器添加到图中,设置输入和输出结构,并调用方法来构建一个包含两个中间过滤器的链:垂直翻转和缩放。
AVDevices
用于访问 libavdevice
库 API 的托管 static
类。所有方法都是 static
的,并允许访问设备集合。
AVInputFormat fmt = null;
do {
fmt = AVDevices.input_video_device_next(fmt);
if (fmt != null) {
Console.WriteLine(fmt);
}
} while (fmt != null);
设备已注册以便使用 libavformat
API 访问。无需额外的注册 API 调用,因为一旦访问 AVDevices
类或任何 libavformat
封装类 API,就会执行所有必需的 API 调用。每个格式类都包含可用的选项来从输入设备子系统访问目标设备。可以从输入格式列出选项
var fmt = AVInputFormat.FindInputFormat("dshow");
foreach (var opt in fmt.priv_class.option) {
Console.WriteLine(opt);
}
选项应在创建 AVFormatContext
时设置。您可以指定分辨率或设备编号。例如,让我们看看如何显示可用的 DirectShow
设备列表
AVDictionary opt = new AVDictionary();
opt.SetValue("list_devices", "true");
var fmt = AVInputFormat.FindInputFormat("dshow");
AVFormatContext ctx = null;
AVFormatContext.OpenInput(out ctx, fmt, "video=dummy", opt);
AVDeviceInfoList
描述 libavdevice
库的 AVDeviceInfo
结构的托管类。该类处理设备集合,并且可以在不带 public
构造函数的情况下从 AVDevices
类的方法访问。
AVDeviceInfoList devices = AVDevices.list_input_sources(
AVInputFormat.FindInputFormat("dshow"),null,null);
if (devices != null) {
foreach (AVDeviceInfo dev in devices) {
Console.WriteLine(dev);
}
}
AVDeviceInfo
libavdevice
库的 AVDeviceInfo
结构的托管类。该类包含设备描述和设备名称的 string
。它可以从 AVDeviceInfoList
类访问。
高级
根据以上文档,我们可以安排一些高级主题。在大多数封装库使用场景中,这可能不是必需的,但了解实现的其他功能是很好的。
委托和回调
在某些 API 用法场景中,需要能够为 API 或结构方法设置回调。此类回调函数设计为实现为 static
方法,并且它们带有 opaque
对象作为参数。每个回调方法都有一个委托
public delegate void AVBufferFreeCB(Object^ opaque, IntPtr buf);
在库中,只需知道方法的委托外观,并创建一个具有相同参数和返回类型的回调方法。回调可以作为函数或对象构造函数的参数设置,甚至设计为对象属性。
static void buffer_free(object opaque, IntPtr buf)
{
Marshal.FreeCoTaskMem(buf);
}
static void Main(string[] args)
{
int cb = 1024 * 1024;
var pkt = new AVPacket(Marshal.AllocCoTaskMem(cb), cb, buffer_free, null);
//...
pkt.Dispose();
}
上面的例子展示了如何创建带有自定义分配数据的 AVPacket
。一旦数据包被释放,就会调用传递的释放回调方法。如前所述,可以使用 opaque
并将其转换为用作回调创建的另一个参数的对象
static int read_packet(object opaque, IntPtr buf, int buf_size)
{
var s = (opaque as Stream);
long available = (s.Length - s.Position);
if (available == 0) return AVRESULT.EOF;
if ((long)buf_size > available) buf_size = (int)available;
var buffer = new byte[buf_size];
buf_size = s.Read(buffer,0,buf_size);
Marshal.Copy(buffer, 0, buf,buf_size);
return buf_size;
}
static void Main(string[] args)
{
var stream = File.OpenRead(@"Test.avi");
var avio_ctx = new AVIOContext(new AVMemPtr(4096),
0, stream, read_packet, null, null);
//...
}
上面的例子演示了在 AVIOContext
对象的构造函数中传递的回调实现。
库中的所有回调都有自己的后台——原生回调函数,它作为托管代码和非托管代码之间的层,并以正确的方式向托管回调方法提供参数。当我们设置上面 AVPacket
示例代码的回调方法中的断点时,可以轻松显示这一点
在调用堆栈中,可以看到 buffer_free
回调方法是从内部库函数调用的,该函数将不透明指针转换为相关 AVPacket
对象的 GCHandle。然后该对象调用该类的通用释放处理程序,该处理程序最终调用作为变量与用户的 opaque
值一起保存的传递的回调方法。
作为属性设置的回调具有相同的实现。例如:AVCodecContext
结构类使用一个设置为上下文属性的不透明对象——以同样的方式,它在原生 FFmpeg
库中完成
// Format selector class
class FormatSelector
{
private AVPixelFormat m_Format;
// Save preferred format for selection
public FormatSelector(AVPixelFormat fmt) { m_Format = fmt; }
// Format selector handler
public AVPixelFormat SelectFormat(AVPixelFormat[] fmts)
{
foreach (var fmt in fmts)
{
if (fmt == m_Format) return fmt;
}
return AVPixelFormat.NONE;
}
}
// Get Pixel format callback
public static AVPixelFormat GetPixelFormat(AVCodecContext s, AVPixelFormat[] fmts)
{
var h = GCHandle.FromIntPtr(s.opaque);
if (h.IsAllocated)
{
// Call our object selector
return ((FormatSelector)h.Target).SelectFormat(fmts);
}
// Call the default selector
return s.default_get_format(fmts);
}
static void Main(string[] args)
{
// Create selector object
var selector = new FormatSelector(AVPixelFormat.YUV420P);
// Create context
var ctx = new AVCodecContext(null);
// Sets opaque
ctx.opaque = GCHandle.ToIntPtr(GCHandle.Alloc(selector, GCHandleType.Weak));
// Set callback
ctx.get_format = GetPixelFormat;
//...
}
在上面的代码中,我们演示了如何为 AVCodecContext
结构的文件格式选择属性设置回调。我们创建一个选择器类对象,在其中尝试选择 **YUV420P** 像素格式。我们将此类作为 GCHandle
指针作为上下文的不透明对象提供。选择器对象必须保持活动状态,直到可以使用回调方法,因为我们设置了一个 弱 句柄类型。我们有一个 GetPixelFormat
回调方法,其中我们将 AVCodecContext opaque
对象用作我们的格式选择器类,并使用其方法执行选择。由于我们有一个 Weak
句柄类型,底层对象可以被释放,在这种情况下,我们调用 AVCodecContext 类默认格式选择器方法。
动态 API
封装库的设计目的是链接到 FFmpeg
库中的指定 .lib 文件。当前实现链接到这些 .lib 文件导出的 FFmpeg
API 条目。但是,如前所述,封装库旨在支持不同版本的 FFmpeg
库,在新版本中,API 可能被弃用或根据 FFmpeg
的构建选项而可选添加。为了妥善处理此类构建情况,一些导入的 API 在代码中被动态检查。内部对象方法实现可能因 FFmpeg
库中存在哪个 API 而异。此实现隐藏在内部,因此用户只看到常规方法。例如,在某些 FFmpeg
版本中,av_codec_next
不存在,其功能被 av_codec_iterate
API 替换
const AVCodec *av_codec_iterate(void **opaque);
#if FF_API_NEXT
/**
* If c is NULL, returns the first registered codec,
* if c is non-NULL, returns the next registered codec after c,
* or NULL if c is the last one.
*/
attribute_deprecated
AVCodec *av_codec_next(const AVCodec *c);
#endif
此外,在新 FFmpeg
版本中,avcodec_register_all
API 也被弃用,并且不再是导出的 API。
为了处理此类版本差异并避免构建错误,对这些 API 的访问是通过动态进行的。以下是评估 avcodec_register_all
API 的实现外观
void FFmpeg::LibAVCodec::RegisterAll()
{
AVBase::EnsureLibraryLoaded();
if (!s_bRegistered)
{
s_bRegistered = true;
VOID_API(AVCodec,avcodec_register_all)
avcodec_register_all();
}
}
而这里是编解码器迭代的实现方式
bool FFmpeg::AVCodec::AVCodecs::AVCodecEnumerator::MoveNext()
{
AVBase::EnsureLibraryLoaded();
const ::AVCodec * p = nullptr;
void * opaque = m_pOpaque.ToPointer();
LOAD_API(AVCodec,::AVCodec *,av_codec_next,const ::AVCodec*);
LOAD_API(AVCodec,::AVCodec *,av_codec_iterate,void **);
if (av_codec_iterate != nullptr)
{
p = av_codec_iterate(&opaque);
}
else
{
if (av_codec_next != nullptr)
{
p = av_codec_next((const ::AVCodec*)opaque);
opaque = (void*)p;
}
}
m_pOpaque = IntPtr(opaque);
m_pCurrent = (p != nullptr) ? gcnew AVCodec((void*)p, nullptr) : nullptr;
return (m_pCurrent != nullptr);
}
在上面的代码中,有一些辅助宏:VOID_API
和 LOAD_API
,它们从指定的 DLL 模块加载 API,如果存在某个 API,则使用它,否则跳过 API 调用。动态 DLL 是 AVBase
类的静态部分,并在其第一次构造函数调用中加载。
internal:
static bool s_bDllLoaded = false;
static HMODULE m_hLibAVUtil = nullptr;
static HMODULE m_hLibAVCodec = nullptr;
static HMODULE m_hLibAVFormat = nullptr;
static HMODULE m_hLibAVFilter = nullptr;
static HMODULE m_hLibAVDevice = nullptr;
static HMODULE m_hLibPostproc = nullptr;
static HMODULE m_hLibSwscale = nullptr;
static HMODULE m_hLibSwresample = nullptr;
辅助宏定义在 AVCore.h 文件中
//////////////////////////////////////////////////////
#define LOAD_API(lib,result,api,...) \
typedef result (WINAPIV *PFN_##api)(__VA_ARGS__); \
PFN_##api api = (AVBase::m_hLib##lib != nullptr ?
(PFN_##api)GetProcAddress(AVBase::m_hLib##lib,#api) : nullptr);
//////////////////////////////////////////////////////
#define DYNAMIC_API(lib,result,api,...) \
LOAD_API(lib,result,api,__VA_ARGS__); \
if (api)
//////////////////////////////////////////////////////
#define DYNAMIC_DEF_API(lib,result,_default,api,...) \
LOAD_API(lib,result,api,__VA_ARGS__); \
if (!api) return _default;
#define DYNAMIC_DEF_SYM(lib,result,_default,sym) \
void * pSym = (AVBase::m_hLib##lib != nullptr ?
GetProcAddress(AVBase::m_hLib##lib,#sym) : nullptr); \
if (!pSym) return _default; \
result sym = (result)pSym;
//////////////////////////////////////////////////////
#define VOID_API(lib,api,...) DYNAMIC_API(lib,void,api,__VA_ARGS__)
#define PTR_API(lib,api,...) DYNAMIC_API(lib,void *,api,__VA_ARGS__)
#define INT_API(lib,api,...) DYNAMIC_API(lib,int,api,__VA_ARGS__)
#define INT_API2(lib,_default,api,...) DYNAMIC_DEF_API(lib,int,_default,api,__VA_ARGS__)
//////////////////////////////////////////////////////
LOAD_API
宏定义 API 变量并从指定的 FFmpeg
库加载它。
DYNAMIC_API
- 加载 API,如果 API 存在,则执行下一行代码。如果需要切换动态或静态 API 链接,或者使用不同的访问方式,这很有用。
FFmpeg::AVRational^ FFmpeg::AVBufferSink::frame_rate::get()
{
::AVRational r = ((::AVFilterLink*)m_pContext->
inputs[0]->_Pointer.ToPointer())->frame_rate;
DYNAMIC_API(AVFilter,::AVRational,av_buffersink_get_frame_rate,::AVFilterContext *)
r = av_buffersink_get_frame_rate((::AVFilterContext *)m_pContext->
_Pointer.ToPointer());
return gcnew AVRational(r.num,r.den);
}
DYNAMIC_DEF_API
- 加载 API,如果无法加载,则返回指定的默认值。
int FFmpeg::AVBufferSink::format::get()
{
DYNAMIC_DEF_API(AVFilter,int,m_pContext->inputs[0]->format,
av_buffersink_get_format,::AVFilterContext *);
return av_buffersink_get_format((::AVFilterContext *)m_pContext->
_Pointer.ToPointer());
}
其他宏只是返回类型的变体。
如果导出的 API 被用作 static
方法,或者用在非 AVBase
子类的类中,请确保在使用这些宏之前调用 AVBase::EnsureLibraryLoaded();
方法,或者直接检查 AVBase::s_bDllLoaded
变量。
结构指针
如前所述:每个 AVBase
类都暴露了一个指向底层 FFmpeg
结构的指针。这是为了能够扩展库功能或手动管理现有 API。例如,AVBase._Pointer
字段可用于直接对象转换或在导出的 API 中手动使用。
[DllImport("avcodec-58.dll")]
private static extern void av_packet_move_ref(IntPtr dst,IntPtr src);
public static AVPacket api_raw_call_example(AVPacket pkt)
{
// Create packet object
AVPacket dst = new AVPacket();
// Use packet structure pointer directly in exported API
av_packet_move_ref(dst._Pointer,pkt._Pointer);
return dst;
}
注意:对象方法可能会在内部管理析构函数和释放结构 API,因此原始 API 的这种使用存在内存泄漏的风险。
原始指针也可用于直接访问任何结构字段。
扩展库
为了展示使用 C++/CLI 实现整个包装库的好处,特添加此主题。可以通过直接访问现有库对象的属性,使用 C# 或其他 .NET 语言实现代码的不同部分。还可以通过添加对任何可能缺失的 API 的支持,或满足其他任何需求来扩展现有功能。如前所述,这些 API 可以与结构指针一起使用,而 AVBase
类用于处理这些结构。要包装自己的结构,只需让该结构继承 AVBase
类并管理其属性即可。下面是一个简单的实现示例。
public class MyAVStruct : AVBase
{
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Ansi)]
private struct S
{
[MarshalAs(UnmanagedType.I4)]
public int Value;
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=200)]
public string Name;
}
public MyAVStruct() {
if (!base._EnsurePointer(false)) {
base.AllocPointer(_StructureSize);
}
}
public override int _StructureSize { get { return Marshal.SizeOf(typeof(S)); } }
public int Value
{
get { return Marshal.PtrToStructure<S>(base._Pointer).Value;}
set {
if (base._EnsurePointer())
{
var s = Marshal.PtrToStructure<S>(base._Pointer);
s.Value = value;
Marshal.StructureToPtr<S>(s, base._Pointer, false);
}
}
}
public string Name
{
get { return Marshal.PtrToStructure<S>(base._Pointer).Name;}
set {
if (base._EnsurePointer())
{
var s = Marshal.PtrToStructure<S>(base._Pointer);
s.Name = value;
Marshal.StructureToPtr<S>(s, base._Pointer, false);
}
}
}
}
我们有一个名为“S
”的结构,我们为其创建了一个名为“MyAVStruct
”的包装对象,以便从 .NET 访问它。通过调用 AVBase
类的 AllocPointer
方法来分配数据指针。此外,通过每次获取或设置属性值时对整个结构进行封送来访问该结构的每个字段。一个调用该结构的示例。
// Create Structure instance
var s = new MyAVStruct();
if (s._IsValid) // Check if the pointer allocated
{
// Set field values
s.Value = 22;
s.Name = "Some text";
// Get values
Console.WriteLine("0x{0:x} StructureSize: {1}
Allocated: {2}\n Name: \"{3}\" Value: {4}",
s._Pointer, s._StructureSize, s._IsAllocated,
s.Name,s.Value);
}
// Destroy structure and free data
s.Dispose();
当然,上面代码中的结构实现并未优化。它仅用于展示功能。请注意,每次封送结构的操作在 C++/CLI 实现中也不会执行,因为这些字段在内部直接访问,这是一个很大的优点。在 .NET 中,我们可以优化上面的结构。
// Our structure
public class MyAVStruct : AVBase
{
// Underlaying Internal structure for wrapper
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Ansi)]
private struct S
{
[MarshalAs(UnmanagedType.I4)]
public int Value;
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=200)]
public string Name;
}
// private structure for optimization
private S m_S = new S();
// Updated flag
private bool m_bUpdated = false;
// Constructor
public MyAVStruct() {
// Just to show so AVBase API usage
if (!base._EnsurePointer(false)) {
// Allocate Structure
base.AllocPointer(_StructureSize);
Update();
}
}
// Method For Update private structure
protected void Update() {
m_S = Marshal.PtrToStructure<S>(base._Pointer);
m_bUpdated = false;
}
// Size of Structure for allocation
public override int _StructureSize { get { return Marshal.SizeOf(typeof(S)); } }
// Pointer access
public override IntPtr _Pointer {
get {
if (m_bUpdated && base._EnsurePointer()) {
Marshal.StructureToPtr<S>(m_S, base._Pointer, false);
m_bUpdated = false;
}
return base._Pointer;
}
}
// Structure Fields Accessing
public int Value { get { return m_S.Value; }
set { m_S.Value = value; m_bUpdated = true; } }
public string Name { get { return m_S.Name; }
set { m_S.Name = value; m_bUpdated = true; } }
// Example of Exported API from library
[DllImport("avutil.dll", EntryPoint = "SomeAPIThatChangeS")]
private static extern int ChangeS(IntPtr s);
// Call API
public void Change() {
if (ChangeS(this._Pointer) == 0){
Update(); // Update structure fields
}
}
}
在修改后的结构中,我们有一个标志:“m_bUpdate
”,用于控制字段更新。如果任何字段值被更新,则设置该标志,当我们访问原始指针时,执行结构封送。同时,还有一个带有结构值的导出 API 调用示例。在调用临时内部结构后,更新为实际指针的值。
正如你所见,这是一个简单的实现示例,FFmpeg
库的结构并不简单,无法以这种方式管理所有结构,或者至少不能管理所有字段,每次直接从指针封送字段然后将它们转换为一个或另一个对象也十分困难。但这对 C++/CLI 来说并不难处理,因此选择了它进行实现。
示例
我制作了几个 C# 示例来展示如何使用包装库。其中一些是原生 FFmpeg
文档中附带的示例的包装,因此您可以轻松比较实现。我还添加了一些我个人认为会很有趣的示例。所有示例代码都位于“Sources\Examples”文件夹中。项目文件可以从“Sources\Examples\proj”文件夹加载,或者只需打开库解决方案文件即可访问所有示例。所有包装标准 FFmpeg
示例的 C# 示例与原生示例的工作方式相同。一些示例描述包含执行截图。
音频播放
使用 libavformat
解码并重采样音频数据,然后使用 WinMM Windows API 进行播放的媒体文件加载示例。
Avio 读取
标准的 FFmpeg libavformat
AVIOContext
API 示例。使 libavformat
demuxer 通过自定义 AVIOContext
读取回调来访问媒体内容。
解码音频
一个标准的 FFmpeg
音频解码 libavcodec
API 示例的 C# 包装。
解码视频
一个标准的 FFmpeg
视频解码 libavcodec
API 示例的 C# 包装。
解封装与解码
一个标准的 FFmpeg
demuxing 和解码示例的 C# 包装。展示如何使用 libavformat
和 libavcodec
API 来 demux 和解码音频和视频数据。
编码音频
一个标准的 FFmpeg
音频编码 libavcodec
API 示例的 C# 包装。
编码视频
一个标准的 FFmpeg
视频编码 libavcodec
API 示例的 C# 包装。
过滤音频
一个标准的 FFmpeg libavfilter
API 用法示例的 C# 包装。此示例将生成一个正弦波音频,通过一个简单的滤波器链,然后计算输出数据的 MD5 校验和。
音频过滤
一个标准的 FFmpeg
音频解码和滤波 API 示例的 C# 包装。
视频过滤
一个标准的 FFmpeg
视频解码和滤波 API 示例的 C# 包装。
元数据
一个标准的 FFmpeg
示例的 C# 包装。展示了元数据 API 如何在应用程序中使用。
封装
一个标准的 FFmpeg libavformat
API 示例的 C# 包装。以任何支持的 libavformat
格式输出媒体文件。
重新封装
一个标准的 FFmpeg libavformat
/libavcodec
demuxing 和 muxing API 示例的 C# 包装。将流从一种容器格式重新封装到另一种格式。
音频重采样
一个标准的 FFmpeg libswresample
API 用法示例的 C# 包装。程序展示了如何使用 libswresample
对音频流进行重采样。它生成一系列音频帧,将它们重采样到指定的输出格式和速率,并将它们保存到输出文件中。
视频缩放
一个标准的 FFmpeg libswscale
API 用法示例的 C# 包装。程序展示了如何使用 libswscale
缩放图像。它生成一系列图片,将它们缩放到给定的尺寸,并将它们保存到输出文件中。
屏幕捕获
使用 Windows GDI 缩放屏幕捕获,使用 libswscale 缩放,使用 libavcodec
编码,并使用 libavformat
保存到文件。
转码
一个标准的 FFmpeg
demuxing、解码、滤波、编码和 muxing API 示例的 C# 包装。
视频播放
使用 libavformat
解码并缩放视频帧加载媒体文件的示例,然后使用 Winforms 和 GDI+ API 进行播放。这只是一个示例,不建议使用相同的方式来显示视频。对于视频输出,最好使用 GPU 输出而不是 GDI+ 进行播放,因为 CPU 开销很大。
源代码
GitHub 上的代码仓库可以在 https://github.com/S0NIC/FFmpeg.NET 找到。
编译后的库
应要求,我为 x86 和 x64 平台添加了包装库的 Release 构建版本,使用了 4.2.2 版本的 lgpl FFmpeg。要在 Visual Studio 中使用它,请从 zip 存档中将 FFmpeg.NET.dll 程序集引用添加到您的项目中。最好在应用程序的构建设置中将平台配置为“x64”或“x86”,因为“AnyCPU”会根据系统在运行时加载。一旦应用程序编译完成,将编译后的库构建存档的全部内容复制到最终位置。需要记住的是——包装库与这些 DLL 链接,并使用相关的 FFmpeg
库的构建设置和配置文件,所以不要替换它们。
构建项目
构建位于“Solutions”文件夹中的库的解决方案。目标 PC 应安装 Windows SDK(我的系统上是 10 版本)和 .NET Framework(v 4.5.2),并且可以使用 Visual Studio 2017(v141)平台工具集构建项目。可以使用不同版本的 SDK、.NET Framework 和构建工具集。它们可以直接在项目中配置。
FFmpeg
库和头文件位于“ThirdParty\ffmpeg\x.x.x”,其中“x.x.x”是 FFmpeg
版本。里面最多有四个子文件夹:“bin”包含 DLL 库,“lib”包含链接库,“include”包含导出的 API 头文件,也可以将 FFmpeg
源文件放在“sources”文件夹中。源文件(如果存在)必须是同一版本。根据构建配置,“lib”和“bin”包含“x64”和“x86”分区。
必须在项目设置中指定 FFmpeg
版本和路径,用于编译器包含路径配置(Properties\C/C++\General\Additional Include directories)、链接器库搜索路径配置(Properties\Linker\General\Additional Library directories)和构建后库复制事件(Properties\Build Events\Post-Build Event)。
如果存在源文件,则可以在项目设置或预编译头文件中设置“HAVE_FFMPEG_SOURCES
”定义。
如果一切设置正确,那么构建项目将成功。
许可和分发
FFmpeg
库的 FFmpeg.NET 包装(以下简称“包装库”)按“原样”提供,不提供任何形式的保证;甚至不提供适销性或特定用途适用性的默示保证。
包装库是源代码可用的软件。它可以在非商业软件和开源项目中免费使用。
使用条款在项目附带的许可证文本文件中描述。
历史
- 2022 年 7 月 15 日:初始版本