1using System; 2using System.Collections.Generic; 3using System.IO; 4using System.Runtime.InteropServices; 5using System.Text; 6using WindowCapture.Native; 7 8namespace WindowCapture.Recording 9{ 10 /// <summary> 11 /// Video writer with fallback chain: H.264 → WMV3 → MJPEG-in-MP4. 12 /// H.264/WMV use Media Foundation SinkWriter (inter-frame compression). 13 /// MJPEG uses a custom MP4 muxer with frame deduplication. 14 /// </summary> 15 public class Mp4Writer : IDisposable 16 { 17 private enum Mode { None, H264, WMV, MJPEG } 18 private Mode mode; 19 20 // --- Media Foundation fields (H264 + WMV) --- 21 private IMFTransform encoder; // H264-only: manual MFT encoding pipeline 22 private IMFSinkWriter writer; // H264+WMV: writes to output file 23 private uint videoStreamIdx; 24 private uint audioStreamIdx; 25 private bool hasMFAudio; 26 27 // H264-only 28 private int encWidth, encHeight; 29 private bool encoderProvidesSamples; 30 private uint encoderOutputBufSize; 31 private IntPtr nv12Buf; 32 private int nv12BufSize; 33 34 // --- MJPEG muxer fields --- 35 private BinaryWriter bw; 36 private FileStream fileStream; 37 private long mdatStart; 38 private List<int> videoChunkSizes; 39 private List<long> videoChunkOffsets; 40 private List<int> audioChunkSizes; 41 private List<long> audioChunkOffsets; 42 private System.Drawing.Imaging.ImageCodecInfo jpegCodec; 43 private System.Drawing.Imaging.EncoderParameters jpegParams; 44 45 // MJPEG frame deduplication 46 private IntPtr prevFrameBuf; 47 private bool hasPrevFrame; 48 private MemoryStream jpegStream; 49 private long lastJpegOffset; 50 private int lastJpegSize; 51 private int dupFrameCount, uniqueFrameCount; 52 53 // --- Common --- 54 private string actualPath; 55 private int srcWidth, srcHeight; 56 private int fps; 57 private long frameDuration; // in 100ns ticks (MF time units) 58 private long videoTimestamp, audioTimestamp; 59 private bool closed; 60 private bool hasAudio; 61 private int audioRate, audioCh, audioBps; 62 63 public string ActualPath { get { return actualPath; } } 64 65 // H264 encoder CLSID 66 static readonly Guid CLSID_H264Encoder = new Guid("6ca50344-051a-4ded-9779-a43305165e35"); 67 68 // MFT constants 69 const uint MFT_MSG_BEGIN_STREAMING = 0x10000000; 70 const uint MFT_MSG_START_OF_STREAM = 0x10000003; 71 const uint MFT_MSG_DRAIN = 1; 72 const uint MFT_PROVIDES_SAMPLES_FLAG = 0x100; 73 const int MF_E_NEED_MORE_INPUT = unchecked((int)0xC00D6D72); 74 75 // Audio codec GUIDs (not in WinApi.cs) 76 static readonly Guid MFAudioFormat_AAC = new Guid(0x00001610, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71); 77 static readonly Guid MFAudioFormat_WMA2 = new Guid(0x00000161, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71); 78 79 [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")] 80 static extern void CopyMemory(IntPtr dst, IntPtr src, int len); 81 82 [DllImport("ole32.dll")] 83 static extern int PropVariantClear(IntPtr pvar); 84 85 #region Logging + PropVariant helpers 86 87 static void Log(string msg) 88 { 89 try 90 { 91 File.AppendAllText( 92 Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "VideoLog.txt"), 93 DateTime.Now.ToString("HH:mm:ss.fff") + " [Writer] " + msg + "\r\n"); 94 } 95 catch { } 96 } 97 98 static IntPtr AllocPropVariantUI4(uint val) 99 { 100 int size = IntPtr.Size == 8 ? 24 : 16; 101 IntPtr pv = Marshal.AllocCoTaskMem(size); 102 for (int i = 0; i < size; i++) Marshal.WriteByte(pv, i, 0); 103 Marshal.WriteInt16(pv, 0, 19); // VT_UI4 104 Marshal.WriteInt32(pv, 8, (int)val); 105 return pv; 106 } 107 108 static void FreePropVariant(IntPtr pv) 109 { 110 if (pv != IntPtr.Zero) { PropVariantClear(pv); Marshal.FreeCoTaskMem(pv); } 111 } 112 113 #endregion 114 115 #region Constructor 116 117 public Mp4Writer(string path, int w, int h, int fps, int kbps, 118 bool audio, int sampleRate, int channels, int bitsPerSample) 119 { 120 srcWidth = w; 121 srcHeight = h; 122 this.fps = fps; 123 frameDuration = 10000000L / fps; 124 hasAudio = audio; 125 audioRate = sampleRate; 126 audioCh = channels; 127 audioBps = bitsPerSample; 128 actualPath = path; 129 130 // H264 requires 16-aligned dimensions 131 encWidth = (w / 16) * 16; 132 encHeight = (h / 16) * 16; 133 if (encWidth < 16) encWidth = 16; 134 if (encHeight < 16) encHeight = 16; 135 136 Log(string.Format("Init: src={0}x{1} enc={2}x{3} fps={4} kbps={5}", 137 w, h, encWidth, encHeight, fps, kbps)); 138 139 // Fallback chain: H.264 → WMV3 → MJPEG 140 if (TryInitH264(path, kbps)) 141 { 142 mode = Mode.H264; 143 actualPath = path; 144 return; 145 } 146 147 string wmvPath = Path.ChangeExtension(path, ".wmv"); 148 if (TryInitWmv(wmvPath, kbps)) 149 { 150 mode = Mode.WMV; 151 actualPath = wmvPath; 152 return; 153 } 154 155 Log("H.264 and WMV both failed — falling back to MJPEG"); 156 mode = Mode.MJPEG; 157 actualPath = path; 158 InitMjpegMuxer(path); 159 } 160 161 #endregion 162 163 #region Media Foundation: shared helpers 164 165 /// <summary>Create SinkWriter with throttling disabled.</summary> 166 private bool CreateSinkWriter(string path) 167 { 168 IMFAttributes attrs; 169 WinApi.MFCreateAttributes(out attrs, 1); 170 var key = WinApi.MF_SINK_WRITER_DISABLE_THROTTLING; 171 attrs.SetUINT32(ref key, 1); 172 173 int hr = WinApi.MFCreateSinkWriterFromURL(path, null, attrs, out writer); 174 Marshal.ReleaseComObject(attrs); 175 Log("SinkWriter create(" + Path.GetExtension(path) + ")=0x" + hr.ToString("X")); 176 return hr == 0; 177 } 178 179 /// <summary>Configure audio streams on the SinkWriter (AAC for MP4, WMA for WMV).</summary> 180 private void TryInitAudio(bool useWma) 181 { 182 if (!hasAudio) return; 183 try 184 { 185 var majKey = WinApi.MF_MT_MAJOR_TYPE; 186 var subKey = WinApi.MF_MT_SUBTYPE; 187 var audMaj = WinApi.MFMediaType_Audio; 188 189 // Output: compressed audio 190 IMFMediaType outType; 191 WinApi.MFCreateMediaType(out outType); 192 outType.SetGUID(ref majKey, ref audMaj); 193 var codecGuid = useWma ? MFAudioFormat_WMA2 : MFAudioFormat_AAC; 194 outType.SetGUID(ref subKey, ref codecGuid); 195 SetAudioAttrs(outType, (uint)audioRate, (uint)audioCh, 16, 20000); 196 writer.AddStream(outType, out audioStreamIdx); 197 Marshal.ReleaseComObject(outType); 198 199 // Input: PCM 200 IMFMediaType inType; 201 WinApi.MFCreateMediaType(out inType); 202 inType.SetGUID(ref majKey, ref audMaj); 203 var pcm = WinApi.MFAudioFormat_PCM; 204 inType.SetGUID(ref subKey, ref pcm); 205 uint blockAlign = (uint)(audioCh * audioBps / 8); 206 SetAudioAttrs(inType, (uint)audioRate, (uint)audioCh, (uint)audioBps, 207 (uint)(audioRate * blockAlign)); 208 var baKey = WinApi.MF_MT_AUDIO_BLOCK_ALIGNMENT; 209 inType.SetUINT32(ref baKey, blockAlign); 210 writer.SetInputMediaType(audioStreamIdx, inType, null); 211 Marshal.ReleaseComObject(inType); 212 213 hasMFAudio = true; 214 Log("Audio init OK (" + (useWma ? "WMA" : "AAC") + ")"); 215 } 216 catch (Exception ex) 217 { 218 Log("Audio init failed: " + ex.Message); 219 hasMFAudio = false; 220 } 221 } 222 223 private static void SetAudioAttrs(IMFMediaType t, uint rate, uint ch, uint bps, uint avgBytesPerSec) 224 { 225 var k1 = WinApi.MF_MT_AUDIO_SAMPLES_PER_SECOND; 226 var k2 = WinApi.MF_MT_AUDIO_NUM_CHANNELS; 227 var k3 = WinApi.MF_MT_AUDIO_BITS_PER_SAMPLE; 228 var k4 = WinApi.MF_MT_AUDIO_AVG_BYTES_PER_SECOND; 229 t.SetUINT32(ref k1, rate); 230 t.SetUINT32(ref k2, ch); 231 t.SetUINT32(ref k3, bps); 232 t.SetUINT32(ref k4, avgBytesPerSec); 233 } 234 235 /// <summary>Build a video media type with common attributes.</summary> 236 private IMFMediaType CreateVideoType(Guid subtype, int w, int h, uint bitrate) 237 { 238 IMFMediaType mt; 239 WinApi.MFCreateMediaType(out mt); 240 var majKey = WinApi.MF_MT_MAJOR_TYPE; 241 var subKey = WinApi.MF_MT_SUBTYPE; 242 var fsKey = WinApi.MF_MT_FRAME_SIZE; 243 var frKey = WinApi.MF_MT_FRAME_RATE; 244 var paKey = WinApi.MF_MT_PIXEL_ASPECT_RATIO; 245 var ilKey = WinApi.MF_MT_INTERLACE_MODE; 246 var vidMaj = WinApi.MFMediaType_Video; 247 248 mt.SetGUID(ref majKey, ref vidMaj); 249 mt.SetGUID(ref subKey, ref subtype); 250 mt.SetUINT64(ref fsKey, WinApi.PackToUINT64((uint)w, (uint)h)); 251 mt.SetUINT64(ref frKey, WinApi.PackToUINT64((uint)fps, 1)); 252 mt.SetUINT64(ref paKey, WinApi.PackToUINT64(1, 1)); 253 mt.SetUINT32(ref ilKey, 2); // MFVideoInterlace_Progressive 254 255 if (bitrate > 0) 256 { 257 var brKey = WinApi.MF_MT_AVG_BITRATE; 258 mt.SetUINT32(ref brKey, bitrate); 259 } 260 return mt; 261 } 262 263 /// <summary>Write an MF sample from raw pixel/audio data to the SinkWriter.</summary> 264 private void WriteMFSample(uint streamIdx, IntPtr data, int dataLen, ref long timestamp) 265 { 266 IMFMediaBuffer buf; 267 WinApi.MFCreateMemoryBuffer((uint)dataLen, out buf); 268 IntPtr ptr; int mx, cl; 269 buf.Lock(out ptr, out mx, out cl); 270 CopyMemory(ptr, data, dataLen); 271 buf.Unlock(); 272 buf.SetCurrentLength(dataLen); 273 274 IMFSample sample; 275 WinApi.MFCreateSample(out sample); 276 sample.AddBuffer(buf); 277 sample.SetSampleTime(timestamp); 278 sample.SetSampleDuration(frameDuration); 279 280 writer.WriteSample(streamIdx, sample); 281 282 Marshal.ReleaseComObject(buf); 283 Marshal.ReleaseComObject(sample); 284 timestamp += frameDuration; 285 } 286 287 #endregion 288 289 #region WMV3 init (SinkWriter handles encoding) 290 291 // Media Foundation startup is process-global and reference counted. Calling MFShutdown more 292 // times than MFStartup (easy to do across the H264→WMV→MJPEG fallback chain) underflows that 293 // count and breaks MF for the rest of the process (e.g. video playback). Make it exactly-once. 294 private bool mfStarted; 295 private bool MfStartOnce() 296 { 297 if (mfStarted) return true; 298 if (WinApi.MFStartup(WinApi.MF_VERSION, 0) != 0) return false; 299 mfStarted = true; 300 return true; 301 } 302 private void MfStopOnce() 303 { 304 if (!mfStarted) return; 305 try { WinApi.MFShutdown(); } catch { } 306 mfStarted = false; 307 } 308 309 private bool TryInitWmv(string path, int kbps) 310 { 311 try 312 { 313 if (!MfStartOnce()) return false; 314 315 if (!CreateSinkWriter(path)) 316 { MfStopOnce(); return false; } 317 318 // Video: WMV3 output, RGB32 input 319 var outType = CreateVideoType(WinApi.MFVideoFormat_WMV3, srcWidth, srcHeight, (uint)(kbps * 1000)); 320 writer.AddStream(outType, out videoStreamIdx); 321 Marshal.ReleaseComObject(outType); 322 323 var inType = CreateVideoType(WinApi.MFVideoFormat_RGB32, srcWidth, srcHeight, 0); 324 var strideKey = WinApi.MF_MT_DEFAULT_STRIDE; 325 inType.SetUINT32(ref strideKey, (uint)(srcWidth * 4)); // top-down DIB 326 writer.SetInputMediaType(videoStreamIdx, inType, null); 327 Marshal.ReleaseComObject(inType); 328 329 TryInitAudio(useWma: true); 330 331 writer.BeginWriting(); 332 Log("WMV recording → " + path); 333 return true; 334 } 335 catch (Exception ex) 336 { 337 Log("WMV init failed: " + ex.Message); 338 CleanupMF(); 339 return false; 340 } 341 } 342 343 #endregion 344 345 #region H.264 init (manual MFT + SinkWriter as muxer) 346 347 private bool TryInitH264(string path, int kbps) 348 { 349 try 350 { 351 if (!MfStartOnce()) return false; 352 353 // Create H.264 encoder MFT 354 Guid clsid = CLSID_H264Encoder; 355 Guid iid = typeof(IMFTransform).GUID; 356 object mftObj; 357 int hr = WinApi.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out mftObj); 358 Log("CoCreate H264=0x" + hr.ToString("X")); 359 if (hr != 0) { MfStopOnce(); return false; } 360 encoder = (IMFTransform)mftObj; 361 362 // Configure quality via ICodecAPI 363 ConfigureEncoderQuality(); 364 365 // Set output type (H.264 Baseline, then Main as fallback) 366 var outType = CreateVideoType(WinApi.MFVideoFormat_H264, encWidth, encHeight, (uint)(kbps * 1000)); 367 var profKey = WinApi.MF_MT_MPEG2_PROFILE; 368 outType.SetUINT32(ref profKey, 66); // Baseline 369 370 hr = encoder.SetOutputType(0, outType, 0); 371 Log("H264 SetOutputType(Baseline)=0x" + hr.ToString("X")); 372 if (hr != 0) 373 { 374 outType.SetUINT32(ref profKey, 77); // Main 375 hr = encoder.SetOutputType(0, outType, 0); 376 Log("H264 SetOutputType(Main)=0x" + hr.ToString("X")); 377 } 378 if (hr != 0) { Marshal.ReleaseComObject(outType); CleanupMF(); return false; } 379 380 // Find NV12 input type 381 if (!SetH264InputType()) 382 { Marshal.ReleaseComObject(outType); CleanupMF(); return false; } 383 384 // Prepare NV12 buffer 385 nv12BufSize = encWidth * encHeight * 3 / 2; 386 nv12Buf = Marshal.AllocHGlobal(nv12BufSize); 387 388 // Query output stream info 389 MFT_OUTPUT_STREAM_INFO osi; 390 hr = encoder.GetOutputStreamInfo(0, out osi); 391 encoderProvidesSamples = (hr == 0) && (osi.dwFlags & MFT_PROVIDES_SAMPLES_FLAG) != 0; 392 encoderOutputBufSize = (hr == 0 && osi.cbSize > 0) ? osi.cbSize : (uint)(encWidth * encHeight * 2); 393 394 encoder.ProcessMessage(MFT_MSG_BEGIN_STREAMING, IntPtr.Zero); 395 encoder.ProcessMessage(MFT_MSG_START_OF_STREAM, IntPtr.Zero); 396 397 // Create SinkWriter (muxer only, we feed pre-encoded H264) 398 if (!CreateSinkWriter(path)) 399 { Marshal.ReleaseComObject(outType); CleanupMF(); return false; } 400 401 IMFMediaType actualOutType; 402 hr = encoder.GetOutputCurrentType(0, out actualOutType); 403 var sinkType = (hr == 0 && actualOutType != null) ? actualOutType : outType; 404 405 writer.AddStream(sinkType, out videoStreamIdx); 406 writer.SetInputMediaType(videoStreamIdx, sinkType, null); 407 408 if (actualOutType != null && actualOutType != outType) Marshal.ReleaseComObject(actualOutType); 409 Marshal.ReleaseComObject(outType); 410 411 TryInitAudio(useWma: false); // AAC for MP4 412 413 writer.BeginWriting(); 414 Log("H.264 recording → " + path); 415 return true; 416 } 417 catch (Exception ex) 418 { 419 Log("H264 init failed: " + ex.Message); 420 CleanupMF(); 421 return false; 422 } 423 } 424 425 private void ConfigureEncoderQuality() 426 { 427 try 428 { 429 ICodecAPI api = (ICodecAPI)encoder; 430 var rcGuid = WinApi.CODECAPI_AVEncCommonRateControlMode; 431 IntPtr pv = AllocPropVariantUI4(0); // UnconstrainedVBR 432 api.SetValue(ref rcGuid, pv); 433 FreePropVariant(pv); 434 var qGuid = WinApi.CODECAPI_AVEncCommonQuality; 435 pv = AllocPropVariantUI4(70); 436 api.SetValue(ref qGuid, pv); 437 FreePropVariant(pv); 438 } 439 catch { } 440 } 441 442 private bool SetH264InputType() 443 { 444 var subKey = WinApi.MF_MT_SUBTYPE; 445 var fsKey = WinApi.MF_MT_FRAME_SIZE; 446 var frKey = WinApi.MF_MT_FRAME_RATE; 447 var paKey = WinApi.MF_MT_PIXEL_ASPECT_RATIO; 448 var ilKey = WinApi.MF_MT_INTERLACE_MODE; 449 var nv12 = WinApi.MFVideoFormat_NV12; 450 451 IMFMediaType chosen = null; 452 for (uint i = 0; i < 100; i++) 453 { 454 IMFMediaType avail; 455 if (encoder.GetInputAvailableType(0, i, out avail) != 0) break; 456 Guid sub; avail.GetGUID(ref subKey, out sub); 457 if (sub == nv12 && chosen == null) 458 chosen = avail; 459 else 460 Marshal.ReleaseComObject(avail); 461 } 462 if (chosen == null) return false; 463 464 chosen.SetUINT64(ref fsKey, WinApi.PackToUINT64((uint)encWidth, (uint)encHeight)); 465 chosen.SetUINT64(ref frKey, WinApi.PackToUINT64((uint)fps, 1)); 466 chosen.SetUINT64(ref paKey, WinApi.PackToUINT64(1, 1)); 467 chosen.SetUINT32(ref ilKey, 2); 468 469 int hr = encoder.SetInputType(0, chosen, 0); 470 Log("H264 SetInputType(NV12)=0x" + hr.ToString("X")); 471 Marshal.ReleaseComObject(chosen); 472 return hr == 0; 473 } 474 475 private void CleanupMF() 476 { 477 if (encoder != null) { try { Marshal.ReleaseComObject(encoder); } catch { } encoder = null; } 478 if (writer != null) { try { Marshal.ReleaseComObject(writer); } catch { } writer = null; } 479 if (nv12Buf != IntPtr.Zero) { Marshal.FreeHGlobal(nv12Buf); nv12Buf = IntPtr.Zero; } 480 MfStopOnce(); 481 } 482 483 #endregion 484 485 #region MJPEG muxer init 486 487 private void InitMjpegMuxer(string path) 488 { 489 videoChunkSizes = new List<int>(); 490 videoChunkOffsets = new List<long>(); 491 audioChunkSizes = new List<int>(); 492 audioChunkOffsets = new List<long>(); 493 494 foreach (var c in System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders()) 495 if (c.MimeType == "image/jpeg") { jpegCodec = c; break; } 496 jpegParams = new System.Drawing.Imaging.EncoderParameters(1); 497 jpegParams.Param[0] = new System.Drawing.Imaging.EncoderParameter( 498 System.Drawing.Imaging.Encoder.Quality, 90L); 499 500 prevFrameBuf = Marshal.AllocHGlobal(srcWidth * srcHeight * 4); 501 hasPrevFrame = false; 502 jpegStream = new MemoryStream(srcWidth * srcHeight); 503 504 fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 65536); 505 bw = new BinaryWriter(fileStream); 506 507 // ftyp box 508 long s = fileStream.Position; 509 WriteU32(0); WriteTag("ftyp"); WriteTag("isom"); 510 WriteU32(0x200); WriteTag("isom"); WriteTag("iso2"); WriteTag("mp41"); 511 PatchBoxSize(s); 512 513 // mdat box (size patched on close) 514 mdatStart = fileStream.Position; 515 WriteU32(0); WriteTag("mdat"); 516 517 Log("MJPEG muxer ready"); 518 } 519 520 #endregion 521 522 #region Write video frame 523 524 public unsafe void WriteVideoFrame(IntPtr bgr32, int byteLen) 525 { 526 switch (mode) 527 { 528 case Mode.H264: WriteFrameH264(bgr32); break; 529 case Mode.WMV: WriteFrameWmv(bgr32); break; 530 case Mode.MJPEG: WriteFrameMjpeg(bgr32); break; 531 } 532 } 533 534 private void WriteFrameWmv(IntPtr bgr32) 535 { 536 WriteMFSample(videoStreamIdx, bgr32, srcWidth * srcHeight * 4, ref videoTimestamp); 537 } 538 539 private unsafe void WriteFrameH264(IntPtr bgr32) 540 { 541 ConvertBgr32ToNv12(bgr32); 542 543 IMFMediaBuffer buf; 544 WinApi.MFCreateMemoryBuffer((uint)nv12BufSize, out buf); 545 IntPtr p; int mx, cl; 546 buf.Lock(out p, out mx, out cl); 547 CopyMemory(p, nv12Buf, nv12BufSize); 548 buf.Unlock(); 549 buf.SetCurrentLength(nv12BufSize); 550 551 IMFSample sample; 552 WinApi.MFCreateSample(out sample); 553 sample.AddBuffer(buf); 554 sample.SetSampleTime(videoTimestamp); 555 sample.SetSampleDuration(frameDuration); 556 557 int hr = encoder.ProcessInput(0, sample, 0); 558 Marshal.ReleaseComObject(buf); 559 Marshal.ReleaseComObject(sample); 560 561 if (hr == 0) DrainEncoder(); 562 videoTimestamp += frameDuration; 563 } 564 565 private unsafe void WriteFrameMjpeg(IntPtr bgr32) 566 { 567 int rawSize = srcWidth * srcHeight * 4; 568 569 // Frame dedup: reuse last JPEG if pixels unchanged 570 if (hasPrevFrame && AreFramesIdentical(bgr32, rawSize)) 571 { 572 videoChunkSizes.Add(lastJpegSize); 573 videoChunkOffsets.Add(lastJpegOffset); 574 dupFrameCount++; 575 return; 576 } 577 578 // Encode new JPEG 579 jpegStream.Position = 0; 580 jpegStream.SetLength(0); 581 using (var bmp = new System.Drawing.Bitmap(srcWidth, srcHeight, 582 srcWidth * 4, System.Drawing.Imaging.PixelFormat.Format32bppRgb, bgr32)) 583 { 584 bmp.Save(jpegStream, jpegCodec, jpegParams); 585 } 586 587 int len = (int)jpegStream.Length; 588 lastJpegOffset = fileStream.Position; 589 lastJpegSize = len; 590 bw.Write(jpegStream.GetBuffer(), 0, len); 591 592 videoChunkSizes.Add(len); 593 videoChunkOffsets.Add(lastJpegOffset); 594 uniqueFrameCount++; 595 596 CopyMemory(prevFrameBuf, bgr32, rawSize); 597 hasPrevFrame = true; 598 } 599 600 private unsafe bool AreFramesIdentical(IntPtr current, int byteLen) 601 { 602 long* a = (long*)current; 603 long* b = (long*)prevFrameBuf; 604 int count = byteLen / 8; 605 for (int i = 0; i < count; i++) 606 if (a[i] != b[i]) return false; 607 return true; 608 } 609 610 #endregion 611 612 #region Write audio 613 614 public void WriteAudioBlock(byte[] data, int offset, int length) 615 { 616 if (!hasAudio || length == 0) return; 617 618 if (mode == Mode.H264 || mode == Mode.WMV) 619 { 620 if (!hasMFAudio) return; 621 IMFMediaBuffer buf; 622 WinApi.MFCreateMemoryBuffer((uint)length, out buf); 623 IntPtr p; int mx, cl; 624 buf.Lock(out p, out mx, out cl); 625 Marshal.Copy(data, offset, p, length); 626 buf.Unlock(); 627 buf.SetCurrentLength(length); 628 629 IMFSample s; 630 WinApi.MFCreateSample(out s); 631 s.AddBuffer(buf); 632 s.SetSampleTime(audioTimestamp); 633 s.SetSampleDuration(frameDuration); 634 writer.WriteSample(audioStreamIdx, s); 635 audioTimestamp += frameDuration; 636 Marshal.ReleaseComObject(s); 637 Marshal.ReleaseComObject(buf); 638 } 639 else 640 { 641 long off = fileStream.Position; 642 bw.Write(data, offset, length); 643 audioChunkSizes.Add(length); 644 audioChunkOffsets.Add(off); 645 } 646 } 647 648 #endregion 649 650 #region Close / Dispose 651 652 public void Close() 653 { 654 if (closed) return; 655 closed = true; 656 657 switch (mode) 658 { 659 case Mode.H264: CloseH264(); break; 660 case Mode.WMV: CloseWmv(); break; 661 case Mode.MJPEG: CloseMjpeg(); break; 662 } 663 } 664 665 private void CloseWmv() 666 { 667 try { writer.Finalize_(); Log("WMV finalized"); } 668 catch (Exception e) { Log("WMV finalize err: " + e.Message); } 669 try { Marshal.ReleaseComObject(writer); } catch { } 670 MfStopOnce(); 671 } 672 673 private void CloseH264() 674 { 675 try 676 { 677 encoder.ProcessMessage(MFT_MSG_DRAIN, IntPtr.Zero); 678 DrainEncoder(); 679 } 680 catch (Exception ex) { Log("H264 drain err: " + ex.Message); } 681 682 try { writer.Finalize_(); Log("H264 finalized"); } 683 catch (Exception e) { Log("H264 finalize err: " + e.Message); } 684 685 try { Marshal.ReleaseComObject(encoder); } catch { } 686 try { Marshal.ReleaseComObject(writer); } catch { } 687 MfStopOnce(); 688 if (nv12Buf != IntPtr.Zero) { Marshal.FreeHGlobal(nv12Buf); nv12Buf = IntPtr.Zero; } 689 } 690 691 private void CloseMjpeg() 692 { 693 bw.Flush(); 694 695 // Patch mdat box size 696 long mdatEnd = fileStream.Position; 697 fileStream.Seek(mdatStart, SeekOrigin.Begin); 698 WriteU32((uint)(mdatEnd - mdatStart)); 699 fileStream.Seek(mdatEnd, SeekOrigin.Begin); 700 701 WriteMoov(); 702 bw.Flush(); bw.Close(); fileStream.Close(); 703 704 Log(string.Format("MJPEG closed: {0} frames ({1} unique, {2} dup)", 705 videoChunkSizes.Count, uniqueFrameCount, dupFrameCount)); 706 707 if (prevFrameBuf != IntPtr.Zero) { Marshal.FreeHGlobal(prevFrameBuf); prevFrameBuf = IntPtr.Zero; } 708 if (jpegStream != null) { jpegStream.Dispose(); jpegStream = null; } 709 } 710 711 public void Dispose() { Close(); } 712 713 #endregion 714 715 #region H.264 encoder helpers 716 717 private unsafe void ConvertBgr32ToNv12(IntPtr bgr32) 718 { 719 byte* src = (byte*)bgr32; 720 byte* yPlane = (byte*)nv12Buf; 721 byte* uvPlane = yPlane + encWidth * encHeight; 722 723 for (int row = 0; row < encHeight; row++) 724 { 725 byte* srcRow = src + row * srcWidth * 4; 726 byte* yRow = yPlane + row * encWidth; 727 bool evenRow = (row & 1) == 0; 728 byte* uvRow = evenRow ? (uvPlane + (row >> 1) * encWidth) : null; 729 730 for (int col = 0; col < encWidth; col++) 731 { 732 byte* px = srcRow + col * 4; 733 int b = px[0], g = px[1], r = px[2]; 734 735 // BT.601 RGB→YUV 736 int y = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16; 737 yRow[col] = (byte)(y < 16 ? 16 : y > 235 ? 235 : y); 738 739 if (evenRow && (col & 1) == 0 && uvRow != null) 740 { 741 int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128; 742 int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128; 743 uvRow[col] = (byte)(u < 16 ? 16 : u > 240 ? 240 : u); 744 uvRow[col + 1] = (byte)(v < 16 ? 16 : v > 240 ? 240 : v); 745 } 746 } 747 } 748 } 749 750 private void DrainEncoder() 751 { 752 while (true) 753 { 754 var outBuf = new MFT_OUTPUT_DATA_BUFFER[1]; 755 outBuf[0].pSample = IntPtr.Zero; 756 outBuf[0].pEvents = IntPtr.Zero; 757 758 IMFSample allocSample = null; 759 IMFMediaBuffer allocBuf = null; 760 761 if (!encoderProvidesSamples) 762 { 763 WinApi.MFCreateMemoryBuffer(encoderOutputBufSize, out allocBuf); 764 WinApi.MFCreateSample(out allocSample); 765 allocSample.AddBuffer(allocBuf); 766 outBuf[0].pSample = Marshal.GetIUnknownForObject(allocSample); 767 } 768 769 uint status; 770 int hr = encoder.ProcessOutput(0, 1, outBuf, out status); 771 772 if (hr == MF_E_NEED_MORE_INPUT || hr != 0) 773 { 774 if (!encoderProvidesSamples) 775 { 776 if (outBuf[0].pSample != IntPtr.Zero) Marshal.Release(outBuf[0].pSample); 777 if (allocBuf != null) Marshal.ReleaseComObject(allocBuf); 778 if (allocSample != null) Marshal.ReleaseComObject(allocSample); 779 } 780 break; 781 } 782 783 IMFSample encoded = encoderProvidesSamples 784 ? (IMFSample)Marshal.GetObjectForIUnknown(outBuf[0].pSample) 785 : allocSample; 786 787 try { writer.WriteSample(videoStreamIdx, encoded); } catch { } 788 789 if (encoderProvidesSamples) 790 { 791 Marshal.ReleaseComObject(encoded); 792 if (outBuf[0].pSample != IntPtr.Zero) Marshal.Release(outBuf[0].pSample); 793 } 794 else 795 { 796 Marshal.Release(outBuf[0].pSample); 797 Marshal.ReleaseComObject(allocBuf); 798 Marshal.ReleaseComObject(allocSample); 799 } 800 if (outBuf[0].pEvents != IntPtr.Zero) Marshal.Release(outBuf[0].pEvents); 801 } 802 } 803 804 #endregion 805 806 #region MP4 muxer (MJPEG) 807 808 private void WriteMoov() 809 { 810 long moovStart = fileStream.Position; 811 WriteU32(0); WriteTag("moov"); 812 WriteMvhd(); 813 WriteVideoTrack(); 814 if (hasAudio && audioChunkSizes != null && audioChunkSizes.Count > 0) 815 WriteAudioTrack(); 816 PatchBoxSize(moovStart); 817 } 818 819 private void WriteMvhd() 820 { 821 long s = fileStream.Position; WriteU32(0); WriteTag("mvhd"); 822 WriteU32(0); WriteU32(0); WriteU32(0); 823 WriteU32(1000); // timescale 824 WriteU32((uint)(videoChunkSizes.Count * 1000 / fps)); // duration 825 WriteU32(0x00010000); // rate 1.0 826 WriteU16(0x0100); // volume 1.0 827 bw.Write(new byte[10]); // reserved 828 // identity matrix 829 WriteU32(0x00010000); WriteU32(0); WriteU32(0); 830 WriteU32(0); WriteU32(0x00010000); WriteU32(0); 831 WriteU32(0); WriteU32(0); WriteU32(0x40000000); 832 bw.Write(new byte[24]); // pre-defined 833 WriteU32(hasAudio && audioChunkSizes.Count > 0 ? 3u : 2u); // next_track_ID 834 PatchBoxSize(s); 835 } 836 837 private void WriteVideoTrack() 838 { 839 int frameCount = videoChunkSizes.Count; 840 uint durationMs = (uint)(frameCount * 1000 / fps); 841 842 long trakStart = fileStream.Position; WriteU32(0); WriteTag("trak"); 843 844 // tkhd 845 long s = fileStream.Position; WriteU32(0); WriteTag("tkhd"); 846 WriteU32(3); WriteU32(0); WriteU32(0); WriteU32(1); WriteU32(0); 847 WriteU32(durationMs); bw.Write(new byte[8]); 848 WriteU16(0); WriteU16(0); WriteU16(0); WriteU16(0); 849 WriteU32(0x00010000); WriteU32(0); WriteU32(0); 850 WriteU32(0); WriteU32(0x00010000); WriteU32(0); 851 WriteU32(0); WriteU32(0); WriteU32(0x40000000); 852 WriteU32((uint)srcWidth << 16); WriteU32((uint)srcHeight << 16); 853 PatchBoxSize(s); 854 855 // mdia 856 long mdiaStart = fileStream.Position; WriteU32(0); WriteTag("mdia"); 857 858 // mdhd 859 s = fileStream.Position; WriteU32(0); WriteTag("mdhd"); 860 WriteU32(0); WriteU32(0); WriteU32(0); 861 WriteU32((uint)fps); // timescale = fps 862 WriteU32((uint)frameCount); // duration in timescale units 863 WriteU32(0x55C40000); // language undetermined 864 PatchBoxSize(s); 865 866 // hdlr 867 s = fileStream.Position; WriteU32(0); WriteTag("hdlr"); 868 WriteU32(0); WriteU32(0); WriteTag("vide"); bw.Write(new byte[12]); 869 bw.Write(Encoding.UTF8.GetBytes("VideoHandler\0")); 870 PatchBoxSize(s); 871 872 // minf 873 long minfStart = fileStream.Position; WriteU32(0); WriteTag("minf"); 874 875 // vmhd 876 s = fileStream.Position; WriteU32(0); WriteTag("vmhd"); 877 WriteU32(1); WriteU16(0); bw.Write(new byte[6]); 878 PatchBoxSize(s); 879 880 WriteDinf(); 881 WriteVideoStbl(frameCount); 882 883 PatchBoxSize(minfStart); 884 PatchBoxSize(mdiaStart); 885 PatchBoxSize(trakStart); 886 } 887 888 private void WriteVideoStbl(int frameCount) 889 { 890 long stblStart = fileStream.Position; WriteU32(0); WriteTag("stbl"); 891 892 // stsd (JPEG sample entry) 893 long s = fileStream.Position; WriteU32(0); WriteTag("stsd"); WriteU32(0); WriteU32(1); 894 long entryStart = fileStream.Position; WriteU32(0); WriteTag("jpeg"); 895 bw.Write(new byte[6]); WriteU16(1); // data_ref_index 896 WriteU16(0); WriteU16(0); bw.Write(new byte[12]); 897 WriteU16((ushort)srcWidth); WriteU16((ushort)srcHeight); 898 WriteU32(0x00480000); WriteU32(0x00480000); // 72 dpi 899 WriteU32(0); WriteU16(1); bw.Write(new byte[32]); 900 WriteU16(0x18); WriteU16(0xFFFF); 901 PatchBoxSize(entryStart); PatchBoxSize(s); 902 903 // stts (constant 1 tick per frame) 904 s = fileStream.Position; WriteU32(0); WriteTag("stts"); WriteU32(0); 905 WriteU32(1); WriteU32((uint)frameCount); WriteU32(1); 906 PatchBoxSize(s); 907 908 // stss (all keyframes in MJPEG) 909 s = fileStream.Position; WriteU32(0); WriteTag("stss"); WriteU32(0); 910 WriteU32((uint)frameCount); 911 for (int i = 0; i < frameCount; i++) WriteU32((uint)(i + 1)); 912 PatchBoxSize(s); 913 914 // stsz (per-frame sizes) 915 s = fileStream.Position; WriteU32(0); WriteTag("stsz"); WriteU32(0); WriteU32(0); 916 WriteU32((uint)frameCount); 917 for (int i = 0; i < frameCount; i++) WriteU32((uint)videoChunkSizes[i]); 918 PatchBoxSize(s); 919 920 // stsc 921 s = fileStream.Position; WriteU32(0); WriteTag("stsc"); WriteU32(0); 922 WriteU32(1); WriteU32(1); WriteU32(1); WriteU32(1); 923 PatchBoxSize(s); 924 925 // co64 (64-bit chunk offsets) 926 s = fileStream.Position; WriteU32(0); WriteTag("co64"); WriteU32(0); 927 WriteU32((uint)videoChunkOffsets.Count); 928 for (int i = 0; i < videoChunkOffsets.Count; i++) WriteU64((ulong)videoChunkOffsets[i]); 929 PatchBoxSize(s); 930 931 PatchBoxSize(stblStart); 932 } 933 934 private void WriteAudioTrack() 935 { 936 int totalBytes = 0; 937 for (int i = 0; i < audioChunkSizes.Count; i++) totalBytes += audioChunkSizes[i]; 938 int blockAlign = audioCh * (audioBps / 8); 939 uint totalSamples = blockAlign > 0 ? (uint)(totalBytes / blockAlign) : 0; 940 uint durationMs = audioRate > 0 ? (uint)(totalSamples * 1000 / audioRate) : 0; 941 942 long trakStart = fileStream.Position; WriteU32(0); WriteTag("trak"); 943 944 // tkhd 945 long s = fileStream.Position; WriteU32(0); WriteTag("tkhd"); 946 WriteU32(3); WriteU32(0); WriteU32(0); WriteU32(2); WriteU32(0); 947 WriteU32(durationMs); bw.Write(new byte[8]); 948 WriteU16(0); WriteU16(0); WriteU16(0x0100); WriteU16(0); 949 WriteU32(0x00010000); WriteU32(0); WriteU32(0); 950 WriteU32(0); WriteU32(0x00010000); WriteU32(0); 951 WriteU32(0); WriteU32(0); WriteU32(0x40000000); 952 WriteU32(0); WriteU32(0); 953 PatchBoxSize(s); 954 955 // mdia 956 long mdiaStart = fileStream.Position; WriteU32(0); WriteTag("mdia"); 957 958 s = fileStream.Position; WriteU32(0); WriteTag("mdhd"); 959 WriteU32(0); WriteU32(0); WriteU32(0); 960 WriteU32((uint)audioRate); WriteU32(totalSamples); 961 WriteU32(0x55C40000); 962 PatchBoxSize(s); 963 964 s = fileStream.Position; WriteU32(0); WriteTag("hdlr"); 965 WriteU32(0); WriteU32(0); WriteTag("soun"); bw.Write(new byte[12]); 966 bw.Write(Encoding.UTF8.GetBytes("SoundHandler\0")); 967 PatchBoxSize(s); 968 969 // minf 970 long minfStart = fileStream.Position; WriteU32(0); WriteTag("minf"); 971 972 s = fileStream.Position; WriteU32(0); WriteTag("smhd"); 973 WriteU32(0); WriteU16(0); WriteU16(0); 974 PatchBoxSize(s); 975 976 WriteDinf(); 977 WriteAudioStbl(blockAlign); 978 979 PatchBoxSize(minfStart); 980 PatchBoxSize(mdiaStart); 981 PatchBoxSize(trakStart); 982 } 983 984 private void WriteAudioStbl(int blockAlign) 985 { 986 long stblStart = fileStream.Position; WriteU32(0); WriteTag("stbl"); 987 988 // stsd (PCM 'sowt' sample entry) 989 long s = fileStream.Position; WriteU32(0); WriteTag("stsd"); WriteU32(0); WriteU32(1); 990 long entryStart = fileStream.Position; WriteU32(0); WriteTag("sowt"); 991 bw.Write(new byte[6]); WriteU16(1); 992 WriteU16(0); WriteU16(0); WriteU32(0); 993 WriteU16((ushort)audioCh); WriteU16((ushort)audioBps); 994 WriteU16(0); WriteU16(0); WriteU32((uint)audioRate << 16); 995 PatchBoxSize(entryStart); PatchBoxSize(s); 996 997 // stts 998 s = fileStream.Position; WriteU32(0); WriteTag("stts"); WriteU32(0); 999 WriteU32((uint)audioChunkSizes.Count); 1000 for (int i = 0; i < audioChunkSizes.Count; i++) 1001 { 1002 uint samples = blockAlign > 0 ? (uint)(audioChunkSizes[i] / blockAlign) : 0; 1003 WriteU32(1); WriteU32(samples); 1004 } 1005 PatchBoxSize(s); 1006 1007 // stsz 1008 s = fileStream.Position; WriteU32(0); WriteTag("stsz"); WriteU32(0); WriteU32(0); 1009 WriteU32((uint)audioChunkSizes.Count); 1010 for (int i = 0; i < audioChunkSizes.Count; i++) WriteU32((uint)audioChunkSizes[i]); 1011 PatchBoxSize(s); 1012 1013 // stsc 1014 s = fileStream.Position; WriteU32(0); WriteTag("stsc"); WriteU32(0); 1015 WriteU32(1); WriteU32(1); WriteU32(1); WriteU32(1); 1016 PatchBoxSize(s); 1017 1018 // co64 1019 s = fileStream.Position; WriteU32(0); WriteTag("co64"); WriteU32(0); 1020 WriteU32((uint)audioChunkOffsets.Count); 1021 for (int i = 0; i < audioChunkOffsets.Count; i++) WriteU64((ulong)audioChunkOffsets[i]); 1022 PatchBoxSize(s); 1023 1024 PatchBoxSize(stblStart); 1025 } 1026 1027 private void WriteDinf() 1028 { 1029 long diStart = fileStream.Position; WriteU32(0); WriteTag("dinf"); 1030 long drStart = fileStream.Position; WriteU32(0); WriteTag("dref"); 1031 WriteU32(0); WriteU32(1); 1032 WriteU32(12); WriteTag("url "); WriteU32(1); 1033 PatchBoxSize(drStart); PatchBoxSize(diStart); 1034 } 1035 1036 #endregion 1037 1038 #region Binary helpers (big-endian MP4) 1039 1040 private void WriteU32(uint v) 1041 { 1042 bw.Write((byte)(v >> 24)); bw.Write((byte)(v >> 16)); 1043 bw.Write((byte)(v >> 8)); bw.Write((byte)v); 1044 } 1045 private void WriteU16(ushort v) { bw.Write((byte)(v >> 8)); bw.Write((byte)v); } 1046 private void WriteU64(ulong v) { WriteU32((uint)(v >> 32)); WriteU32((uint)v); } 1047 private void WriteTag(string t) { bw.Write((byte)t[0]); bw.Write((byte)t[1]); bw.Write((byte)t[2]); bw.Write((byte)t[3]); } 1048 1049 private void PatchBoxSize(long boxStart) 1050 { 1051 long end = fileStream.Position; 1052 fileStream.Seek(boxStart, SeekOrigin.Begin); 1053 WriteU32((uint)(end - boxStart)); 1054 fileStream.Seek(end, SeekOrigin.Begin); 1055 } 1056 1057 #endregion 1058 } 1059}