<tbody id="86a2i"></tbody>


<dd id="86a2i"></dd>
<progress id="86a2i"><track id="86a2i"></track></progress>

<dd id="86a2i"></dd>
<em id="86a2i"><ruby id="86a2i"><u id="86a2i"></u></ruby></em>

    <dd id="86a2i"></dd>
    sdflysha

    我做的FFmpeg開源C#封裝庫Sdcb.FFmpeg main

    寫在前面:

    該主題為2022年12月份.NET Conf China 2022我的主題,項目地址:https://github.com/sdcb/Sdcb.FFmpeg

    對應的PPT可以從這下載:https://io.starworks.cc:88/cv-public/2022/.NET玩轉音視頻操作FFmpeg.pptx

    對應的視頻可以從這里觀看(從3:19:00開始):https://bbs.csdn.net/topics/609897502

    FFmpeg是知名的音頻視頻處理軟件,我平時工作生活中會經常用到。但同時我也是.NET程序員,在嘗試性的用C#調用FFmpeg時,有以下這些選擇:

    • 進程外調用,比如:
      • FFmpeg.NET
      • MediaToolkit
      • Xabe.Ffmpeg
    • 基于C API平臺調用,比如:
      • FFmpeg.AutoGen
      • EmguFFmpeg
      • Sdcb.FFmpeg

    如果基于命令行的話,有以下優缺點:

    • 優點:容易學習、入門方便、不與GPL開源協議沖突
    • 基于進程互操作,依賴于標準流重定向管理狀態
    • 輸入和輸出依賴于文件,很難精細控制

    如果是基于C API做平臺調用,則可以很好解決上面一些問題,有如下優缺點:

    • 輸入和輸出可基于內存,可精細控制每一幀
    • 性能方面減少了跨進程的損耗,更能有保障
    • 缺點:C API代碼比較復雜
    • 缺點:業界普遍使用FFmpeg.AutoGen,在C#的基礎上糅合C指針,寫起來甚至比C API更復雜

    我做了什么?

    受制于以上這些困難,我以業界普遍使用的開源項目FFmpeg.AutoGen為基礎,我我自己動手做了一個Sdcb.FFmpeg,它有如下優點:

    • 保留所有直接調用C API的能力、保留跨平臺的能力
    • 刪掉并完全重寫了ClangMacroParser依賴,因此比原版支持更多的宏解析
    • 動態庫加載方式從手動LoadLibrary改為了自動的[DllImport],這在.NET Core中可以自動從NuGet包中加載dll,這更符合.NET社區共識
    • 刪掉了倉庫所有大二進制依賴和大二進制歷史,改成自動從網上下載,這縮小了倉庫體積
    • 簡化了枚舉名字,如AVCodecID.AV_CODEC_ID_H264 -> AVCodecID.H264
    • 為許多C宏改造成了C#枚舉,如ffmpeg.AV_DICT_MATCH_CASE -> AV_DICT_READ.MatchCase
    • 除了底層封裝,還提供了中層(類)封裝和高層(幫助類)封裝,比如CodecContextMediaDictionary
    • 我制作了動態鏈接庫的NuGet包,這可以保障程序不需要安裝外部依賴直接就能運行

    NuGet包列表

    • FFmpeg 5.x:

      Package Link
      Sdcb.FFmpeg NuGet
      Sdcb.FFmpeg.runtime.windows-x64 NuGet
    • FFmpeg 4.4.x:

      Package Link
      Sdcb.FFmpeg NuGet
      Sdcb.FFmpeg.runtime.windows-x64 NuGet

    Linux/MacOS下如何使用?

    Linux下你并不需要這些NuGet包,Linux的發行版本很多,這些發行版大都內置了FFmpeg這樣非常常見的庫,比如在Ubuntu 22.04中,就可以通過如下命令來安裝FFmpeg 5.x的動態鏈接庫:

    apt update
    apt install software-properties-common
    add-apt-repository ppa:savoury1/ffmpeg4 -y
    add-apt-repository ppa:savoury1/ffmpeg5 -y
    apt update
    apt install ffmpeg -y
    

    如果是FFmpeg 4.x,則可以通過以下命令來安裝動態鏈接庫:

    apt update
    apt install software-properties-common
    add-apt-repository ppa:savoury1/ffmpeg4 -y
    apt update
    apt install ffmpeg -y
    

    如果是MacOS,則可以通過以下命令來安裝動態鏈接庫:

    brew install ffmpeg
    

    NuGet包一般會和libc相關的庫綁定,沒有很好的泛用性,而且一般Linux中有更好的解決方案,因此我沒有為Linux制作運行時NuGet包。

    但不要理解錯了,Sdcb.FFmpegLinux中也是經過測試的,也運行得很好,Github Actions測試鏈接:https://github.com/sdcb/Sdcb.FFmpeg/actions

    為什么我要另起爐灶?

    其實我并不是一上來就準備另起爐灶,一開始我受到北京大佬于宏偉這個EmguFFmpeg項目的啟發,覺得FFmpeg.AutoGen確實很難用,但只要依賴于FFmpeg.AutoGen,稍做點封裝,就能減少許多維護工作,為此我于2020~2021年一直在想辦法開發和維護這個開源項目:Sdcb.FFmpegAPIWrapper,這個項目是完全基于Sdcb.FFmpeg開發的,當時這個項目也已經基本完成(就是沒怎么做宣傳、示例和教程)。

    然而隨著項目的深入,我越來越覺得直接依賴于FFmpeg.AutoGen會導致代碼過于“笨重”,比如同一套東西,原始的和“高級”的有兩種不同的寫法(比如同時存在AVCodecID.AV_CODEC_ID_H264AVCodecID.H264,用戶大概率會迷失,因此經過了許久的迷茫期后我終于下定決心改造FFmpeg.AutoGen,整個改造的過程伴隨了大約一年的時間,最后就造就了今天的狀態。

    6個示例演示Sdcb.FFmpeg

    示例1 純代碼生成視頻

    可以理解這個示例是FFmpeg的“Hello World”,需要引用如下NuGet包:

    • Sdcb.FFmpeg 5.1.2
    • Sdcb.FFmpeg.runtime.windows-x64

    需要引用以下名字空間:

    • Sdcb.FFmpeg.Codecs
    • Sdcb.FFmpeg.Formats
    • Sdcb.FFmpeg.Raw
    • Sdcb.FFmpeg.Toolboxs.Extensions
    • Sdcb.FFmpeg.Toolboxs.Generators
    • Sdcb.FFmpeg.Utils

    完整代碼如下(點擊展開):

    // this example is based on Sdcb.FFmpeg 5.1.2
    FFmpegLogger.LogWriter = (level, msg) => Console.Write(msg);
    
    using FormatContext fc = FormatContext.AllocOutput(formatName: "mp4");
    fc.VideoCodec = Codec.CommonEncoders.Libx264;
    MediaStream vstream = fc.NewStream(fc.VideoCodec);
    using CodecContext vcodec = new CodecContext(fc.VideoCodec)
    {
        Width = 800,
        Height = 600,
        TimeBase = new AVRational(1, 30),
        PixelFormat = AVPixelFormat.Yuv420p,
        Flags = AV_CODEC_FLAG.GlobalHeader,
    };
    vcodec.Open(fc.VideoCodec);
    vstream.Codecpar!.CopyFrom(vcodec);
    vstream.TimeBase = vcodec.TimeBase;
    
    string outputPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "muxing.mp4");
    fc.DumpFormat(streamIndex: 0, outputPath, isOutput: true);
    
    using IOContext io = IOContext.OpenWrite(outputPath);
    fc.Pb = io;
    fc.WriteHeader();
    VideoFrameGenerator.Yuv420pSequence(vcodec.Width, vcodec.Height, 600)
    	.ConvertFrames(vcodec)
    	.EncodeAllFrames(fc, null, vcodec)
    	.WriteAll(fc);
    fc.WriteTrailer();
    

    運行后應該可以在桌面上看到一個muxing.mp4的文件,這個文件就是通過上述代碼生成的,這個視頻效果如下圖所示:

    值得一提的是,我寫了VideoFrameGenerator.Yuv420pSequence,它輸入了少量參數,返回了IEnumerable<Frame>(或者在其它示例中IEnumerable<Packet>),這是我項目里面非常常見的寫法,這樣既體現了C#語言簡明強大的魅力,又其實保障了資源管理和內存釋放。

    示例2 壓制視頻

    這個示例將展示如何將一個視頻壓制成如下參數,這些參數也是微信Windows桌面端視頻不受二壓的參數:

    • 編碼:H264
    • 視頻碼率:600kbps以下
    • 視頻分辨率:未限制,但推薦長邊960
    • 音頻編碼:AAC
    • 音頻碼率:48kbps

    需要引用如下NuGet包:

    • Sdcb.FFmpeg 5.1.2
    • Sdcb.FFmpeg.runtime.windows-x64

    需要引用如下名字空間:

    • Sdcb.FFmpeg.Codecs
    • Sdcb.FFmpeg.Common
    • Sdcb.FFmpeg.Filters
    • Sdcb.FFmpeg.Formats
    • Sdcb.FFmpeg.Raw
    • Sdcb.FFmpeg.Toolboxs
    • Sdcb.FFmpeg.Toolboxs.Extensions
    • Sdcb.FFmpeg.Toolboxs.FilterTools
    • Sdcb.FFmpeg.Toolboxs.Generators
    • Sdcb.FFmpeg.Utils
    • static Sdcb.FFmpeg.Raw.ffmpeg
    • System.Collections.Concurrent
    • System.Runtime.CompilerServices
    • System.Threading.Tasks

    完整代碼如下(點擊展開):

    void Main()
    {
    	FFmpegLogger.LogLevel = LogLevel.Error;
    	FFmpegLogger.LogWriter = (level, msg) => Console.Write(msg);
    
    	Task.Run(() => A7r3VideoToWechat(@"Y:\a7r3\2022-12-12\C0060.MP4")).Wait();
    }
    
    void A7r3VideoToWechat(string mp4Path)
    {
    	using FormatContext inFc = FormatContext.OpenInputUrl(mp4Path);
    	inFc.LoadStreamInfo();
    
    	// prepare input stream/codec
    	MediaStream inAudioStream = inFc.GetAudioStream();
    	using CodecContext audioDecoder = new(Codec.FindDecoderById(inAudioStream.Codecpar!.CodecId));
    	audioDecoder.FillParameters(inAudioStream.Codecpar);
    	audioDecoder.Open();
    	audioDecoder.ChannelLayout = (ulong)ffmpeg.av_get_default_channel_layout(audioDecoder.Channels);
    
    	MediaStream inVideoStream = inFc.GetVideoStream();
    	using CodecContext videoDecoder = new(Codec.FindDecoderByName("h264_cuvid"));
    	videoDecoder.FillParameters(inVideoStream.Codecpar!);
    	videoDecoder.Open();
    
    	// dest file
    	string destFile = Path.Combine(Path.GetDirectoryName(mp4Path)!, Path.GetFileNameWithoutExtension(mp4Path) + "_wechat.mp4");
    	using FormatContext outFc = FormatContext.AllocOutput(fileName: destFile);
    
    	// dest encoder and streams
    	outFc.AudioCodec = Codec.CommonEncoders.AAC;
    	MediaStream outAudioStream = outFc.NewStream(outFc.AudioCodec);
    	using CodecContext audioEncoder = new(outFc.AudioCodec)
    	{
    		Channels = 1,
    		SampleFormat = outFc.AudioCodec.Value.NegociateSampleFormat(AVSampleFormat.Fltp),
    		SampleRate = outFc.AudioCodec.Value.NegociateSampleRates(48000),
    		BitRate = 48000
    	};
    	audioEncoder.ChannelLayout = (ulong)ffmpeg.av_get_default_channel_layout(audioEncoder.Channels);
    	audioEncoder.TimeBase = new AVRational(1, audioEncoder.SampleRate);
    	audioEncoder.Open(outFc.AudioCodec);
    	outAudioStream.Codecpar!.CopyFrom(audioEncoder);
    
    	outFc.VideoCodec = Codec.FindEncoderByName("libx264");
    	MediaStream outVideoStream = outFc.NewStream(outFc.VideoCodec);
    	using VideoFilterContext vfilter = VideoFilterContext.Create(inVideoStream, "scale=1920:-1");
    	using CodecContext videoEncoder = new(outFc.VideoCodec)
    	{
    		Flags = AV_CODEC_FLAG.GlobalHeader,
    		ThreadCount = Environment.ProcessorCount, 
    		ThreadType = ffmpeg.FF_THREAD_FRAME,
    		BitRate = 595_000
    	};
    	vfilter.ConfigureEncoder(videoEncoder);
    	var dict = new MediaDictionary
    	{
    		//["qp"] = "30",
    		["tune"] = "zerolatency",
    		["preset"] = "veryfast"
    	};
    	videoEncoder.Open(outFc.VideoCodec, dict);
    	//dict.Dump();
    	outVideoStream.Codecpar!.CopyFrom(videoEncoder);
    	outVideoStream.TimeBase = videoEncoder.TimeBase;
    
    	// begin write
    	using IOContext io = IOContext.OpenWrite(destFile);
    	outFc.Pb = io;
    	outFc.WriteHeader();
    
    	MediaThreadQueue<Frame> decodingQueue = inFc
    		.ReadPackets(inVideoStream.Index, inAudioStream.Index)
    		.DecodeAllPackets(inFc, audioDecoder, videoDecoder)
    		.ToThreadQueue(cancellationToken: QueryCancelToken, boundedCapacity: 64);
    
    	MediaThreadQueue<Packet> encodingQueue = decodingQueue.GetConsumingEnumerable()
    		.ApplyVideoFilters(vfilter)
    		.ConvertAllFrames(audioEncoder, videoEncoder)
    		.AudioFifo(audioEncoder)
    		.EncodeAllFrames(outFc, audioEncoder, videoEncoder)
    		.ToThreadQueue(cancellationToken: QueryCancelToken);
    
    	CancellationTokenSource end = new();
    	QueryCancelToken.Register(() => end.Cancel());
    	Dictionary<int, PtsDts> ptsDts = new();
    	Task.Run(async () =>
    	{
    		double totalDuration = Math.Max(inVideoStream.GetDurationInSeconds(), inAudioStream.GetDurationInSeconds());
    		try
    		{
    			while (!end.IsCancellationRequested)
    			{
    				Log();
    				await Task.Delay(1000, end.Token);
    			}
    		}
    		finally
    		{
    			Log();
    		}
    
    		void Log() => Console.WriteLine($"{GetStatusText()}, dec/enc queue: {decodingQueue.Count}/{encodingQueue.Count}");
    		string GetStatusText() => $"{(outVideoStream.TimeBase * ptsDts.GetValueOrDefault(outVideoStream.Index, PtsDts.Default).Dts).ToDouble():F2} of {totalDuration:F2}";
    	});
    	encodingQueue.GetConsumingEnumerable()
    		.RecordPtsDts(ptsDts)
    		.WriteAll(outFc);
    	end.Cancel();
    	outFc.WriteTrailer();
    }
    

    運行效果如圖(將500多MB壓縮為5MB):

    值得一提的是這里的MediaThreadQueue<Frame>MediaThreadQueue<Packet>,內部都是基于C#BlockingCollection加多線程做的,這樣可能提高效率,保證性能。

    示例3 創建gif(表情包?)

    注意,我創建了一個demo網站可以用于演示該功能,可以點擊“生成”按鈕,比如可以得到這樣的表情包:

    我把所有有完整Visual Studio代碼示例上傳到了Github,可以在這下載:https://github.com/sdcb/ffmpeg-wjz-sorry-generator

    它有如下步驟和要點:

    1. 視頻解碼
    2. 將每一幀轉換為BGRA像素格式
    3. 使用Direct2D讀取并繪制字幕
    4. 將每一幀輸入視頻過濾器,轉換為PAL8格式
    5. 將PAL8編碼像素格式的幀編碼為gif

    注意這個demo我用到了Direct2D,它基于這個開源項目做的:Vortice.Windows

    示例4 實際桌面投屏(遠程桌面?)

    這個可以實現將一臺電腦的屏幕內容,以較低的網絡開銷,通過網絡實時地傳輸到另一臺電腦,它的使用場景包含實時視頻通話、遠程投屏、遠程桌面控制等。

    代碼分為兩部分,桌面錄制-編碼-發送端遠程接收-解碼-顯示端。

    桌面錄制-編碼-發送端完整源代碼

    需要引用NuGet包:

    • Sdcb.FFmpeg 4.4.3
    • Sdcb.FFmpeg.runtime.windows-x64 4.4.3
    • Sdcb.ScreenCapture

    完整源代碼如下(點擊展開):

    // This example was initially written based on Sdcb.FFmpeg 4.4.3 & Sdcb.ScreenCapture
    void Main()
    {
    	StartService(QueryCancelToken);
    }
    
    void StartService(CancellationToken cancellationToken = default)
    {
    	var tcpListener = new TcpListener(IPAddress.Any, 5555);
    	cancellationToken.Register(() => tcpListener.Stop());
    	tcpListener.Start();
    
    	while (!cancellationToken.IsCancellationRequested)
    	{
    		TcpClient client = tcpListener.AcceptTcpClient();
    		Task.Run(() => ServeClient(client, cancellationToken));
    	}
    }
    
    void ServeClient(TcpClient tcpClient, CancellationToken cancellationToken = default)
    {
    	try
    	{
    		using var _ = tcpClient;
    		using NetworkStream stream = tcpClient.GetStream();
    		using BinaryWriter writer = new(stream);
    		RectI screenSize = ScreenCapture.GetScreenSize(screenId: 0);
    		RdpCodecParameter rcp = new(AVCodecID.H264, screenSize.Width, screenSize.Height, AVPixelFormat.Bgr0);
    
    		using CodecContext cc = new(Codec.CommonEncoders.Libx264RGB)
    		{
    			Width = rcp.Width,
    			Height = rcp.Height,
    			PixelFormat = rcp.PixelFormat,
    			TimeBase = new AVRational(1, 20),
    		};
    		cc.Open(null, new MediaDictionary
    		{
    			["crf"] = "30",
    			["tune"] = "zerolatency",
    			["preset"] = "veryfast"
    		});
    
    		writer.Write(rcp.ToArray());
    		using Frame source = new();
    		foreach (Packet packet in ScreenCapture
    			.CaptureScreenFrames(screenId: 0)
    			.ToBgraFrame()
    			.ConvertFrames(cc)
    			.EncodeFrames(cc))
    		{
    			if (cancellationToken.IsCancellationRequested)
    			{
    				break;
    			}
    			writer.Write(packet.Data.Length);
    			writer.Write(packet.Data.AsSpan());
    		}
    	}
    	catch (IOException ex)
    	{
    		// Unable to write data to the transport connection: 遠程主機強迫關閉了一個現有的連接。.
    		// Unable to write data to the transport connection: 你的主機中的軟件中止了一個已建立的連接。
    		ex.Dump();
    	}
    }
    
    public class Filo<T> : IDisposable
    {
    	private T? Item { get; set; }
    	private ManualResetEventSlim Notify { get; } = new ManualResetEventSlim();
    
    	public void Update(T item)
    	{
    		Item = item;
    		Notify.Set();
    	}
    
    	public IEnumerable<T> Consume(CancellationToken cancellationToken = default)
    	{
    		while (!cancellationToken.IsCancellationRequested)
    		{
    			Notify.Wait(cancellationToken);
    			yield return Item!;
    		}
    	}
    
    	public void Dispose() => Notify.Dispose();
    }
    
    public static class BgraFrameExtensions
    {
    	public static IEnumerable<Frame> ToBgraFrame(this IEnumerable<LockedBgraFrame> bgras)
    	{
    		using Frame frame = new Frame();
    		foreach (LockedBgraFrame bgra in bgras)
    		{
    			frame.Width = bgra.Width;
    			frame.Height = bgra.Height;
    			frame.Format = (int)AVPixelFormat.Bgra;
    			frame.Data[0] = bgra.DataPointer;
    			frame.Linesize[0] = bgra.RowPitch;
    			yield return frame;
    		}
    	}
    }
    
    record RdpCodecParameter(AVCodecID CodecId, int Width, int Height, AVPixelFormat PixelFormat)
    {
    	public byte[] ToArray()
    	{
    		byte[] data = new byte[16];
    		Span<byte> span = data.AsSpan();
    		BinaryPrimitives.WriteInt32LittleEndian(span, (int)CodecId);
    		BinaryPrimitives.WriteInt32LittleEndian(span[4..], Width);
    		BinaryPrimitives.WriteInt32LittleEndian(span[8..], Height);
    		BinaryPrimitives.WriteInt32LittleEndian(span[12..], (int)PixelFormat);
    		return data;
    	}
    }
    

    值得一提的是Sdcb.ScreenCapture這個NuGet包也是我做的,它是基于DXGI的技術,錄屏時能做到內存0復制,可以實現每秒60幀錄屏且CPU占用率很低。這里挖個坑以后有機會介紹這個開源項目,Github地址如下:https://github.com/sdcb/Sdcb.ScreenCapture

    遠程接收-解碼-顯示端完整源代碼

    需要引用的NuGet包:

    • Sdcb.FFmpeg 4.4.3
    • Sdcb.FFmpeg.runtime.windows-x64 4.4.3
    • FlysEngine.Desktop

    點擊展開顯示:

    // This example was initially written based on Sdcb.FFmpeg 4.4.3 & FlysEngine.Desktop
    #nullable enable
    
    ManagedBgraFrame? managedFrame = null;
    bool cancel = false;
    
    unsafe void Main()
    {
    	using RenderWindow w = new();
    	w.FormClosed += delegate { cancel = true; };
    	Task decodingTask = Task.Run(() => DecodeThread(() => (3840, 2160)));
    
    	w.Draw += (_, ctx) =>
    	{
    		ctx.Clear(Colors.CornflowerBlue);
    		if (managedFrame == null) return;
    
    		ManagedBgraFrame frame = managedFrame.Value;
    
    		fixed (byte* ptr = frame.Data)
    		{
    			//new System.Drawing.Bitmap(frame.Width, frame.Height, frame.RowPitch, System.Drawing.Imaging.PixelFormat.Format32bppPArgb, (IntPtr)ptr).DumpUnscaled();
    			BitmapProperties1 props = new(new PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied));
    			using ID2D1Bitmap bmp = ctx.CreateBitmap(new SizeI(frame.Width, frame.Height), (IntPtr)ptr, frame.RowPitch, props);
    			ctx.UnitMode = UnitMode.Dips;
    			ctx.DrawBitmap(bmp, 1.0f, InterpolationMode.NearestNeighbor);
    		}
    	};
    	RenderLoop.Run(w, () => w.Render(1, Vortice.DXGI.PresentFlags.None));
    }
    
    async Task DecodeThread(Func<(int width, int height)> sizeAccessor)
    {
    	using TcpClient client = new TcpClient();
    	await client.ConnectAsync(IPAddress.Loopback, 5555);
    	using NetworkStream stream = client.GetStream();
    
    	using BinaryReader reader = new(stream);
    	RdpCodecParameter rcp = RdpCodecParameter.FromSpan(reader.ReadBytes(16));
    
    	using CodecContext cc = new(Codec.FindDecoderById(rcp.CodecId))
    	{
    		Width = rcp.Width,
    		Height = rcp.Height,
    		PixelFormat = rcp.PixelFormat,
    	};
    	cc.Open(null);
    
    	foreach (var frame in reader
    		.ReadPackets()
    		.DecodePackets(cc)
    		.ConvertVideoFrames(sizeAccessor, AVPixelFormat.Bgra)
    		.ToManaged()
    		)
    	{
    		if (cancel) break;
    		managedFrame = frame;
    	}
    }
    
    
    public static class FramesExtensions
    {
    	public static IEnumerable<ManagedBgraFrame> ToManaged(this IEnumerable<Frame> bgraFrames, bool unref = true)
    	{
    		foreach (Frame frame in bgraFrames)
    		{
    			int rowPitch = frame.Linesize[0];
    			int length = rowPitch * frame.Height;
    			byte[] buffer = new byte[length];
    			Marshal.Copy(frame.Data._0, buffer, 0, length);
    			ManagedBgraFrame managed = new(buffer, length, length / frame.Height);
    			if (unref) frame.Unref();
    			yield return managed;
    		}
    	}
    }
    
    public record struct ManagedBgraFrame(byte[] Data, int Length, int RowPitch)
    {
    	public int Width => RowPitch / BytePerPixel;
    	public int Height => Length / RowPitch;
    
    	public const int BytePerPixel = 4;
    }
    
    
    public static class ReadPacketExtensions
    {
    	public static IEnumerable<Packet> ReadPackets(this BinaryReader reader)
    	{
    		using Packet packet = new();
    		while (true)
    		{
    			int packetSize = reader.ReadInt32();
    			if (packetSize == 0) yield break;
    
    			byte[] data = reader.ReadBytes(packetSize);
    			GCHandle dataHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
    			try
    			{
    				packet.Data = new DataPointer(dataHandle.AddrOfPinnedObject(), packetSize);
    				yield return packet;
    			}
    			finally
    			{
    				dataHandle.Free();
    			}
    		}
    	}
    }
    
    record RdpCodecParameter(AVCodecID CodecId, int Width, int Height, AVPixelFormat PixelFormat)
    {
    	public static RdpCodecParameter FromSpan(ReadOnlySpan<byte> data)
    	{
    		return new RdpCodecParameter(
    			CodecId: (AVCodecID)BinaryPrimitives.ReadInt32LittleEndian(data),
    			Width: BinaryPrimitives.ReadInt32LittleEndian(data[4..]),
    			Height: BinaryPrimitives.ReadInt32LittleEndian(data[8..]),
    			PixelFormat: (AVPixelFormat)BinaryPrimitives.ReadInt32LittleEndian(data[12..]));
    	}
    }
    

    兩者運行效果如圖:

    可見傳輸延遲在0.28秒的樣子,這是通過libx264編碼通過yuv420p傳輸的我4k顯示器視頻,可見可以滿足實際網絡會議演示、投屏直播、遠程控制方面的需求(如果是1080p延遲應該可以更低)。

    注意該源代碼用上了我自己寫的開源Direct2D封裝引擎:FlysEngine,你不需要關注它的細節(只需要安裝NuGet包即可),但如果你碰巧關注,這里又挖個坑看以后有機會介紹介紹,在這之前只需要知道的是它只對D3D11、DXGI、Direct2D、WIC、DirectWrite做了一層薄薄的封裝。

    示例5 接收顯示RTSP攝像頭視頻

    這個程序依賴于如下NuGet包:

    • FlysEngine.Desktop
    • Sdcb.FFmpeg 4.4.3
    • Sdcb.FFmpeg.runtime.windows-x64 4.4.3

    完整代碼(點擊展開):

    #nullable enable
    
    FFmpegBmp? ffBmp = null;
    FFmpegBmp? lastFFbmp = null;
    FFmpegLogger.LogWriter = (level, msg) => Console.Write(msg);
    CancellationTokenSource cts = new();
    
    using RenderWindow w = new();
    Task.Run(() => DecodeRTSP(Util.GetPassword("home-rtsp-ipc"), cts.Token));
    w.Draw += (_, ctx) =>
    {
    	if (ffBmp == null) return;
    	if (lastFFbmp == ffBmp) return;
    
    	GCHandle handle = GCHandle.Alloc(ffBmp.Data, GCHandleType.Pinned);
    	try
    	{
    		using ID2D1Bitmap bmp = ctx.CreateBitmap(new SizeI(ffBmp.Width, ffBmp.Height), handle.AddrOfPinnedObject(), ffBmp.RowPitch, new BitmapProperties(new Vortice.DCommon.PixelFormat(Format.B8G8R8A8_UNorm, Vortice.DCommon.AlphaMode.Premultiplied)));
    		lastFFbmp = ffBmp;
    		Size clientSize = ctx.Size;
    		float top = (clientSize.Height - ffBmp.Height) / 2;
    		ctx.Transform = Matrix3x2.CreateTranslation(0, top);
    		ctx.DrawBitmap(bmp, 1.0f, InterpolationMode.Linear);
    	}
    	finally
    	{
    		handle.Free();
    	}
    };
    w.FormClosing += delegate { cts.Cancel(); };
    RenderLoop.Run(w, () => w.Render(1, Vortice.DXGI.PresentFlags.None));
    
    void DecodeRTSP(string url, CancellationToken cancellationToken = default)
    {
    	using FormatContext fc = FormatContext.OpenInputUrl(url);
    	fc.LoadStreamInfo();
    	MediaStream videoStream = fc.GetVideoStream();
    
    	using CodecContext videoDecoder = new CodecContext(Codec.FindDecoderByName("hevc_qsv"));
    	videoDecoder.FillParameters(videoStream.Codecpar!);
    	videoDecoder.Open();
    
    	foreach (Frame frame in fc
    		.ReadPackets(videoStream.Index)
    		.DecodePackets(videoDecoder)
    		.ConvertVideoFrames(() => new(w.ClientSize.Width, w.ClientSize.Width * videoDecoder.Height / videoDecoder.Width), AVPixelFormat.Bgr0))
    	{
    		if (cancellationToken.IsCancellationRequested) break;
    
    		try
    		{
    			byte[] data = new byte[frame.Linesize[0] * frame.Height];
    			Marshal.Copy(frame.Data._0, data, 0, data.Length);
    			ffBmp = new FFmpegBmp(frame.Width, frame.Height, frame.Linesize[0], data);
    		}
    		finally
    		{
    			frame.Unref();
    		}
    	}
    }
    
    public record FFmpegBmp(int Width, int Height, int RowPitch, byte[] Data);
    

    我農村老家的攝像頭使用的是RTSP攝像頭,這是使用上述代碼的運行效果:

    示例6 讀RTSP流并保存為mp4/mov文件

    這個示例依賴于以下NuGet包:

    • Sdcb.FFmpeg 4.4.3
    • Sdcb.FFmpeg.runtime.windows-x64 4.4.3

    完整代碼示例(請點擊展開):

    // The example was initially written using Sdcb.FFmpeg 4.4.3
    FFmpegLogger.LogWriter = (level, msg) => Console.Write(msg);
    
    using FormatContext inFc = FormatContext.OpenInputUrl(Util.GetPassword("home-rtsp-ipc"));
    inFc.LoadStreamInfo();
    MediaStream inAudioStream = inFc.GetAudioStream();
    MediaStream inVideoStream = inFc.GetVideoStream();
    long gpts_v = 0, gpts_a = 0, gdts_v = 0, gdts_a = 0;
    
    while (!QueryCancelToken.IsCancellationRequested)
    {
    	using FormatContext outFc = FormatContext.AllocOutput(formatName: "mov");
    	string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "rtsp", DateTime.Now.ToString("yyyy-MM-dd"));
    	Directory.CreateDirectory(dir);
    	using IOContext io = IOContext.OpenWrite(Path.Combine(dir, $"{DateTime.Now:HHmmss}.mov"));
    	outFc.Pb = io;
    
    	MediaStream videoStream = outFc.NewStream(Codec.FindEncoderById(inVideoStream.Codecpar!.CodecId));
    	videoStream.Codecpar!.CopyFrom(inVideoStream.Codecpar);
    	videoStream.TimeBase = inVideoStream.RFrameRate.Inverse();
    	videoStream.SampleAspectRatio = inVideoStream.SampleAspectRatio;
    
    	MediaStream audioStream = outFc.NewStream(Codec.FindEncoderById(inAudioStream.Codecpar!.CodecId));
    	audioStream.Codecpar!.CopyFrom(inAudioStream.Codecpar);
    	audioStream.TimeBase = inAudioStream.TimeBase;
    	audioStream.Codecpar.ChannelLayout = (ulong)ffmpeg.av_get_default_channel_layout(inAudioStream.Codecpar.Channels);
    
    	outFc.WriteHeader();
    	
    	FilterPackets(inFc.ReadPackets(inAudioStream.Index, inVideoStream.Index), videoFrameCount: 60 * 20)
    		.WriteAll(outFc);
    	outFc.WriteTrailer();
    
    	IEnumerable<Packet> FilterPackets(IEnumerable<Packet> packets, int videoFrameCount)
    	{
    		long pts_v = gpts_v, pts_a = gpts_a, dts_v = gdts_v, dts_a = gdts_a;
    		long[] buffer = new long[200];
    		long ithreshold = -1;
    		int videoFrame = 0;
    
    		foreach (Packet pkt in packets)
    		{
    			pkt.StreamIndex = pkt.StreamIndex == inAudioStream.Index ?
    					audioStream.Index :
    					videoStream.Index;
    			if (pkt.StreamIndex == inAudioStream.Index)
    			{
    				// audio
    				(gpts_a, gdts_a, pkt.Pts, pkt.Dts) = (pkt.Pts, pkt.Dts, pkt.Pts - pts_a, pkt.Dts - dts_a);
    				pkt.RescaleTimestamp(inAudioStream.TimeBase, audioStream.TimeBase);
    			}
    			else
    			{
    				// video
    				if (videoFrame < buffer.Length)
    				{
    					buffer[videoFrame] = pkt.Data.Length;
    					ithreshold = -1;
    				}
    				else if (videoFrame == buffer.Length)
    				{
    					ithreshold = buffer.Order().ToArray()[buffer.Length / 2] * 4;
    				}
    				
    				if (videoFrame >= videoFrameCount && pkt.Data.Length > ithreshold)
    				{
    					break;
    				}
    
    				(gpts_v, gdts_v, pkt.Pts, pkt.Dts) = (pkt.Pts, pkt.Dts, pkt.Pts - pts_v, pkt.Dts - dts_v);
    				pkt.RescaleTimestamp(inVideoStream.TimeBase, videoStream.TimeBase);
    				videoFrame++;
    			}
    			yield return pkt;
    		}
    	}
    }
    

    這個程序可以全天候運行,運行后RTSP攝像頭錄的完整視頻和音頻,大約每1.5分鐘對應一個視頻文件,都會保存到桌面的這個文件夾中(如圖):

    這樣的話也許就有機會取代錄機了~

    總結與展望

    我認為把東西做出來和把東西做好是有區別的,以前在C#里面東西也就是“能用”的狀態,這和許多node.js或者python那樣的極客玩家有本質區別,希望通過這樣一個開源項目能向“.NET作為第一等公民”方向努力。

    維護開源不易,喜歡的朋友請點個贊,賞個star:https://github.com/sdcb/Sdcb.FFmpeg

    我也想能給自己立個flag,希望未來我可以封裝FlyCV、libyuv、x264基于libaom-av1,甚至也許有一點有機會做一個.NET版本的FFmpeg。

    喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】

    DotNet騷操作

    相關文章:

    免费一级a片在线播放视频|亚洲娇小性XXXX色|曰本无码毛片道毛片视频清|亚洲一级a片视频免费观看
    <tbody id="86a2i"></tbody>

    
    
    <dd id="86a2i"></dd>
    <progress id="86a2i"><track id="86a2i"></track></progress>

    <dd id="86a2i"></dd>
    <em id="86a2i"><ruby id="86a2i"><u id="86a2i"></u></ruby></em>

      <dd id="86a2i"></dd>