背景
我正在构建一个基本的MFC应用程序,它可以帮助我运行测试。GUI非常简洁。一个按钮用于启动测试,另一个按钮用来停止测试,再加上一个静态文本对象,显示几个字母和数字,指示测试正在执行的阶段。几个星期以来,这项工作一直令人满意
如果我的应用程序在执行测试时也进行了视频屏幕录制,我会觉得这很有用。大计划是将FFmpeg库函数调用编译到代码中以实现此功能,但这肯定需要我数周的时间来学习、尝试和完成。同时,一个快速但足够的解决方案是从下载的Windows二进制构建中调用预编译的ffmpeg.exe
我向::OnBnClickedButtonStarttest()
方法添加了代码,以便通过CreateProcess()
调用ffmpeg.exe。这很好用。FFmpeg屏幕录制过程在一个新的控制台窗口中开始,并很好地完成了预期的工作。当我选择控制台窗口并按下Ctrl+C时,录制将停止,并且我有了所需的视频文件。我还向::OnBnClickedButtonStoptest()
方法添加了代码,以将所需的Ctrl+C发送到ffmpeg.exe进程的STDIN,并在单击STOP按钮时完成录制。这也很好大部分时间
我的源代码的这两个部分基于上发布的示例https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output微软。
问题
大多数时间停止屏幕录制过程意味着它并不总是停止。有时解决方案不起作用。我找不出为什么大多数时候都有效的解决方案在其他时候却失败了。最终,我宁愿隐藏FFmpeg控制台窗口,让它在幕后工作,但这需要一种可靠的方法来停止子进程,而我当前的代码被证明不能可靠地工作。注意:在源代码的顶部有一行#define BUFSIZE 128
。
DWORD dwNumOfBytesToWrite = 7;
DWORD dwWritten = 0;
CHAR chBuf[BUFSIZE] = { 0x03, 0x03, 0x03, 0x03, 0x03, 0x0A, 0x0D, 0x03, 0x0A, 0x0D };
BOOL bSuccess = FALSE;
bSuccess = WriteFile(g_hChildStd_IN_Wr, chBuf, dwNumOfBytesToWrite, &dwWritten, NULL);
虽然发送一个Ctrl+C(0x03)应该会停止ffmpeg进程,但我确实发送了更多的输入。尽管如此,出于某种原因,我不得不发送两次上述WriteFile
指令,以便实际看到进程停止。我不知道为什么。不过,这对我来说并不重要,只要第二次它确实有效
让我担心的是,有时子进程不会停止。无论我在上面的WriteFile
线路上发送了多少次。我发现了另一个有希望的例子。这一次没有匿名管道——我怀疑这是不可靠行为的根源
https://www.techpowerup.com/forums/threads/c-c-c-console-redirection-with-sockets-win32.62350/
这似乎使用自己的命名通道来访问子进程的标准输入。我更喜欢这种方法。不幸的是,它不仅不起作用,而且也无法编译。Socket thisSocket;
行指的是不存在的类型。没有Socket
这样的类型,没有大写的S
和小写的ocket
。我尝试在它的位置使用全大写的SOCKET
类型,它确实可以编译,但相应修改的WriteFile((HANDLE)thisSocket, chBuf, dwNumOfBytesToWrite, &dwWritten, NULL);
行总是返回FAILURE,并且不会停止子进程。为了让我的调查更加困难,大量的互联网搜索点击让我转而关注与WINSOCK相关的话题。
我错过什么了吗
向ffmpeg.exe进程发送Ctrl+C以委婉地要求其完成录制的可靠方法是什么
请注意,我可以通过TerminateProcess(g_ffmpeg_process_handle, 0)
残忍地杀死FFmpeg控制台窗口,但这并不总是允许孩子正确关闭视频文件,从而导致屏幕录制损坏。
没有合适的解决方案,但我有一个可以接受的解决方案
我引入了一个初始值为ZERO的全局变量ffmpChildProc_statusFlag
当CreateProcess()在其隐藏的控制台窗口中成功启动FFmpeg子进程时,我将ffmpChildProc_statusFlag的值分配为ONE
当FFmpeg子进程实际终止时,我使用RegisterWaitForSingleObject()自动启动CALLBACK函数。
VOID CALLBACK onSubprocessExit(_In_ PVOID lpParameter, _In_ BOOLEAN TimerOrWaitFired) {
//// Do clean-up by unregistering this callback instance.
//// Update _ffmpChildProc_statusFlag_ to have a value of TWO.
}
这些让我自信地知道我的努力有多成功。
我还编写了一个EnumWindowsProc回调,它使用GetWindowThreadProcessId()返回属于当前检查窗口的进程ID,直到我遇到我的FFmpeg子进程的进程IDCreateProcess()在其process_INFORMATION结构参数的dwProcessId成员中返回该进程ID找不到更好的方法从其已知进程Id中获取创建的子进程的窗口句柄。因此,EnumWindowsUntilProcessHandleFound()。
下面是我所做的
当完成FFmpeg子进程的时间到来时,我首先尝试将WriteFile(0x03)设置为问题中显示的重定向StdIN管道方法。我这样做了1-5次,每次都检查ffmpChildProc_statusFlag的值,看看是否需要再尝试一次,每次尝试后有几百毫秒的暂停时间。不关闭管道的Write端,以便下一次写入尝试能够成功
如果子进程尚未结束,则我关闭的3个句柄
- 管道的写入端
- 子进程
- 子进程的线程
并再次暂停数百毫秒,以便有时间完成回调(如果子进程完成)
作为最后一次尝试,如果FFmpeg进程仍在运行,则我取消隐藏子进程的控制台窗口,使用户可以使用实际的Ctrl+C手动停止它。但是,在最后一次尝试时,我尝试了几次使用SendInput()以编程方式输入Ctrl+C。
CWnd* wFFmpPtr = CWnd::FromHandle(ffmpExe_window_handle);
if (!wFFmpPtr->IsWindowVisible()) {
TRACE(L"tREVEALING FFmp window.n");
wFFmpPtr->ShowWindow(SW_SHOWNORMAL);
} //// if not already visible
SetActiveWindow(ffmpExe_window_handle);
TRACE(L"tSending Ctrl+C keyboard action to the frontmost window.n");
INPUT vips; //// My temporary virtual-input structure to define a keyboard key action.
vips.type = INPUT_KEYBOARD; //// Setting up a generic keyboard event.
vips.ki.wScan = 0; //// Ignoring the scan code, because the virtual-keycode will be provided instead.
vips.ki.time = 0; //// Ignoring the timestamp, so the system will fill it for me.
vips.ki.dwExtraInfo = 0; //// I have no extra info to provide.
//// Simulating a "Ctrl" key press.
vips.ki.wVk = 0x11; //// Virtual-key code for the "Ctrl" key.
vips.ki.dwFlags = 0; //// 0 means a normal key press (key going downwards).
SendInput(1, &vips, sizeof(INPUT));
//// Simulating a "C" key press.
vips.ki.wVk = 0x43; //// Virtual-key code for the "C" key.
vips.ki.dwFlags = 0;
SendInput(1, &vips, sizeof(INPUT));
//// Simulating a "C" key release.
vips.ki.dwFlags = KEYEVENTF_KEYUP; //// KEYEVENTF_KEYUP means a key release (key going upwards).
SendInput(1, &vips, sizeof(INPUT));
//// Simulating a "Ctrl" key release.
vips.ki.wVk = 0x11; //// Virtual-key code for the "Ctrl" key.
SendInput(1, &vips, sizeof(INPUT));
TRACE(L"tActive window should have received a Ctrl+C.n");
此时,子窗口始终关闭。但是,可以肯定的是,我也留下了一个条件TerminateProcess()——如果调用它,它可能会以损坏的视频记录结束子进程。
尝试但失败的方法
GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); //// Never worked.
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0); //// Never worked.
msgResult = wFFmp_ptr->SendMessage(WM_CHAR, 0x03, 1); // Never worked.
msgResult = wFFmp_ptr->SendMessage(WM_UNICHAR, 0x03, 1); // Never worked.
msgResult = wFFmp_ptr->SendMessage(WM_KEYDOWN, 0x03, 1); // Not these two.
msgResult = wFFmp_ptr->SendMessage(WM_KEYUP, 0x03, 1);
msgResult = wFFmp_ptr->SendMessage(WM_KEYDOWN, 0x11, 1); // Or these four either.
msgResult = wFFmp_ptr->SendMessage(WM_KEYDOWN, 0x43, 1);
msgResult = wFFmp_ptr->SendMessage(WM_KEYUP, 0x43, 1);
msgResult = wFFmp_ptr->SendMessage(WM_KEYUP, 0x11, 1);
其中没有一个能够关闭子进程。一次也没有。尽管我对他们寄予厚望,因为他们会将Ctrl+C信号引导到我的特定窗口。因此,假设更可靠-(
虽然SendInput方法似乎一直有效,但我对这种方法不满意,因为它将模拟的击键发送到最前面的窗口,而不是特定的窗口。因此,它更容易出错。用户的鼠标点击或实际键盘动作可以激活另一个窗口,然后该窗口将接收用于FFmpeg控制台窗口的模拟Ctrl+C击键
目前,这是可以接受的