-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathVideoEncoder.cs
More file actions
304 lines (259 loc) · 9.96 KB
/
VideoEncoder.cs
File metadata and controls
304 lines (259 loc) · 9.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
using System;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Collections.Concurrent;
using UnityEngine;
namespace ReplayRenderer
{
/// <summary>
/// Streams frames directly to FFmpeg without saving PNG files first
/// This is the PC equivalent of Quest's "Hollywood" renderer
/// </summary>
public class VideoEncoder : MonoBehaviour
{
// FFmpeg process
private Process ffmpegProcess;
private Stream ffmpegInputStream;
private Thread encodingThread;
private bool isEncoding;
// Frame queue
private ConcurrentQueue<byte[]> frameQueue = new ConcurrentQueue<byte[]>();
private int totalFramesQueued = 0;
private int totalFramesEncoded = 0;
// Settings
private int width;
private int height;
private int frameRate;
private string outputPath;
private string audioPath;
// Performance
private const int MAX_QUEUE_SIZE = 30; // Buffer up to 30 frames
public int FramesQueued => totalFramesQueued;
public int FramesEncoded => totalFramesEncoded;
public bool IsReady => isEncoding && ffmpegProcess != null && !ffmpegProcess.HasExited;
/// <summary>
/// Initialize the encoder and start FFmpeg process
/// </summary>
public bool Initialize(int width, int height, int fps, string outputFile, string audioFile = null)
{
this.width = width;
this.height = height;
this.frameRate = fps;
this.outputPath = outputFile;
this.audioPath = audioFile;
Plugin.Log.Info($"Initializing video encoder: {width}x{height} @ {fps}fps");
string ffmpegPath = FindFFmpeg();
if (string.IsNullOrEmpty(ffmpegPath))
{
Plugin.Log.Error("FFmpeg not found!");
return false;
}
try
{
// Build FFmpeg command for streaming input
string arguments = BuildFFmpegArguments();
Plugin.Log.Info($"Starting FFmpeg with args: {arguments}");
ffmpegProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
// Start process
ffmpegProcess.Start();
ffmpegInputStream = ffmpegProcess.StandardInput.BaseStream;
// Start encoding thread
isEncoding = true;
encodingThread = new Thread(EncodingLoop);
encodingThread.Start();
Plugin.Log.Info("Video encoder initialized successfully");
return true;
}
catch (Exception ex)
{
Plugin.Log.Error($"Failed to initialize encoder: {ex.Message}");
return false;
}
}
private string BuildFFmpegArguments()
{
// Input: raw RGB24 frames from stdin
string args = $"-f rawvideo -pixel_format rgb24 -video_size {width}x{height} -framerate {frameRate} -i pipe:0";
// Add audio if available
if (!string.IsNullOrEmpty(audioPath) && File.Exists(audioPath))
{
args += $" -i \"{audioPath}\"";
args += " -c:a aac -b:a 192k";
args += " -shortest"; // Stop at shortest stream
}
// Video encoding settings
string preset = Plugin.Config.FFmpegPreset;
int crf = Plugin.Config.FFmpegCRF;
args += $" -c:v libx264 -preset {preset} -crf {crf}";
args += " -pix_fmt yuv420p"; // For compatibility
// Output file
args += $" \"{outputPath}\" -y";
return args;
}
/// <summary>
/// Queue a frame for encoding
/// </summary>
public void QueueFrame(Texture2D texture)
{
if (!isEncoding || frameQueue.Count >= MAX_QUEUE_SIZE)
{
// Wait if queue is full
while (frameQueue.Count >= MAX_QUEUE_SIZE && isEncoding)
{
Thread.Sleep(1);
}
}
// Get raw RGB24 bytes from texture
byte[] frameData = GetRawRGBData(texture);
frameQueue.Enqueue(frameData);
totalFramesQueued++;
}
/// <summary>
/// Convert Unity texture to raw RGB24 bytes
/// </summary>
private byte[] GetRawRGBData(Texture2D texture)
{
Color32[] pixels = texture.GetPixels32();
byte[] rgbData = new byte[width * height * 3];
int index = 0;
// Unity textures are bottom-to-top, FFmpeg expects top-to-bottom
for (int y = height - 1; y >= 0; y--)
{
for (int x = 0; x < width; x++)
{
Color32 pixel = pixels[y * width + x];
rgbData[index++] = pixel.r;
rgbData[index++] = pixel.g;
rgbData[index++] = pixel.b;
}
}
return rgbData;
}
/// <summary>
/// Background thread that writes frames to FFmpeg
/// </summary>
private void EncodingLoop()
{
Plugin.Log.Info("Encoding thread started");
try
{
while (isEncoding)
{
if (frameQueue.TryDequeue(out byte[] frameData))
{
// Write frame to FFmpeg stdin
ffmpegInputStream.Write(frameData, 0, frameData.Length);
ffmpegInputStream.Flush();
totalFramesEncoded++;
if (totalFramesEncoded % 100 == 0)
{
Plugin.Log.Info($"Encoded {totalFramesEncoded} frames");
}
}
else
{
// Queue is empty, wait a bit
Thread.Sleep(10);
}
}
Plugin.Log.Info("Encoding loop finished");
}
catch (Exception ex)
{
Plugin.Log.Error($"Encoding thread error: {ex.Message}");
}
}
/// <summary>
/// Finish encoding and close FFmpeg
/// </summary>
public void Finish()
{
Plugin.Log.Info("Finishing video encoding...");
isEncoding = false;
// Wait for remaining frames to be encoded
int waitCount = 0;
while (frameQueue.Count > 0 && waitCount < 100)
{
Thread.Sleep(100);
waitCount++;
}
// Close FFmpeg stdin to signal end of input
if (ffmpegInputStream != null)
{
try
{
ffmpegInputStream.Close();
}
catch { }
}
// Wait for FFmpeg to finish
if (ffmpegProcess != null && !ffmpegProcess.HasExited)
{
Plugin.Log.Info("Waiting for FFmpeg to finish...");
ffmpegProcess.WaitForExit(30000); // 30 second timeout
if (!ffmpegProcess.HasExited)
{
Plugin.Log.Warn("FFmpeg did not exit, killing process");
ffmpegProcess.Kill();
}
}
// Wait for encoding thread
if (encodingThread != null && encodingThread.IsAlive)
{
encodingThread.Join(5000);
}
Plugin.Log.Info($"Video encoding complete. Total frames: {totalFramesEncoded}");
if (File.Exists(outputPath))
{
FileInfo fileInfo = new FileInfo(outputPath);
Plugin.Log.Info($"Output file: {outputPath} ({fileInfo.Length / 1024 / 1024}MB)");
}
}
private string FindFFmpeg()
{
if (!string.IsNullOrEmpty(Plugin.Config.FFmpegPath) && File.Exists(Plugin.Config.FFmpegPath))
{
return Plugin.Config.FFmpegPath;
}
string[] possiblePaths = new string[]
{
Path.Combine(Application.dataPath, "..", "FFmpeg", "ffmpeg.exe"),
Path.Combine(Application.dataPath, "..", "Libs", "ffmpeg.exe"),
@"C:\ffmpeg\bin\ffmpeg.exe",
};
foreach (string path in possiblePaths)
{
try
{
string fullPath = Path.GetFullPath(path);
if (File.Exists(fullPath))
{
return fullPath;
}
}
catch { }
}
return "ffmpeg"; // Try PATH
}
void OnDestroy()
{
if (isEncoding)
{
Finish();
}
}
}
}