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}