windowcapture
исходный код / Recording/Mp4Writer.cs

Mp4Writer.cs

1059 строк · 40,739 байт · модуль Recording
   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}