It is 2025, we are awaiting Windows 12, and still we have applications that use Video for Windows API and should be maintained because they "have worked admirably for many years". So I was tasked to write a VfW codec for a novel video compression format.
To help developers like me to master this relict technology, Microsoft supplies a full reference documentation on Video for Windows API. The section Using the Video for Windows, subsection Compressing Data gives a detailed account on how to compress the input data but stops short of teaching how to write compressed data to the avi file. To rule out possible errors of my VfW codec, I tried to make an AVI file with RLE compressed data, but equally failed: in every frame, the count of bytes written by AVIStreamWrite (returned with the plBytesWritten parameter) was a fixed value for all frames, this value greater than dwFrameSize parameter returned with a ICCompress call, which I pass through a AVIStreamWrite call with the cbBuffer parameter. An Internet search on this problem immediately presented me with a reference to the SO post Is it possible to encode using the MRLE codec on Video for Windows under Windows 8? by David Heffernan. This post immediately solved my problem:
We do still need to create the compressed stream, but we no longer write to it. Instead we write RLE8 encoded data to the raw stream.
As this SO question-and-answer stops short of writing a real RLE8 encoder (`Obviously in a real application, you'd need to write a real RLE8 encoder, but this proves the point`), and being grateful for this helpful QA, I post a code excerpt that does exactly use a real RLE8 encoder
unsigned char* bits = new unsigned char[bmi->biSizeImage];
LPVOID lpInput = (LPVOID)bits;
HRESULT hr;
for (int frame = 0; frame < nframes; frame++)
{
for (int i = 0; i < bmi->biSizeImage; ++i)
bits[i] = (frame + 1) * ((i + 5) / 5);
ICCompress(hIC, 0, lpbiOut, lpOutput, lpbiIn, lpInput,
&dwCkID, &dwCompFlags, frame, bmi->biSizeImage, dwQuality, NULL, NULL);
hr = AVIStreamWrite(pStream, frame, 1, lpOutput, lpbiOut->biSizeImage,
AVIIF_KEYFRAME, &lSamplesWritten, &lBytesWritten);
if (hr != S_OK)
{
std::cout << "AVIStreamWrite failed" << std::endl;
return 1;
}
}
I'm going to replace the comment line `// Write compressed data to the AVI file` of the Using the Video for Windows' subsection Compressing Data with this code sample as soon as possible. For completeness, here I attach the code sample of how to write compressed data to the AVI file:
// runlength_encoding.cpp : This file contains the 'main' function.
// Program execution begins and ends there.
// based on learn.microsoft.com articles on Using the Video Compression Manager
// and SO post https://stackoverflow.com/questions/22765194/
// also see
// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
// why bmi size should be augmented by the color table size
// `However, some legacy components might assume that a color table is present.
// `Therefore, if you are allocating
// `a BITMAPINFOHEADER structure, it is recommended to allocate space for a color table
// `when the bit depth is 8 bpp or less, even if the color table is not used.`
//
#include <Windows.h>
#include <vfw.h>
#include <stdlib.h>
#include <iostream>
#pragma comment(lib, "vfw32.lib")
int main()
{
RECT frame = { 0, 0, 64, 8 };
int nframes = 10;
const char* filename = "rlenc.avi";
FILE* f;
errno_t err = fopen_s(&f, filename, "wb");
if (err)
{
printf("couldn't open file for write\n");
return 0;
}
fclose(f);
AVIFileInit();
IAVIFile* pFile;
if (AVIFileOpenA(&pFile, filename, OF_CREATE | OF_WRITE, NULL) != 0)
{
std::cout << "AVIFileOpen failed" << std::endl;
return 1;
}
AVISTREAMINFO si = { 0 };
si.fccType = streamtypeVIDEO;
si.fccHandler = mmioFOURCC('M', 'R', 'L', 'E');
si.dwScale = 1;
si.dwRate = 15;
si.dwQuality = (DWORD)-1;
si.rcFrame = frame;
IAVIStream* pStream;
if (AVIFileCreateStream(pFile, &pStream, &si) != 0)
{
std::cout << "AVIFileCreateStream failed" << std::endl;
return 1;
}
AVICOMPRESSOPTIONS co = { 0 };
co.fccType = si.fccType;
co.fccHandler = si.fccHandler;
co.dwQuality = si.dwQuality;
IAVIStream* pCompressedStream;
if (AVIMakeCompressedStream(&pCompressedStream, pStream, &co, NULL) != 0)
{
std::cout << "AVIMakeCompressedStream failed" << std::endl;
return 1;
}
BITMAPINFOHEADER bihIn, bihOut;
HIC hIC;
bihIn.biSize = bihOut.biSize = sizeof(BITMAPINFOHEADER);
bihIn.biWidth = bihOut.biWidth = si.rcFrame.right;
bihIn.biHeight = bihOut.biHeight = si.rcFrame.bottom;
bihIn.biPlanes = bihOut.biPlanes = 1;
bihIn.biCompression = BI_RGB; // standard RGB bitmap for input
bihOut.biCompression = BI_RLE8; // 8-bit RLE for output format
bihIn.biBitCount = bihOut.biBitCount = 8; // 8 bits-per-pixel format
bihIn.biSizeImage = bihIn.biWidth * bihIn.biHeight;
bihOut.biSizeImage = 0;
bihIn.biXPelsPerMeter = bihIn.biYPelsPerMeter =
bihOut.biXPelsPerMeter = bihOut.biYPelsPerMeter = 0;
bihIn.biClrUsed = bihIn.biClrImportant =
bihOut.biClrUsed = bihOut.biClrImportant = 256;
hIC = ICLocate(ICTYPE_VIDEO, 0L,
(LPBITMAPINFOHEADER)&bihIn,
(LPBITMAPINFOHEADER)&bihOut, ICMODE_COMPRESS);
ICINFO ICInfo;
ICGetInfo(hIC, &ICInfo, sizeof(ICInfo));
DWORD dwKeyFrameRate, dwQuality;
dwKeyFrameRate = ICGetDefaultKeyFrameRate(hIC);
dwQuality = ICGetDefaultQuality(hIC);
LPBITMAPINFOHEADER lpbiIn, lpbiOut;
lpbiIn = &bihIn;
DWORD dwFormatSize = ICCompressGetFormatSize(hIC, lpbiIn);
HGLOBAL h = GlobalAlloc(GHND, dwFormatSize);
lpbiOut = (LPBITMAPINFOHEADER)GlobalLock(h);
ICCompressGetFormat(hIC, lpbiIn, lpbiOut);
LPVOID lpOutput = 0;
DWORD dwCompressBufferSize = 0;
if (ICCompressQuery(hIC, lpbiIn, lpbiOut) == ICERR_OK)
{
// Find the worst-case buffer size.
dwCompressBufferSize = ICCompressGetSize(hIC, lpbiIn, lpbiOut);
// Allocate a buffer and get lpOutput to point to it.
h = GlobalAlloc(GHND, dwCompressBufferSize);
lpOutput = (LPVOID)GlobalLock(h);
}
DWORD dwCkID;
DWORD dwCompFlags = AVIIF_KEYFRAME;
LONG lNumFrames = 15, lFrameNum = 0;
LONG lSamplesWritten = 0;
LONG lBytesWritten = 0;
size_t bmiSize = sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD);
BITMAPINFOHEADER* bmi = (BITMAPINFOHEADER*)malloc(bmiSize);
ZeroMemory(bmi, bmiSize);
bmi->biSize = sizeof(BITMAPINFOHEADER);
bmi->biWidth = si.rcFrame.right;
bmi->biHeight = si.rcFrame.bottom;
bmi->biPlanes = 1;
bmi->biBitCount = 8;
bmi->biCompression = BI_RGB;
bmi->biSizeImage = bmi->biWidth * bmi->biHeight;
if (AVIStreamSetFormat(pCompressedStream, 0, bmi, bmiSize) != 0)
{
std::cout << "AVIStreamSetFormat failed" << std::endl;
return 1;
}
unsigned char* bits = new unsigned char[bmi->biSizeImage];
LPVOID lpInput = (LPVOID)bits;
HRESULT hr;
for (int frame = 0; frame < nframes; frame++)
{
for (int i = 0; i < bmi->biSizeImage; ++i)
bits[i] = (frame + 1) * ((i + 5) / 5);
ICCompress(hIC, 0, lpbiOut, lpOutput, lpbiIn, lpInput,
&dwCkID, &dwCompFlags, frame, bmi->biSizeImage, dwQuality, NULL, NULL);
hr = AVIStreamWrite(pStream, frame, 1, lpOutput, lpbiOut->biSizeImage,
AVIIF_KEYFRAME, &lSamplesWritten, &lBytesWritten);
if (hr != S_OK)
{
std::cout << "AVIStreamWrite failed" << std::endl;
return 1;
}
}
if (AVIStreamRelease(pCompressedStream) != 0 || AVIStreamRelease(pStream) != 0)
{
std::cout << "AVIStreamRelease failed" << std::endl;
return 1;
}
if (AVIFileRelease(pFile) != 0)
{
std::cout << "AVIFileRelease failed" << std::endl;
return 1;
}
std::cout << "Succeeded" << std::endl;
return 0;
}