using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Profiling; using UnityEngine.Rendering; using Debug = UnityEngine.Debug; using Object = UnityEngine.Object; namespace ZC { public class RTMPPublisher : MonoBehaviour { Thread _thread; private Timer _timer; Process _process; private RectInt _rectInt; private RTMPConfig _config; private const int _rectIntWidth = 1920; private const int _rectIntHeight = 1080; [SerializeField] private Camera _camera; private Texture2D _output; private volatile int _sendData; private AsyncGPUReadbackRequest _asyncGPUReadbackRequest; private bool _isRequesting; private byte[] _bitmapArray; private ConcurrentQueue> _textureDataPool; private Queue<(AsyncGPUReadbackRequest, NativeArray)> _textureDatas; private ConcurrentQueue<(AsyncGPUReadbackRequest, NativeArray)> _doneTextureDatas; private int _targetTextureWidth; private int _targetTextureHeight; public void SetCamera(Camera camera) { this._camera = camera; } // Start is called before the first frame update void Start() { _config = Object.FindAnyObjectByType(); if (_config == null) { Debug.LogError("This need the config, plz create a GameObject with RTMPConfig"); return; } _textureDataPool = new ConcurrentQueue>(); _textureDatas = new Queue<(AsyncGPUReadbackRequest, NativeArray)>(); _doneTextureDatas = new ConcurrentQueue<(AsyncGPUReadbackRequest, NativeArray)>(); _config.Load(); _timer = new Timer(Tick, null, TimeSpan.FromMilliseconds(0), TimeSpan.FromMilliseconds(16)); Setup(Camera.main); } /// /// 其他线程tick /// /// private void Tick(object state) { try { if (this._doneTextureDatas.TryDequeue(out var result)) { var textureData = result.Item2; Profiler.BeginSample("Conversion"); Bitmap.EncodeToBitmap(textureData, _bitmapArray, 0, textureData.Length, _targetTextureWidth, this._targetTextureHeight); Profiler.EndSample(); Profiler.BeginSample("WriteBuffer"); _process.StandardInput.BaseStream.Write(_bitmapArray, 0, _bitmapArray.Length); Profiler.EndSample(); _textureDataPool.Enqueue(result.Item2); } } catch (Exception e) { UnityEngine.Debug.LogError(e.ToString()); } } private void CreateCaptureThread() { this._thread = new Thread(this.CreateThread); this._thread.Start(); } private void OnDestroy() { Dispose(); } private void Update() { if (_sendData != 0) { if (_camera && !_isRequesting) { var cameraActiveTexture = _camera.targetTexture; RenderTexture.active = cameraActiveTexture; if (!_textureDataPool.TryDequeue(out var nativeArray)) { var targetTextureByteCount = this._camera.targetTexture.width * _camera.targetTexture.height * 3; nativeArray = new NativeArray(targetTextureByteCount, Allocator.Persistent); } var asyncGPUReadbackRequest = AsyncGPUReadback.RequestIntoNativeArray(ref nativeArray, cameraActiveTexture, 0, GraphicsFormat.B8G8R8_SRGB, null); _textureDatas.Enqueue((asyncGPUReadbackRequest, nativeArray)); } if (_textureDatas.TryPeek(out var result)) { if (result.Item1.done) { this._textureDatas.Dequeue(); if (result.Item1.hasError) { Debug.LogError("Request GPU DATA has error"); } else { //放入多线程计算 this._doneTextureDatas.Enqueue(result); } } } } } private void DisposeCaptureThread() { foreach (var textureData in this._textureDatas) { textureData.Item2.Dispose(); } _textureDatas.Clear(); foreach (var doneTextureData in this._doneTextureDatas) { doneTextureData.Item2.Dispose(); } _doneTextureDatas.Clear(); foreach (var nativeArray in this._textureDataPool) { nativeArray.Dispose(); } if (_process != null && !this._process.HasExited) { GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0); // _process.StandardInput.Close(); this._process.Kill(); this._process.Close(); } this._thread.Abort(); } void CreateThread(object state) { _process = new Process(); ProcessStartInfo processStartInfo = new ProcessStartInfo(); processStartInfo.UseShellExecute = false; processStartInfo.CreateNoWindow = true; processStartInfo.RedirectStandardInput = true; processStartInfo.RedirectStandardOutput = true; processStartInfo.RedirectStandardError = true; processStartInfo.StandardOutputEncoding = System.Text.Encoding.UTF8; processStartInfo.StandardErrorEncoding = System.Text.Encoding.UTF8; processStartInfo.FileName = $"\"{_config.ffmpegPath}\""; processStartInfo.Arguments = $" -probesize 32 -thread_queue_size 5096 -fflags discardcorrupt -flags low_delay -analyzeduration 0 " + $" -rtbufsize 100M -f dshow -i audio=\"virtual-audio-capturer\" " + $" -f image2pipe -use_wallclock_as_timestamps 1 -r 60 -i - " + $" -loglevel info " + $" -map 0:a:0 -map 1:v:0 " + $" -c:a aac " + $" -c:v:0 libx264 -g 1 -bf 0 -max_delay 0 -vf scale={this._config.resolution} -preset:v ultrafast -tune:v zerolatency -crf 10 -pix_fmt yuv420p -strict -2 " + $" -f flv {this._config.server}{this._config.appName} -bf 0 "; Debug.Log(processStartInfo.Arguments); _process.StartInfo = processStartInfo; _process.EnableRaisingEvents = true; _process.OutputDataReceived += (s, e) => { Debug.Log(e.Data); }; _process.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data) && e.Data.ToLower().Contains("error")) { Debug.LogError(e.Data); } else { Debug.Log(e.Data); } }; _process.Start(); Interlocked.Exchange(ref _sendData, 1); _process.BeginOutputReadLine(); _process.BeginErrorReadLine(); _process.WaitForExit(); Debug.Log("Process exited!"); } public void Setup(Camera camera) { if (!camera) throw new NullReferenceException("camera is null"); if (!camera.targetTexture) throw new Exception($"The camera[{camera}]'s targetTexture is null;"); _isRequesting = true; SetCamera(camera); _sendData = 0; Object.Destroy(_output); this._targetTextureWidth = camera.targetTexture.width; this._targetTextureHeight = camera.targetTexture.height; var targetTextureByteCount = this._targetTextureWidth * this._targetTextureHeight * 3; if (_bitmapArray == null || _bitmapArray.Length < Bitmap.FileHeaderSize + Bitmap.ImageHeaderSize + targetTextureByteCount) { _bitmapArray = new byte[Bitmap.FileHeaderSize + Bitmap.ImageHeaderSize + targetTextureByteCount]; } _output = new Texture2D(this._targetTextureWidth, this._targetTextureHeight); CreateCaptureThread(); StartCoroutine(this.Test()); } IEnumerator Test() { yield return new WaitForSeconds(0.3f); _isRequesting = false; } public void Dispose() { try { this._isRequesting = true; _sendData = 0; _camera = null; Object.Destroy(_output); this.DisposeCaptureThread(); } finally { #if UNITY_EDITOR #else _textureData.Dispose(); #endif } } [DllImport("kernel32.dll", SetLastError = true)] static extern bool GenerateConsoleCtrlEvent(ConsoleCtrlEvent sigevent, int dwProcessGroupId); public enum ConsoleCtrlEvent { CTRL_C = 0, CTRL_BREAK = 1, CTRL_CLOSE = 2, CTRL_LOGOFF = 5, CTRL_SHUTDOWN = 6 } } }