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

AviWriter.cs

345 строк · 13,143 байт · модуль Recording
  1using System;
  2using System.Drawing;
  3using System.Drawing.Imaging;
  4using System.IO;
  5using System.Collections.Generic;
  6
  7namespace WindowCapture.Recording
  8{
  9    /// <summary>
 10    /// AVI writer supporting MJPEG video + PCM audio streams.
 11    /// Pure managed code — no Media Foundation, no COM codecs.
 12    /// Supports OpenDML extensions for files >2GB.
 13    /// </summary>
 14    public class AviWriter : IDisposable
 15    {
 16        private BinaryWriter writer;
 17        private FileStream stream;
 18        private int width, height, fps;
 19        private int videoFrameCount;
 20        private int audioSampleCount; // total audio samples written
 21        private bool hasAudio;
 22        private int audioChannels, audioSampleRate, audioBitsPerSample;
 23        private int audioBlockAlign;
 24
 25        // File structure offsets for patching on close
 26        private long riffSizeOffset;
 27        private long moviListSizeOffset;
 28        private long moviListStart;
 29        private long avihTotalFramesOffset;
 30        private long videoStrhLengthOffset;
 31        private long audioStrhLengthOffset;
 32
 33        // Index
 34        private List<AviIndexEntry> index = new List<AviIndexEntry>();
 35
 36        // JPEG encoder
 37        private ImageCodecInfo jpegCodec;
 38        private EncoderParameters jpegParams;
 39        private bool disposed;
 40
 41        private struct AviIndexEntry
 42        {
 43            public int ChunkId; // FourCC: '00dc' for video, '01wb' for audio
 44            public int Flags;
 45            public int Offset;
 46            public int Size;
 47        }
 48
 49        public AviWriter(string filePath, int width, int height, int fps, int jpegQuality,
 50                          int audioSampleRate = 0, int audioChannels = 0, int audioBitsPerSample = 0)
 51        {
 52            this.width = width;
 53            this.height = height;
 54            this.fps = fps;
 55
 56            this.hasAudio = audioSampleRate > 0 && audioChannels > 0;
 57            this.audioSampleRate = audioSampleRate;
 58            this.audioChannels = audioChannels;
 59            this.audioBitsPerSample = audioBitsPerSample > 0 ? audioBitsPerSample : 16;
 60            this.audioBlockAlign = this.audioChannels * (this.audioBitsPerSample / 8);
 61
 62            // Find JPEG codec
 63            foreach (var codec in ImageCodecInfo.GetImageEncoders())
 64            {
 65                if (codec.FormatID == ImageFormat.Jpeg.Guid)
 66                {
 67                    jpegCodec = codec;
 68                    break;
 69                }
 70            }
 71            jpegParams = new EncoderParameters(1);
 72            jpegParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)jpegQuality);
 73
 74            stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536);
 75            writer = new BinaryWriter(stream);
 76
 77            WriteHeaders();
 78        }
 79
 80        private void WriteHeaders()
 81        {
 82            int streamCount = hasAudio ? 2 : 1;
 83
 84            // ===== RIFF 'AVI ' =====
 85            Write4CC("RIFF");
 86            riffSizeOffset = stream.Position;
 87            writer.Write(0); // placeholder
 88            Write4CC("AVI ");
 89
 90            // ===== LIST 'hdrl' =====
 91            Write4CC("LIST");
 92            long hdrlSizePos = stream.Position;
 93            writer.Write(0);
 94            long hdrlStart = stream.Position;
 95            Write4CC("hdrl");
 96
 97            // ----- avih (MainAVIHeader) -----
 98            Write4CC("avih");
 99            writer.Write(56); // chunk size
100            writer.Write(1000000 / fps);    // dwMicroSecPerFrame
101            writer.Write(0);                // dwMaxBytesPerSec
102            writer.Write(0);                // dwPaddingGranularity
103            writer.Write(0x10);             // dwFlags: AVIF_HASINDEX
104            avihTotalFramesOffset = stream.Position;
105            writer.Write(0);                // dwTotalFrames (patch later)
106            writer.Write(0);                // dwInitialFrames
107            writer.Write(streamCount);      // dwStreams
108            writer.Write(width * height * 3); // dwSuggestedBufferSize
109            writer.Write(width);
110            writer.Write(height);
111            writer.Write(0); writer.Write(0); writer.Write(0); writer.Write(0); // reserved
112
113            // ----- Video stream: LIST 'strl' -----
114            Write4CC("LIST");
115            long vstrlSizePos = stream.Position;
116            writer.Write(0);
117            long vstrlStart = stream.Position;
118            Write4CC("strl");
119
120            // strh (video stream header)
121            Write4CC("strh");
122            writer.Write(56);
123            Write4CC("vids");               // fccType
124            Write4CC("MJPG");               // fccHandler
125            writer.Write(0);                // dwFlags
126            writer.Write((short)0);         // wPriority
127            writer.Write((short)0);         // wLanguage
128            writer.Write(0);                // dwInitialFrames
129            writer.Write(1);                // dwScale
130            writer.Write(fps);              // dwRate
131            writer.Write(0);                // dwStart
132            videoStrhLengthOffset = stream.Position;
133            writer.Write(0);                // dwLength (patch later)
134            writer.Write(width * height * 3); // dwSuggestedBufferSize
135            writer.Write(-1);               // dwQuality
136            writer.Write(0);                // dwSampleSize
137            writer.Write((short)0); writer.Write((short)0);
138            writer.Write((short)width); writer.Write((short)height);
139
140            // strf (BITMAPINFOHEADER)
141            Write4CC("strf");
142            writer.Write(40);
143            writer.Write(40);               // biSize
144            writer.Write(width);
145            writer.Write(height);
146            writer.Write((short)1);         // biPlanes
147            writer.Write((short)24);        // biBitCount
148            Write4CC("MJPG");               // biCompression
149            writer.Write(width * height * 3);
150            writer.Write(0); writer.Write(0); writer.Write(0); writer.Write(0);
151
152            // Patch video strl size
153            PatchSize(vstrlSizePos, vstrlStart);
154
155            // ----- Audio stream: LIST 'strl' (if audio) -----
156            if (hasAudio)
157            {
158                Write4CC("LIST");
159                long astrlSizePos = stream.Position;
160                writer.Write(0);
161                long astrlStart = stream.Position;
162                Write4CC("strl");
163
164                // strh (audio stream header)
165                Write4CC("strh");
166                writer.Write(56);
167                Write4CC("auds");               // fccType
168                writer.Write(0);                // fccHandler (0 for PCM)
169                writer.Write(0);                // dwFlags
170                writer.Write((short)0);         // wPriority
171                writer.Write((short)0);         // wLanguage
172                writer.Write(0);                // dwInitialFrames
173                writer.Write(audioBlockAlign);  // dwScale = block align
174                writer.Write(audioSampleRate * audioBlockAlign); // dwRate = bytes/sec
175                writer.Write(0);                // dwStart
176                audioStrhLengthOffset = stream.Position;
177                writer.Write(0);                // dwLength (patch: total samples)
178                writer.Write(audioSampleRate);  // dwSuggestedBufferSize
179                writer.Write(-1);               // dwQuality
180                writer.Write(audioBlockAlign);  // dwSampleSize
181                writer.Write((short)0); writer.Write((short)0);
182                writer.Write((short)0); writer.Write((short)0);
183
184                // strf (WAVEFORMATEX)
185                Write4CC("strf");
186                writer.Write(18);               // chunk size
187                writer.Write((short)1);         // wFormatTag: WAVE_FORMAT_PCM
188                writer.Write((short)audioChannels);
189                writer.Write(audioSampleRate);
190                writer.Write(audioSampleRate * audioBlockAlign); // nAvgBytesPerSec
191                writer.Write((short)audioBlockAlign);
192                writer.Write((short)audioBitsPerSample);
193                writer.Write((short)0);         // cbSize
194
195                PatchSize(astrlSizePos, astrlStart);
196            }
197
198            // Patch hdrl size
199            PatchSize(hdrlSizePos, hdrlStart);
200
201            // ===== LIST 'movi' =====
202            Write4CC("LIST");
203            moviListSizeOffset = stream.Position;
204            writer.Write(0);
205            moviListStart = stream.Position;
206            Write4CC("movi");
207        }
208
209        /// <summary>Write a video frame (JPEG-compressed)</summary>
210        public void WriteVideoFrame(Bitmap bitmap)
211        {
212            if (disposed) throw new ObjectDisposedException("AviWriter");
213            if (sizeLimitReached) return;
214
215            byte[] jpegData;
216            using (var ms = new MemoryStream())
217            {
218                bitmap.Save(ms, jpegCodec, jpegParams);
219                jpegData = ms.ToArray();
220            }
221
222            WriteChunk(FourCC("00dc"), jpegData, 0, jpegData.Length, 0x10); // AVIIF_KEYFRAME
223            videoFrameCount++;
224        }
225
226        /// <summary>Write a video frame from raw JPEG bytes</summary>
227        public void WriteVideoFrame(byte[] jpegData, int offset, int count)
228        {
229            if (disposed) throw new ObjectDisposedException("AviWriter");
230            if (sizeLimitReached) return;
231            WriteChunk(FourCC("00dc"), jpegData, offset, count, 0x10);
232            videoFrameCount++;
233        }
234
235        /// <summary>Write audio data (PCM samples)</summary>
236        public void WriteAudioBlock(byte[] audioData, int offset, int count)
237        {
238            if (disposed || !hasAudio) return;
239            if (sizeLimitReached) return;
240            WriteChunk(FourCC("01wb"), audioData, offset, count, 0x10);
241            audioSampleCount += count / audioBlockAlign;
242        }
243
244        // AVI 'idx1' offsets and the RIFF/LIST sizes are 32-bit, and OpenDML (>2GB) is not
245        // implemented. Stop cleanly before the offsets would overflow rather than silently
246        // corrupting the file; Close() then finalizes a valid AVI of what was written so far.
247        private const long MaxAviBytes = 1900L * 1024 * 1024;
248        private bool sizeLimitReached;
249
250        private void WriteChunk(int chunkId, byte[] data, int offset, int count, int flags)
251        {
252            if (sizeLimitReached) return;
253            if ((stream.Position - moviListStart) + count + 16 > MaxAviBytes) { sizeLimitReached = true; return; }
254
255            int chunkOffset = (int)(stream.Position - moviListStart);
256
257            writer.Write(chunkId);
258            writer.Write(count);
259            writer.Write(data, offset, count);
260
261            // Word-align
262            if (count % 2 != 0)
263                writer.Write((byte)0);
264
265            index.Add(new AviIndexEntry
266            {
267                ChunkId = chunkId,
268                Flags = flags,
269                Offset = chunkOffset,
270                Size = count
271            });
272        }
273
274        public void Close()
275        {
276            if (disposed) return;
277            disposed = true;
278
279            // Patch movi LIST size
280            long moviEnd = stream.Position;
281            PatchInt(moviListSizeOffset, (int)(moviEnd - moviListStart));
282
283            // Write idx1 index
284            Write4CC("idx1");
285            writer.Write(index.Count * 16);
286            for (int i = 0; i < index.Count; i++)
287            {
288                writer.Write(index[i].ChunkId);
289                writer.Write(index[i].Flags);
290                writer.Write(index[i].Offset);
291                writer.Write(index[i].Size);
292            }
293
294            // Patch RIFF size
295            long fileEnd = stream.Position;
296            PatchInt(riffSizeOffset, (int)(fileEnd - riffSizeOffset - 4));
297
298            // Patch frame counts
299            PatchInt(avihTotalFramesOffset, videoFrameCount);
300            PatchInt(videoStrhLengthOffset, videoFrameCount);
301            if (hasAudio)
302                PatchInt(audioStrhLengthOffset, audioSampleCount);
303
304            writer.Flush();
305            writer.Close();
306            stream.Close();
307        }
308
309        // -- Helpers --
310
311        private void PatchInt(long offset, int value)
312        {
313            long pos = stream.Position;
314            stream.Seek(offset, SeekOrigin.Begin);
315            writer.Write(value);
316            stream.Seek(pos, SeekOrigin.Begin);
317        }
318
319        private void PatchSize(long sizeOffset, long dataStart)
320        {
321            long end = stream.Position;
322            stream.Seek(sizeOffset, SeekOrigin.Begin);
323            writer.Write((int)(end - dataStart));
324            stream.Seek(end, SeekOrigin.Begin);
325        }
326
327        private void Write4CC(string s)
328        {
329            writer.Write((byte)s[0]);
330            writer.Write((byte)s[1]);
331            writer.Write((byte)s[2]);
332            writer.Write((byte)s[3]);
333        }
334
335        private static int FourCC(string s)
336        {
337            return s[0] | (s[1] << 8) | (s[2] << 16) | (s[3] << 24);
338        }
339
340        public void Dispose()
341        {
342            Close();
343        }
344    }
345}