So, I decided to do things a little different. Instead of calculating the frames per second, I calculte how long each frame should be displayed. AVIStreamSampleToTime() converts a position in the animation to how many milliseconds it would take to get to that position. So we calculate how many milliseconds the entire video is by grabbing the time (in milliseconds) of the last frame (lastframe). We then divide the result by the total number of frames in the animation (lastframe). This gives us the amount of time each frame is displayed for in milliseconds. We store the result in mpf (milliseconds per frame). You could also calculate the milliseconds per frame by grabbing the amount of time for just 1 frame of animation with the following code: AVIStreamSampleToTime(pavi,1). Either way should work fine! Big thanks to Albert Chaulk for the idea!
The reason I say rough milliseconds per frame is because mpf is an integer so any floating values will be rounded off.
AVIStreamInfo(pavi, &psi, sizeof(psi)); // Reads Information About The Stream Into psi
width = psi.rcFrame.right-psi.rcFrame.left; // Width Is Right Side Of Frame Minus Left
height = psi.rcFrame.bottom-psi.rcFrame.top; // Height Is Bottom Of Frame Minus Top
lastframe = AVIStreamLength(pavi); // The Last Frame Of The Stream
mpf = AVIStreamSampleToTime(pavi,lastframe)/lastframe; // Calculate Rough Milliseconds Per Frame
Because OpenGL requires texture data to be a power of 2, and because most videos are 160×120, 320×240 or some other odd dimensions we need a fast way to resize the video on the fly to a format that we can use as a texture. To do this, we take advantage of specific Windows Dib functions.
The first thing we need to do is describe the type of image we want. To do this, we fill the bmih BitmapInfoHeader structure with our requested parameters. We start off by setting the size of the structure. We then set the bitplanes to 1. Three bytes of data works out to 24 bits (RGB). We want the image to be 256 pixels wide and 256 pixels tall and finally we want the data returned as UNCOMPRESSED RGB data (BI_RGB).
CreateDIBSection creates a dib that we can directly write to. If everything goes well, hBitmap will point to the dib's bit values. hdc is a handle to a device context (DC). The second parameter is a pointer to our BitmapInfo structure. The structure contains information about the dib file as mentioned above. The third parameter (DIB_RGB_COLORS) specifies that the data is RGB values. data is a pointer to a variable that receives a pointer to the location of the DIB's bit values (whew, that was a mouthful). By setting the 5th value to NULL, memory is allocated for our DIB. Finally, the last parameter can be ignored (set to NULL).
Quoted from the MSDN: The SelectObject function selects an object into the specified device context (DC).
We have now created a DIB that we can directly draw to. Yay :)
bmih.biSize = sizeof (BITMAPINFOHEADER); // Size Of The BitmapInfoHeader
bmih.biPlanes = 1; // Bitplanes
bmih.biBitCount = 24; // Bits Format We Want (24 Bit, 3 Bytes)
bmih.biWidth = 256; // Width We Want (256 Pixels)
bmih.biHeight = 256; // Height We Want (256 Pixels)
bmih.biCompression = BI_RGB; // Requested Mode = RGB
hBitmap = CreateDIBSection (hdc, (BITMAPINFO*)(&bmih), DIB_RGB_COLORS, (void**)(&data), NULL, NULL);
SelectObject (hdc, hBitmap); // Select hBitmap Into Our Device Context (hdc)
A few more things to do before we're ready to read frames from the AVI. The next thing we have to do is prepare our program to decompress video frames from the AVI file. We do this with the AVIStreamGetFrameOpen(…) function.
You can pass a structure similar to the one above as the second parameter to have a specific video format returned. Unfortunately, the only thing you can alter is the width and height of the returned image. The MSDN also mentions that you can pass AVIGETFRAMEF_BESTDISPLAYFMT to select the best display format. Oddly enough, my compiler had no definition for it.
If everything goes well, a GETFRAME object is returned (which we need to read frames of data). If there are any problems, a message box will pop onto the screen telling you there was an error!
pgf = AVIStreamGetFrameOpen(pavi, NULL); // Create The PGETFRAME Using Our Request Mode
if (pgf == NULL) {
// An Error Occurred Opening The Frame
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Frame", "Error", MB_OK | MB_ICONEXCLAMATION);
}
The code below prints the videos width, height and frames to title. We display title at the top of the window with the command SetWindowText(…). Run the program in windowed mode to see what the code below does.
// Information For The Title Bar (Width / Height / Last Frame)
wsprintf(title, "NeHe's AVI Player: Width: %d, Height: %d, Frames: %d", width, height, lastframe);
SetWindowText(g_window->hWnd, title); // Modify The Title Bar
}
Now for the fun stuff… we grab a frame from the AVI and then convert it to a usable image size / color depth. lpbi will hold the BitmapInfoHeader information for the frame of animation. We accomplish a few things at once in the second line of code below. First we grab a frame of animation … The frame we want is specified by frame. This will pull in the frame of animation and will fill lpbi with the header information for that frame.
Now for the fun stuff… we need to point to the image data. To do this we need to skip over the header information (lpbi->biSize). One thing I didn't realize until I started writing this tut was that we also have to skip over any color information. To do this we also add colors used multiplied by the size of RGBQUAD (biClrUsed*sizeof(RGBQUAD)). After doing ALL of that :) we are left with a pointer to the image data (pdata).
Now we need to convert the frame of animation to a usuable texture size as well, we need to convert the data to RGB data. To do this, we use DrawDibDraw(…).
A quick explanation. We can draw directly to our custom DIB. That's what DrawDibDraw(…) does. The first parameter is a handle to our DrawDib DC. The second parameter is a handle to the DC. Next we have the upper left corner (0,0) and the lower right corner (256, 256) of the destination rectangle.
lpbi is a pointer to the bitmapinfoheader information for the frame we just read. pdata is a pointer to the image data for the frame we just read.
Then we have the upper left corner (0,0) of the source image (frame we just read) and the lower right corner of the frame we just read (width of the frame, height of the frame). The last parameter should be left at 0.
This will convert an image of any size / color depth to a 256×256×24bit image.
void GrabAVIFrame(int frame) // Grabs A Frame From The Stream
{
LPBITMAPINFOHEADER lpbi; // Holds The Bitmap Header Information
lpbi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, frame); // Grab Data From The AVI Stream
pdata=(char *)lpbi+lpbi->biSize+lpbi->biClrUsed * sizeof(RGBQUAD); // Pointer To Data Returned By AVIStreamGetFrame
// (Skip The Header Info To Get To The Data)
// Convert Data To Requested Bitmap Format
DrawDibDraw(hdd, hdc, 0, 0, 256, 256, lpbi, pdata, 0, 0, width, height, 0);