The function is currently functioning properly.
However, using ConvertBGRtoRGB will cause the CPU usage to be excessively high.
using SIPSorcery.Net;
using SIPSorcery.Sys;
using SIPSorceryMedia.Abstractions;
using SIPSorceryMedia.FFmpeg;
using System.Net;
namespace Kvm.Control
{
/// <summary>
/// 解析WebRTC视频数据
/// </summary>
public class WebRtcHelper : IDisposable
{
private RTCPeerConnection pc;
public Action<RawImage> OnVideoSinkDecodedSampleFaster { get; set; }
public Action<byte[], uint, uint, int, VideoPixelFormatsEnum> OnVideoSinkDecodedSample { get; set; }
public Action<RTCIceCandidate> OnIceCandidateReceived { get; set; }
public Action<string> OnAnswerReceived { get; set; }
public Action OnDisConnected { get; set; }
public Action OnConnected { get; set; }
public bool IsConnected { get; private set; } = false;
private DateTime _lastRenderTime = DateTime.MinValue; // 上次渲染时间
private const int MinFrameIntervalMs = 33; // 限制最大30帧/秒 (1000/30 ≈ 33ms)
public WebRtcHelper()
{
var ffmpegLib = Path.Combine(AppContext.BaseDirectory, "ffmpeg", "bin");
SIPSorceryMedia.FFmpeg.FFmpegInit.Initialise(FfmpegLogLevelEnum.AV_LOG_VERBOSE, ffmpegLib);
}
public async Task CreatePeerConnection(string sdpOffer)
{
var videoEP = new FFmpegVideoEndPoint();
videoEP.RestrictFormats(format => format.Codec == VideoCodecsEnum.H264);
videoEP.OnVideoSinkDecodedSampleFaster += (RawImage rawImage) =>
{
var now = DateTime.UtcNow;
if ((now - _lastRenderTime).TotalMilliseconds < MinFrameIntervalMs)
return;
_lastRenderTime = now;
OnVideoSinkDecodedSampleFaster?.Invoke(rawImage);
};
RTCConfiguration config = new RTCConfiguration
{
X_BindAddress = IPAddress.Any,
};
pc = new RTCPeerConnection(config, portRange: new PortRange(20000, 20050));
pc.onicecandidate += (candidate) =>
{
if (candidate != null)
{
OnIceCandidateReceived?.Invoke(candidate);
}
};
MediaStreamTrack videoTrack = new MediaStreamTrack(videoEP.GetVideoSinkFormats(), MediaStreamStatusEnum.RecvOnly);
pc.addTrack(videoTrack);
pc.OnVideoFrameReceived += videoEP.GotVideoFrame;
pc.OnVideoFormatsNegotiated += (formats) => videoEP.SetVideoSinkFormat(formats.First());
// 监听连接状态
pc.onconnectionstatechange += async (state) =>
{
KvmLog.Information($"PeerConnection state: {state}");
if(state == RTCPeerConnectionState.connected)
{
IsConnected = true;
OnConnected?.Invoke();
}
else if (state == RTCPeerConnectionState.failed || state == RTCPeerConnectionState.closed)
{
IsConnected = false;
pc.Close("ice disconnection");
await videoEP.CloseVideo();
OnDisConnected?.Invoke();
}
};
// 设置Answer
pc.setRemoteDescription(new RTCSessionDescriptionInit { type = RTCSdpType.offer, sdp = sdpOffer });
var answer = pc.createAnswer(null);
await pc.setLocalDescription(answer);
OnAnswerReceived?.Invoke(answer.sdp);
}
public void Dispose()
{
pc?.Dispose();
}
}
}
using Serilog.Extensions.Logging;
using SIPSorceryMedia.Abstractions;
using System.Drawing.Imaging;
namespace Kvm.Control
{
internal class WebRTCMediaPlayer : KvmMediaPlayerBase
{
private JanusHelper janusHelper;
private WebRtcHelper webRtcHelper;
private PictureBox picBox;
private Bitmap _cachedBitmap; // 复用Bitmap对象,优化性能
public override bool Connected => janusHelper != null && janusHelper.IsConnected && webRtcHelper != null && webRtcHelper.IsConnected ? true : false;
public WebRTCMediaPlayer(AspectRatio ratio) : base(ratio)
{
if (KvmLog.Logger != null)
SIPSorcery.LogFactory.Set(new SerilogLoggerFactory(KvmLog.Logger));
janusHelper = new JanusHelper();
webRtcHelper = new WebRtcHelper();
picBox = new PictureBox();
}
public override async void Play(string ip, int port, System.Windows.Forms.Control control)
{
try
{
Url = $"ws://{ip}:{port}/janus/ws?auth_token={AuthToken}";
webRtcHelper.OnIceCandidateReceived = (candidate) =>
{
janusHelper.SendTrickleMessage(candidate);
};
webRtcHelper.OnAnswerReceived = (answerMessage) =>
{
janusHelper.SendAnswerMessage(answerMessage);
};
#region MyRegion
webRtcHelper.OnVideoSinkDecodedSampleFaster = async (rawImage) =>
{
if (control.IsHandleCreated)
{
try
{
#region Bitmap 渲染方式
control.BeginInvoke(() =>
{
if (!control.IsHandleCreated || control.Width <= 0 || control.Height <= 0 || !control.Visible)
{
return;
}
if (rawImage.PixelFormat == SIPSorceryMedia.Abstractions.VideoPixelFormatsEnum.Rgb)
{
Bitmap bmpImage = ConvertBGRtoRGB(rawImage);
//Bitmap bmpImage = new Bitmap(rawImage.Width, rawImage.Height, rawImage.Stride, PixelFormat.Format24bppRgb, rawImage.Sample);
if (bmpImage != null)
{
picBox.Image = bmpImage;
if (aspectRatio == AspectRatio.Fill)
{
picBox.SizeMode = PictureBoxSizeMode.StretchImage; // 拉伸填满
picBox.Dock = DockStyle.Fill;
}
else if (aspectRatio == AspectRatio.BestFit)
{
picBox.SizeMode = PictureBoxSizeMode.Zoom; // 等比例缩放,不超出控件边界
picBox.Dock = DockStyle.Fill;
}
}
}
});
#endregion
}
catch (Exception ex)
{
KvmLog.Error("WebRTC 视频渲染错误:" + ex.Message);
}
}
};
#endregion
webRtcHelper.OnConnected = () =>
{
OnConnected?.Invoke();
};
webRtcHelper.OnDisConnected = () =>
{
janusHelper.Dispose();
OnDisConnected?.Invoke();
};
janusHelper.OnConnected = async (sdpOffer) =>
{
await webRtcHelper.CreatePeerConnection(sdpOffer);
control.BeginInvoke(new Action(() =>
{
picBox.Enabled = false;
picBox.Dock = DockStyle.Fill;
control.Controls.Add(picBox);
control.SizeChanged += (s, e) =>
{
picBox.Size = control.Size;
picBox.Anchor = AnchorStyles.None;
int x = (picBox.Parent.ClientSize.Width - picBox.Width) / 2;
int y = (picBox.Parent.ClientSize.Height - picBox.Height) / 2;
picBox.Location = new Point(x, y);
UpdateAspectRatio();
};
}));
};
janusHelper.OnDisConnected = () =>
{
webRtcHelper.Dispose();
OnDisConnected?.Invoke();
};
await janusHelper.ConnectToJanus(Url);
}
catch (Exception ex)
{
Dispose();
throw new Exception($"kvm media play error: {ex.Message}");
}
}
private unsafe Bitmap ConvertBGRtoRGB(RawImage rawImage)
{
// 分辨率改变时重建Bitmap
if (_cachedBitmap == null || _cachedBitmap?.Width != rawImage.Width || _cachedBitmap?.Height != rawImage.Height)
{
_cachedBitmap?.Dispose();
_cachedBitmap = new Bitmap(rawImage.Width, rawImage.Height, PixelFormat.Format24bppRgb);
}
BitmapData bmpData = _cachedBitmap.LockBits(
new Rectangle(0, 0, rawImage.Width, rawImage.Height),
ImageLockMode.WriteOnly,
PixelFormat.Format24bppRgb);
try
{
byte* srcPtr = (byte*)rawImage.Sample;
byte* dstPtr = (byte*)bmpData.Scan0;
int width = rawImage.Width;
int height = rawImage.Height;
int srcStride = rawImage.Stride;
int dstStride = bmpData.Stride;
// 逐行交换 BGR -> RGB
for (int y = 0; y < height; y++)
{
byte* srcRow = srcPtr + y * srcStride;
byte* dstRow = dstPtr + y * dstStride;
for (int x = 0; x < width; x++)
{
int srcOff = x * 3;
int dstOff = x * 3;
dstRow[dstOff] = srcRow[srcOff + 2]; // B -> R
dstRow[dstOff + 1] = srcRow[srcOff + 1]; // G -> G
dstRow[dstOff + 2] = srcRow[srcOff]; // R -> B
}
}
}
finally
{
_cachedBitmap.UnlockBits(bmpData);
}
return _cachedBitmap;
}
public override void UpdateAspectRatio()
{
}
public override void Dispose()
{
_cachedBitmap?.Dispose();
janusHelper?.Dispose();
webRtcHelper?.Dispose();
base.Dispose();
}
}
}
The function is currently functioning properly.
However, using ConvertBGRtoRGB will cause the CPU usage to be excessively high.