493 lines
18 KiB
C#
493 lines
18 KiB
C#
using ManageLiteAV;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Media;
|
||
using System.Windows.Media.Imaging;
|
||
|
||
namespace JianGongYun.TRTC.Components
|
||
{
|
||
public class TXLiteAVVideoView : Canvas, IDisposable
|
||
{
|
||
private bool mOccupy = false; // view 是否已被占用
|
||
private bool mLocalView = false; // 是否为本地画面
|
||
private bool mPause = false;
|
||
private bool mFirstFrame = false;
|
||
|
||
private string mUserId;
|
||
private TRTCVideoStreamType mStreamType;
|
||
private TRTCVideoFillMode mRenderMode = TRTCVideoFillMode.TRTCVideoFillMode_Fit; // 0:填充,1:适应
|
||
|
||
private volatile FrameBufferInfo mArgbFrame = new FrameBufferInfo(); // 帧缓存
|
||
|
||
// 位图缓存,防止GC频繁
|
||
private WriteableBitmap mWriteableBitmap;
|
||
private Int32Rect mInt32Rect;
|
||
private Pen mPen;
|
||
|
||
public TXLiteAVVideoView()
|
||
{
|
||
mPen = new Pen { Brush = Brushes.DarkGray, Thickness = 1 };
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 View 绑定参数
|
||
/// </summary>
|
||
/// <param name="userId">需要渲染画面的 userId,如果是本地画面,则传空字符串。</param>
|
||
/// <param name="type">渲染类型</param>
|
||
/// <param name="engine">TRTCCloud 实例,用户注册视频数据回调。</param>
|
||
/// <param name="local">渲染本地画面,SDK 返回的 userId 为""</param>
|
||
/// <returns>true:绑定成功,false:绑定失败</returns>
|
||
public bool RegEngine(string userId, TRTCVideoStreamType type, ITRTCCloud engine, bool local = false)
|
||
{
|
||
if (mOccupy) return false;
|
||
mLocalView = local;
|
||
mUserId = userId;
|
||
mStreamType = type;
|
||
int count = TXLiteAVVideoViewManager.GetInstance().Count;
|
||
if (engine != null)
|
||
{
|
||
if (count == 0)
|
||
{
|
||
engine.setLocalVideoRenderCallback(TRTCVideoPixelFormat.TRTCVideoPixelFormat_BGRA32,
|
||
TRTCVideoBufferType.TRTCVideoBufferType_Buffer, TXLiteAVVideoViewManager.GetInstance());
|
||
}
|
||
if (!mLocalView)
|
||
{
|
||
engine.setRemoteVideoRenderCallback(userId, TRTCVideoPixelFormat.TRTCVideoPixelFormat_BGRA32,
|
||
TRTCVideoBufferType.TRTCVideoBufferType_Buffer, TXLiteAVVideoViewManager.GetInstance());
|
||
}
|
||
}
|
||
if (mLocalView)
|
||
TXLiteAVVideoViewManager.GetInstance().AddView("", type, this);
|
||
else
|
||
TXLiteAVVideoViewManager.GetInstance().AddView(userId, type, this);
|
||
lock (mArgbFrame)
|
||
ReleaseBuffer(mArgbFrame);
|
||
mOccupy = true;
|
||
this.InvalidateVisual();
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除 View 绑定参数
|
||
/// </summary>
|
||
/// <param name="engine">TRTCCloud 实例,用户注册视频数据回调。</param>
|
||
public void RemoveEngine(ITRTCCloud engine)
|
||
{
|
||
if (mLocalView)
|
||
TXLiteAVVideoViewManager.GetInstance().RemoveView("", mStreamType, this);
|
||
else
|
||
TXLiteAVVideoViewManager.GetInstance().RemoveView(mUserId, mStreamType, this);
|
||
int count = TXLiteAVVideoViewManager.GetInstance().Count;
|
||
if (engine != null)
|
||
{
|
||
if (count == 0)
|
||
{
|
||
engine.setLocalVideoRenderCallback(TRTCVideoPixelFormat.TRTCVideoPixelFormat_Unknown,
|
||
TRTCVideoBufferType.TRTCVideoBufferType_Unknown, null);
|
||
}
|
||
if (!mLocalView && !TXLiteAVVideoViewManager.GetInstance().HasUserId(mUserId))
|
||
{
|
||
engine.setRemoteVideoRenderCallback(mUserId, TRTCVideoPixelFormat.TRTCVideoPixelFormat_Unknown,
|
||
TRTCVideoBufferType.TRTCVideoBufferType_Unknown, null);
|
||
}
|
||
}
|
||
lock (mArgbFrame)
|
||
ReleaseBuffer(mArgbFrame);
|
||
mUserId = "";
|
||
mOccupy = false;
|
||
mLocalView = false;
|
||
mFirstFrame = false;
|
||
mRenderMode = TRTCVideoFillMode.TRTCVideoFillMode_Fit;
|
||
//this.OnViewRemove?.Invoke();
|
||
this.InvalidateVisual();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 View 的渲染模式
|
||
/// </summary>
|
||
/// <param name="mode">渲染模式</param>
|
||
public void SetRenderMode(TRTCVideoFillMode mode)
|
||
{
|
||
mRenderMode = mode;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断 View 是否被占用
|
||
/// </summary>
|
||
/// <returns>true:当前 View 已被占用,false:当前 View 未被占用</returns>
|
||
public bool IsViewOccupy()
|
||
{
|
||
return mOccupy;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 暂停渲染,显示默认图片
|
||
/// </summary>
|
||
/// <param name="pause">是否暂停</param>
|
||
public void SetPause(bool pause)
|
||
{
|
||
if (mPause != pause)
|
||
{
|
||
mPause = pause;
|
||
if (mPause)
|
||
{
|
||
this.Background = new SolidColorBrush(Color.FromArgb(0xFF, 0x20, 0x20, 0x20));
|
||
}
|
||
else
|
||
{
|
||
this.Background = new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0x00, 0x00));
|
||
// 避免刷新最后一帧数据
|
||
lock (mArgbFrame)
|
||
ReleaseBuffer(mArgbFrame);
|
||
}
|
||
this.InvalidateVisual();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除所有映射信息
|
||
/// </summary>
|
||
public static void RemoveAllRegEngine()
|
||
{
|
||
TXLiteAVVideoViewManager.GetInstance().RemoveAllView();
|
||
}
|
||
|
||
public bool AppendVideoFrame(byte[] data, int width, int height, TRTCVideoPixelFormat videoFormat, TRTCVideoRotation rotation)
|
||
{
|
||
if (!mFirstFrame)
|
||
mFirstFrame = true;
|
||
|
||
OnRenderVideoFrameHandler?.Invoke(data, width, height);
|
||
|
||
if (mPause)
|
||
return false;
|
||
if (data == null || data.Length <= 0)
|
||
return false;
|
||
// data 数据有误
|
||
if (videoFormat == TRTCVideoPixelFormat.TRTCVideoPixelFormat_BGRA32 && width * height * 4 != data.Length)
|
||
return false;
|
||
// 暂时不支持其他 YUV 类型
|
||
if (videoFormat == TRTCVideoPixelFormat.TRTCVideoPixelFormat_I420 && width * height * 3 / 2 != data.Length)
|
||
return false;
|
||
|
||
// 目前只实现了 BGRA32 的数据类型,如需其他类型请重写,并设置回调的数据类型
|
||
if (videoFormat == TRTCVideoPixelFormat.TRTCVideoPixelFormat_BGRA32)
|
||
{
|
||
lock (mArgbFrame)
|
||
{
|
||
if (mArgbFrame.data == null || mArgbFrame.width != width || mArgbFrame.height != height)
|
||
{
|
||
ReleaseBuffer(mArgbFrame);
|
||
mArgbFrame.width = width;
|
||
mArgbFrame.height = height;
|
||
mArgbFrame.data = new byte[data.Length];
|
||
}
|
||
Buffer.BlockCopy(data, 0, mArgbFrame.data, 0, (int)data.Length);
|
||
mArgbFrame.newFrame = true;
|
||
mArgbFrame.rotation = rotation;
|
||
}
|
||
}
|
||
|
||
// 回到主线程刷新当前画面
|
||
this.Dispatcher.Invoke(new Action(() =>
|
||
{
|
||
this.InvalidateVisual();
|
||
}));
|
||
|
||
return true;
|
||
}
|
||
|
||
protected override void OnRender(DrawingContext dc)
|
||
{
|
||
bool needDrawFrame = true;
|
||
if (mPause)
|
||
needDrawFrame = false;
|
||
if (mArgbFrame.data == null)
|
||
needDrawFrame = false;
|
||
if (!needDrawFrame)
|
||
{
|
||
return;
|
||
}
|
||
|
||
lock (mArgbFrame)
|
||
{
|
||
if (mArgbFrame.data == null)
|
||
return;
|
||
if (mRenderMode == TRTCVideoFillMode.TRTCVideoFillMode_Fill)
|
||
{
|
||
RenderFillMode(dc, mArgbFrame.data, mArgbFrame.width, mArgbFrame.height, (int)mArgbFrame.rotation * 90);
|
||
}
|
||
else if (mRenderMode == TRTCVideoFillMode.TRTCVideoFillMode_Fit)
|
||
{
|
||
RenderFitMode(dc, mArgbFrame.data, mArgbFrame.width, mArgbFrame.height, (int)mArgbFrame.rotation * 90);
|
||
}
|
||
}
|
||
}
|
||
|
||
//public event Action<WriteableBitmap> OnRenderVideoFrameHandler;
|
||
/// <summary>
|
||
/// 渲染回调byte[] data,int width, int height
|
||
/// </summary>
|
||
public event Action<byte[], int, int> OnRenderVideoFrameHandler;
|
||
//public event Action OnViewRemove;
|
||
|
||
private void RenderFillMode(DrawingContext dc, byte[] data, int width, int height, int rotation)
|
||
{
|
||
int viewWidth = (int)this.ActualWidth, viewHeight = (int)this.ActualHeight;
|
||
PixelFormat pixelFormat = PixelFormats.Pbgra32;
|
||
int bytesPerPixel = (pixelFormat.BitsPerPixel + 7) / 8;
|
||
int stride = bytesPerPixel * width;
|
||
if (mWriteableBitmap == null || mWriteableBitmap.PixelWidth != width || mWriteableBitmap.PixelHeight != height)
|
||
{
|
||
mWriteableBitmap = new WriteableBitmap(width, height, 96, 96, pixelFormat, null);
|
||
mInt32Rect = new Int32Rect(0, 0, width, height);
|
||
}
|
||
mWriteableBitmap.Lock();
|
||
Marshal.Copy(data, 0, mWriteableBitmap.BackBuffer, data.Length);
|
||
mWriteableBitmap.AddDirtyRect(mInt32Rect);
|
||
mWriteableBitmap.Unlock();
|
||
|
||
ImageBrush brush = new ImageBrush(mWriteableBitmap);
|
||
if (rotation > 0)
|
||
{
|
||
Matrix transform = Matrix.Identity;
|
||
double scale = (double)viewWidth / (double)viewHeight;
|
||
if (rotation == 90 || rotation == 270)
|
||
transform.ScaleAt(scale, scale, 0.5, 0.5);
|
||
transform.RotateAt(rotation, 0.5, 0.5);
|
||
brush.RelativeTransform = new MatrixTransform(transform);
|
||
}
|
||
brush.Stretch = Stretch.UniformToFill;
|
||
Rect rect = new Rect(0, 0, viewWidth, viewHeight);
|
||
dc.DrawRectangle(brush, mPen, rect);
|
||
}
|
||
|
||
|
||
|
||
private void RenderFitMode(DrawingContext dc, byte[] data, int width, int height, int rotation)
|
||
{
|
||
int viewWidth = (int)this.ActualWidth, viewHeight = (int)this.ActualHeight;
|
||
PixelFormat pixelFormat = PixelFormats.Pbgra32;
|
||
int bytesPerPixel = (pixelFormat.BitsPerPixel + 7) / 8;
|
||
int stride = bytesPerPixel * width;
|
||
if (mWriteableBitmap == null || mWriteableBitmap.PixelWidth != width || mWriteableBitmap.PixelHeight != height)
|
||
{
|
||
mWriteableBitmap = new WriteableBitmap(width, height, 96, 96, pixelFormat, null);
|
||
mInt32Rect = new Int32Rect(0, 0, width, height);
|
||
}
|
||
mWriteableBitmap.Lock();
|
||
Marshal.Copy(data, 0, mWriteableBitmap.BackBuffer, data.Length);
|
||
mWriteableBitmap.AddDirtyRect(mInt32Rect);
|
||
mWriteableBitmap.Unlock();
|
||
|
||
//OnRenderVideoFrameHandler?.Invoke(mWriteableBitmap);
|
||
|
||
ImageBrush brush = new ImageBrush(mWriteableBitmap);
|
||
if (rotation > 0)
|
||
{
|
||
Matrix transform = Matrix.Identity;
|
||
double scale = (double)viewHeight / (double)viewWidth;
|
||
if (rotation == 90 || rotation == 270)
|
||
transform.ScaleAt(1, scale, 0.5, 0.5);
|
||
transform.RotateAt(rotation, 0.5, 0.5);
|
||
brush.RelativeTransform = new MatrixTransform(transform);
|
||
}
|
||
brush.Stretch = Stretch.Uniform;
|
||
Rect rect = new Rect(0, 0, viewWidth, viewHeight);
|
||
dc.DrawRectangle(brush, mPen, rect);
|
||
}
|
||
|
||
private void ReleaseBuffer(FrameBufferInfo info)
|
||
{
|
||
if (info.data != null)
|
||
info.data = null;
|
||
info.width = 0;
|
||
info.height = 0;
|
||
info.newFrame = false;
|
||
info.rotation = TRTCVideoRotation.TRTCVideoRotation0;
|
||
}
|
||
|
||
#region Dispose
|
||
private bool disposed = false;
|
||
|
||
public void Dispose()
|
||
{
|
||
this.Dispose(true);
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
|
||
protected virtual void Dispose(bool disposing)
|
||
{
|
||
if (disposed) return;
|
||
if (disposing)
|
||
{
|
||
ReleaseBuffer(mArgbFrame);
|
||
mWriteableBitmap = null;
|
||
}
|
||
disposed = true;
|
||
}
|
||
|
||
~TXLiteAVVideoView()
|
||
{
|
||
this.Dispose(false);
|
||
}
|
||
#endregion
|
||
}
|
||
|
||
public class TXLiteAVVideoViewManager : ITRTCVideoRenderCallback
|
||
{
|
||
private volatile Dictionary<string, TXLiteAVVideoView> mMapViews;
|
||
|
||
public static TXLiteAVVideoViewManager sInstance;
|
||
|
||
private static Object mLocker = new Object();
|
||
|
||
public static TXLiteAVVideoViewManager GetInstance()
|
||
{
|
||
if (sInstance == null)
|
||
{
|
||
lock (mLocker)
|
||
{
|
||
if (sInstance == null)
|
||
sInstance = new TXLiteAVVideoViewManager();
|
||
}
|
||
}
|
||
return sInstance;
|
||
}
|
||
|
||
private TXLiteAVVideoViewManager()
|
||
{
|
||
mMapViews = new Dictionary<string, TXLiteAVVideoView>();
|
||
}
|
||
|
||
private string GetKey(string userId, TRTCVideoStreamType type)
|
||
{
|
||
return String.Format("{0}_{1}", userId, type);
|
||
}
|
||
|
||
// 主要用于判断当前 user 是否还有存在流画面,存在则不移除监听。
|
||
public bool HasUserId(string userId)
|
||
{
|
||
bool exit = false;
|
||
lock (mMapViews)
|
||
{
|
||
exit = mMapViews.ContainsKey(GetKey(userId, TRTCVideoStreamType.TRTCVideoStreamTypeBig)) ||
|
||
mMapViews.ContainsKey(GetKey(userId, TRTCVideoStreamType.TRTCVideoStreamTypeSub));
|
||
}
|
||
return exit;
|
||
}
|
||
|
||
public void AddView(string userId, TRTCVideoStreamType type, TXLiteAVVideoView view)
|
||
{
|
||
lock (mMapViews)
|
||
{
|
||
bool find = false;
|
||
foreach (var item in mMapViews)
|
||
{
|
||
if (item.Key.Equals(GetKey(userId, type)))
|
||
{
|
||
find = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!find)
|
||
{
|
||
mMapViews.Add(GetKey(userId, type), view);
|
||
}
|
||
}
|
||
}
|
||
|
||
public void RemoveView(string userId, TRTCVideoStreamType type, TXLiteAVVideoView view)
|
||
{
|
||
lock (mMapViews)
|
||
{
|
||
foreach (var item in mMapViews.ToList())
|
||
{
|
||
if (item.Key.Equals(GetKey(userId, type)))
|
||
{
|
||
if (item.Value != null)
|
||
{
|
||
item.Value.Dispose();
|
||
}
|
||
mMapViews.Remove(item.Key);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public void RemoveAllView()
|
||
{
|
||
lock (mMapViews)
|
||
mMapViews.Clear();
|
||
}
|
||
|
||
public int Count
|
||
{
|
||
get
|
||
{
|
||
lock (mMapViews)
|
||
return mMapViews.Count;
|
||
}
|
||
}
|
||
|
||
//public event Action<string, TRTCVideoStreamType, TRTCVideoFrame> OnRenderVideoFrameHandler;
|
||
/// <summary>
|
||
/// 自定义渲染回调,只能存在一个回调
|
||
/// </summary>
|
||
/// <param name="userId"></param>
|
||
/// <param name="streamType"></param>
|
||
/// <param name="frame"></param>
|
||
public void onRenderVideoFrame(string userId, TRTCVideoStreamType streamType, TRTCVideoFrame frame)
|
||
{
|
||
//OnRenderVideoFrameHandler?.Invoke(userId, streamType, frame);
|
||
// 大小视频是占一个视频位,底层支持动态切换。
|
||
if (streamType == TRTCVideoStreamType.TRTCVideoStreamTypeSmall)
|
||
streamType = TRTCVideoStreamType.TRTCVideoStreamTypeBig;
|
||
TXLiteAVVideoView view = null;
|
||
lock (mMapViews)
|
||
{
|
||
foreach (var item in mMapViews)
|
||
{
|
||
if (item.Key.Equals(GetKey(userId, streamType)) && item.Value != null)
|
||
{
|
||
view = item.Value;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (view != null)
|
||
view.AppendVideoFrame(frame.data, (int)frame.width, (int)frame.height, frame.videoFormat, frame.rotation);
|
||
}
|
||
}
|
||
|
||
class FrameBufferInfo
|
||
{
|
||
public byte[] data { get; set; }
|
||
|
||
public int width { get; set; }
|
||
|
||
public int height { get; set; }
|
||
|
||
public bool newFrame { get; set; }
|
||
|
||
public TRTCVideoRotation rotation { get; set; }
|
||
|
||
public FrameBufferInfo()
|
||
{
|
||
rotation = TRTCVideoRotation.TRTCVideoRotation0;
|
||
newFrame = false;
|
||
width = 0;
|
||
height = 0;
|
||
data = null;
|
||
}
|
||
}
|
||
}
|