1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2 
3 #if       defined AVI_CAPTURING
4 #include "AVIGenerator.h"
5 
6 #include "Rendering/GlobalRendering.h"
7 #include "Rendering/GL/myGL.h"
8 #include "Game/GameVersion.h"
9 #include "System/Log/ILog.h"
10 #include "System/SpringApp.h"
11 #include "System/Platform/Threading.h"
12 
13 #include <windows.h>
14 
15 #include <boost/bind.hpp>
16 #include <cassert>
17 
18 #if defined(_WIN32) && !defined(__MINGW32__)
19 #pragma message("Adding library: vfw32.lib")
20 #pragma comment(lib, "vfw32.lib")
21 #endif
22 
23 
24 
25 
initVFW()26 bool CAVIGenerator::initVFW() {
27 #if defined(_WIN32) && defined(__MINGW32__)
28 
29 	msvfw32 = LoadLibrary("msvfw32.dll");
30 	avifil32 = LoadLibrary("avifil32.dll");
31 
32 	VideoForWindowsVersion_ptr=(VideoForWindowsVersion_type)GetProcAddress(msvfw32, "VideoForWindowsVersion");
33 	ICCompressorChoose_ptr=(ICCompressorChoose_type)GetProcAddress(msvfw32, "ICCompressorChoose");
34 	ICCompressorFree_ptr=(ICCompressorFree_type)GetProcAddress(msvfw32, "ICCompressorFree");
35 	ICOpen_ptr=(ICOpen_type)GetProcAddress(msvfw32, "ICOpen");
36 
37 	AVIFileInit_ptr=(AVIFileInit_type)GetProcAddress(avifil32, "AVIFileInit");
38 	AVIFileOpenA_ptr=(AVIFileOpenA_type)GetProcAddress(avifil32, "AVIFileOpenA");
39 	AVIFileCreateStreamA_ptr=(AVIFileCreateStreamA_type)GetProcAddress(avifil32, "AVIFileCreateStreamA");
40 	AVIMakeCompressedStream_ptr=(AVIMakeCompressedStream_type)GetProcAddress(avifil32, "AVIMakeCompressedStream");
41 	AVIStreamSetFormat_ptr=(AVIStreamSetFormat_type)GetProcAddress(avifil32, "AVIStreamSetFormat");
42 	AVIStreamRelease_ptr=(AVIStreamRelease_type)GetProcAddress(avifil32, "AVIStreamRelease");
43 	AVIFileRelease_ptr=(AVIFileRelease_type)GetProcAddress(avifil32, "AVIFileRelease");
44 	AVIFileExit_ptr=(AVIFileExit_type)GetProcAddress(avifil32, "AVIFileExit");
45 	AVIStreamWrite_ptr=(AVIStreamWrite_type)GetProcAddress(avifil32, "AVIStreamWrite");
46 
47 
48 	if (NULL == msvfw32 || NULL == avifil32) {
49 		errorMsg="LoadLibrary failed.";
50 		return false;
51 	}
52 
53 	if (!VideoForWindowsVersion_ptr || !ICCompressorChoose_ptr || !ICCompressorFree_ptr || !ICOpen_ptr) {
54 		errorMsg="initVFW Error.";
55 		return false;
56 	}
57 
58 	if (!AVIFileInit_ptr || !AVIFileOpenA_ptr || !AVIFileCreateStreamA_ptr ||
59 		!AVIMakeCompressedStream_ptr || !AVIStreamSetFormat_ptr || !AVIStreamRelease_ptr ||
60 		!AVIFileRelease_ptr || !AVIFileExit_ptr || !AVIStreamWrite_ptr) {
61 			errorMsg="initVFW Error.";
62 			return false;
63 	}
64 #else
65 
66 	VideoForWindowsVersion_ptr =&VideoForWindowsVersion;
67 	AVIFileInit_ptr = &AVIFileInit;
68 	AVIFileOpenA_ptr = &AVIFileOpenA;
69 	AVIFileCreateStreamA_ptr = &AVIFileCreateStreamA;
70 	AVIMakeCompressedStream_ptr = &AVIMakeCompressedStream;
71 	AVIStreamSetFormat_ptr = &AVIStreamSetFormat;
72 	AVIStreamRelease_ptr = &AVIStreamRelease;
73 	AVIFileRelease_ptr = &AVIFileRelease;
74 	AVIFileExit_ptr = &AVIFileExit;
75 	AVIStreamWrite_ptr = &AVIStreamWrite;
76 	ICCompressorChoose_ptr = &ICCompressorChoose;
77 	ICCompressorFree_ptr = &ICCompressorFree;
78 	ICOpen_ptr = &ICOpen;
79 #endif
80 	return true;
81 }
82 
83 
CAVIGenerator(const std::string & fileName,int videoSizeX,int videoSizeY,DWORD videoFPS)84 CAVIGenerator::CAVIGenerator(const std::string& fileName, int videoSizeX, int videoSizeY, DWORD videoFPS)
85 							:
86 							fileName(fileName),
87 							videoFPS(videoFPS),
88 							errorMsg("Ok"),
89 							quitAVIgen(false),
90 							AVIThread(0),
91 							readBuf(NULL),
92 							m_lFrame(0),
93 							m_pAVIFile(NULL),
94 							m_pStream(NULL),
95 							m_pStreamCompressed(NULL) {
96 
97 	assert(videoSizeX % 4 == 0);
98 	assert(videoSizeY % 4 == 0);
99 
100 	memset(&bitmapInfo, 0, sizeof(BITMAPINFOHEADER));
101 	bitmapInfo.biSize = sizeof(BITMAPINFOHEADER);
102 	bitmapInfo.biWidth = videoSizeX;
103 	bitmapInfo.biHeight = videoSizeY;
104 	bitmapInfo.biPlanes = 1;
105 	bitmapInfo.biBitCount = 24;
106 	bitmapInfo.biSizeImage = videoSizeX * videoSizeY * 3;
107 	bitmapInfo.biCompression = BI_RGB;
108 
109 	if (!initVFW()) {
110 		quitAVIgen = true;
111 	}
112 }
113 
114 
~CAVIGenerator()115 CAVIGenerator::~CAVIGenerator() {
116 
117 	if (AVIThread) {
118 		{
119 			boost::mutex::scoped_lock lock(AVIMutex);
120 			quitAVIgen = true;
121 			AVICondition.notify_all();
122 		}
123 		AVIThread->join();
124 
125 		delete AVIThread;
126 		AVIThread = 0;
127 	}
128 
129 	while (!freeImageBuffers.empty()) {
130 		unsigned char* tmp = freeImageBuffers.front();
131 		freeImageBuffers.pop_front();
132 		delete [] tmp;
133 	}
134 	while (!imageBuffers.empty()) {
135 		unsigned char* tmp = imageBuffers.front();
136 		imageBuffers.pop_front();
137 		delete [] tmp;
138 	}
139 
140 	delete [] readBuf;
141 	readBuf = 0;
142 
143 
144 	ReleaseAVICompressionEngine();
145 	LOG("Finished writing avi file %s", fileName.c_str());
146 
147 	// Just checking that all allocated ressources have been released.
148 	assert(AVIThread == NULL);
149 	assert(m_pAVIFile == NULL);
150 	assert(m_pStream == NULL);
151 	assert(m_pStreamCompressed == NULL);
152 	assert(freeImageBuffers.empty());
153 	assert(imageBuffers.empty());
154 	assert(readBuf == NULL);
155 }
156 
157 
ReleaseAVICompressionEngine()158 void CAVIGenerator::ReleaseAVICompressionEngine() {
159 
160 	if (m_pStream && AVIStreamRelease_ptr) {
161 		AVIStreamRelease_ptr(m_pStream);
162 		m_pStream=NULL;
163 	}
164 
165 	if(m_pStreamCompressed && AVIStreamRelease_ptr){
166 		AVIStreamRelease_ptr(m_pStreamCompressed);
167 		m_pStreamCompressed=NULL;
168 	}
169 
170 	if (m_pAVIFile && AVIFileRelease_ptr) {
171 		AVIFileRelease_ptr(m_pAVIFile);
172 		m_pAVIFile=NULL;
173 	}
174 
175 	if (ICCompressorFree_ptr) {
176 		ICCompressorFree_ptr(&cv);
177 	}
178 	if (AVIFileExit_ptr) {
179 		AVIFileExit_ptr();
180 	}
181 
182 	if (msvfw32) {
183 		FreeLibrary(msvfw32);
184 	}
185 	if (avifil32) {
186 		FreeLibrary(avifil32);
187 	}
188 }
189 
190 
InitAVICompressionEngine()191 HRESULT CAVIGenerator::InitAVICompressionEngine() {
192 
193 	AVISTREAMINFO strHdr; // Information for a single stream
194 	AVICOMPRESSOPTIONS opts;
195 	HRESULT hr;
196 
197 
198 	// Let us make sure we are running on 1.1
199 	DWORD wVer = HIWORD(VideoForWindowsVersion_ptr());
200 	if (wVer < 0x010a) {
201 		// oops, we are too old, blow out of here
202 		errorMsg = "Version of Video for Windows too old. Come on, join the 21th century!";
203 		return S_FALSE;
204 	}
205 
206 	// Initialize AVI engine
207 	AVIFileInit_ptr();
208 
209 
210 	memset(&cv, 0, sizeof(COMPVARS));
211 	cv.cbSize = sizeof(COMPVARS);
212 	cv.dwFlags = ICMF_COMPVARS_VALID;
213 	cv.fccHandler = mmioFOURCC('x', 'v', 'i', 'd'); // default video codec
214 	cv.lQ = ICQUALITY_DEFAULT;
215 
216 
217 	//Set the compression, prompting dialog if necessary
218 	if (!ICCompressorChoose_ptr(NULL, ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME, &bitmapInfo, NULL, &cv, NULL)) {
219 		return S_FALSE;
220 	}
221 
222 	// Fill in the header for the video stream....
223 	memset(&strHdr, 0, sizeof(AVISTREAMINFO));
224 	strHdr.fccType                = streamtypeVIDEO;	// video stream type
225 	strHdr.fccHandler             = cv.fccHandler;
226 	strHdr.dwScale                = 1;					// should be one for video
227 	strHdr.dwRate                 = videoFPS;			// fps
228 	strHdr.dwSuggestedBufferSize  = bitmapInfo.biSizeImage;	// Recommended buffer size, in bytes, for the stream.
229 	SetRect(&strHdr.rcFrame, 0, 0, bitmapInfo.biWidth, bitmapInfo.biHeight);
230 	strcpy(strHdr.szName, "Spring video.");
231 
232 
233 	memset(&opts, 0, sizeof(AVICOMPRESSOPTIONS));
234 	opts.fccType = streamtypeVIDEO;
235 	opts.fccHandler = cv.fccHandler;
236 	opts.dwKeyFrameEvery = cv.lKey;
237 	opts.dwQuality = cv.lQ;
238 	opts.dwBytesPerSecond = cv.lDataRate;
239 	opts.dwFlags = ((cv.lDataRate > 0) ? AVICOMPRESSF_DATARATE : 0) | ((cv.lKey > 0) ? AVICOMPRESSF_KEYFRAMES : 0);
240 	opts.lpFormat = NULL;
241 	opts.cbFormat = 0;
242 	opts.lpParms = cv.lpState;
243 	opts.cbParms = cv.cbState;
244 	opts.dwInterleaveEvery = 0;
245 
246 
247 
248 	//Open the movie file for writing
249 	hr = AVIFileOpenA_ptr(&m_pAVIFile,			// Address to contain the new file interface pointer
250 		fileName.c_str(),				// Null-terminated string containing the name of the file to open
251 		OF_WRITE | OF_CREATE | OF_SHARE_EXCLUSIVE,	// Access mode to use when opening the file.
252 		NULL);						// use handler determined from file extension.
253 
254 	if (hr != AVIERR_OK) {
255 		errorMsg = "AVI Engine failed to initialize. Check filename ";
256 		errorMsg += fileName;
257 		// Translate error code
258 		switch (hr) {
259 			case AVIERR_BADFORMAT: {
260 				errorMsg += "The file couldn't be read, indicating a corrupt file or an unrecognized format.";
261 			} break;
262 			case AVIERR_MEMORY: {
263 				errorMsg += "The file could not be opened because of insufficient memory.";
264 			} break;
265 			case AVIERR_FILEREAD: {
266 				errorMsg += "A disk error occurred while reading the file.";
267 			} break;
268 			case AVIERR_FILEOPEN: {
269 				errorMsg += "A disk error occurred while opening the file.";
270 			} break;
271 			case REGDB_E_CLASSNOTREG: {
272 				errorMsg += "According to the registry, the type of file specified in AVIFileOpen does not have a handler to process it";
273 			} break;
274 			default: {
275 				errorMsg += "Unknown error.";
276 			}
277 		}
278 		return hr;
279 	}
280 
281 
282 	// Create the stream
283 	hr = AVIFileCreateStreamA_ptr(m_pAVIFile,		    // file pointer
284 		&m_pStream,		    // returned stream pointer
285 		&strHdr);	    // stream header
286 
287 	if (hr != AVIERR_OK) {
288 		errorMsg = "AVI Stream creation failed. Check Bitmap info.";
289 		if (hr == AVIERR_READONLY) {
290 			errorMsg += " Read only file.";
291 		}
292 		return hr;
293 	}
294 
295 
296 	// Create a compressed stream using codec options.
297 	hr = AVIMakeCompressedStream_ptr(&m_pStreamCompressed, m_pStream, &opts, NULL);
298 	if (hr != AVIERR_OK) {
299 		errorMsg = "AVI Compressed Stream creation failed.";
300 
301 		switch (hr) {
302 			case AVIERR_NOCOMPRESSOR: {
303 				errorMsg += " A suitable compressor cannot be found.";
304 			} break;
305 			case AVIERR_MEMORY: {
306 				errorMsg += " There is not enough memory to complete the operation.";
307 			} break;
308 			case AVIERR_UNSUPPORTED: {
309 				errorMsg += "Compression is not supported for this type of data. This error might be returned if you try to compress data that is not audio or video.";
310 			} break;
311 			default: {
312 				errorMsg += "Unknown error.";
313 			}
314 		}
315 		return hr;
316 	}
317 
318 
319 	//Sets the format of a stream at the specified position
320 	hr = AVIStreamSetFormat_ptr(m_pStreamCompressed,
321 		0,			// position
322 		&bitmapInfo,	    // stream format
323 		bitmapInfo.biSize +   // format size
324 		bitmapInfo.biClrUsed * sizeof(RGBQUAD));
325 
326 	if (hr != AVIERR_OK) {
327 		errorMsg = "AVI Compressed Stream format setting failed.";
328 		return hr;
329 	}
330 
331 	return hr;
332 }
333 
334 
InitEngine()335 bool CAVIGenerator::InitEngine() {
336 
337 	if (quitAVIgen) {
338 		// error in initVFW
339 		return false;
340 	}
341 
342 	for (int i=0; i < 10; i++) {
343 		unsigned char* tmpBuf = new unsigned char[bitmapInfo.biSizeImage];
344 		freeImageBuffers.push_back(tmpBuf);
345 	}
346 
347 	HWND mainWindow = FindWindow(NULL, ("Spring " + SpringVersion::GetFull()).c_str());
348 	if (globalRendering->fullScreen) {
349 		ShowWindow(mainWindow, SW_SHOWMINNOACTIVE);
350 	}
351 	boost::mutex::scoped_lock lock(AVIMutex);
352 	AVIThread = new boost::thread(boost::bind(&CAVIGenerator::AVIGeneratorThreadProc, this));
353 	AVICondition.wait(lock);  // Wait until InitAVICompressionEngine() completes.
354 	if (globalRendering->fullScreen) {
355 		ShowWindow(mainWindow, SW_RESTORE);
356 	}
357 	return !quitAVIgen;
358 }
359 
360 
AddFrame(unsigned char * pixelData)361 HRESULT CAVIGenerator::AddFrame(unsigned char* pixelData){
362 
363 	// compress bitmap
364 	HRESULT hr = AVIStreamWrite_ptr(m_pStreamCompressed,	// stream pointer
365 		m_lFrame,				// time of this frame
366 		1,						// number to write
367 		pixelData,				// image buffer
368 		bitmapInfo.biSizeImage,	// size of this frame
369 		AVIIF_KEYFRAME,			// flags....
370 		NULL,
371 		NULL);
372 
373 	// updating frame counter
374 	m_lFrame++;
375 
376 	return hr;
377 }
378 
379 
readOpenglPixelDataThreaded()380 bool CAVIGenerator::readOpenglPixelDataThreaded() {
381 
382 	while (true) {
383 		boost::mutex::scoped_lock lock(AVIMutex);
384 		if (quitAVIgen) {
385 			return false;
386 		} else if (readBuf != NULL) {
387 			imageBuffers.push_back(readBuf);
388 			readBuf = NULL;
389 			AVICondition.notify_all();
390 		}
391 		if (freeImageBuffers.empty()) {
392 			AVICondition.wait(lock);
393 		} else {
394 			readBuf = freeImageBuffers.front();
395 			freeImageBuffers.pop_front();
396 			break;
397 		}
398 	}
399 
400 	glReadPixels(0, 0, bitmapInfo.biWidth, bitmapInfo.biHeight, GL_BGR_EXT, GL_UNSIGNED_BYTE, readBuf);
401 	return true;
402 }
403 
404 
405 __FORCE_ALIGN_STACK__
AVIGeneratorThreadProc()406 void CAVIGenerator::AVIGeneratorThreadProc() {
407 
408 	Threading::SetThreadName("avi-recorder");
409 
410 	//Run init from the encoding thread because custom controls displayed by codecs
411 	//sends window messages to the thread it started from, thus deadlocking if
412 	//sending to the main thread whilst it is blocking on readOpenglPixelDataThreaded().
413 	quitAVIgen = InitAVICompressionEngine() != AVIERR_OK;
414 	AVICondition.notify_all();
415 	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
416 	unsigned char* localWriteBuf = 0;
417 	bool encoderError = false;
418 
419 	while (true) {
420 		{
421 			boost::mutex::scoped_lock lock(AVIMutex);
422 			if (encoderError) {
423 				LOG_L(L_ERROR, "The avi generator terminated unexpectedly!");
424 				quitAVIgen = true;
425 				// Do not let the main thread wait, as the encoder will not
426 				// process and free the remaining content in imageBuffers.
427 				AVICondition.notify_all();
428 			}
429 			if (quitAVIgen) {
430 				break;
431 			}
432 			if (localWriteBuf != 0) {
433 				freeImageBuffers.push_back(localWriteBuf);
434 				localWriteBuf = 0;
435 				AVICondition.notify_all();
436 			}
437 			if (imageBuffers.empty()) {
438 				AVICondition.wait(lock);
439 				continue;
440 			}
441 			localWriteBuf = imageBuffers.front();
442 			imageBuffers.pop_front();
443 		}
444 		if (AddFrame(localWriteBuf)) {
445 			encoderError = true;
446 		}
447 		MSG msg;
448 		// Handle all messages the codec sends.
449 		while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
450 			if (GetMessage(&msg, NULL, 0, 0) < 0) {
451 				encoderError = true;
452 				break;
453 			}
454 			TranslateMessage(&msg);
455 			DispatchMessage(&msg);
456 		}
457 	}
458 	delete [] localWriteBuf;
459 }
460 
461 #endif // defined AVI_CAPTURING
462 
463