項目:“表情包”製作---利用ffmpeg製作Gif動態圖

項目背景

隨着互聯網傳播技術的普及和網絡社交文化的繁榮,傳統的文字傳播在日常表達上不具有完整的信息,同樣一句話在不同的場景或者情緒下會出現不同的效果,使用傳統的文字聊天,我們無法通過聊天軟件來判別聊天者的表情和動作來理解對方的情緒,因此,表情包在文字表達上可以更好地使對方理解我們此時的情緒,以方便更好的表達情感。
“一言不合就鬥圖”已經成爲了大多數網民所必備的一項技能,一般常用於各式聊天軟件,例如微信、QQ等,“鬥圖”開始於QQ,在進行聊天時,大家發送搞怪圖片來表達自己想要表達的意思,後發展在各式社交論壇上,例如貼吧、知乎等;

可行性分析

可行性分析從三方面來分析:
經濟可行性:成本比較低
操作可行性:操作簡單
技術可行性:利用一些工具和第三方庫

需求分析

要生成Gif動態圖,我們有兩種生成方式:圖片生成和視頻生成,藉助的工具是ffmpeg工具,UI界面佈局器,首先介紹一下ffmpeg工具:
ffmpeg是特別強大的專門用於處理音視頻的開源庫,既可以使用它的API對音視頻進行處理,也可以使用它提供的工具,如 ffmpeg, ffplay, ffprobe,來編輯你的音視頻文件;ffmpeg由以下幾部分組成:
(1)libavcodec: 提供了一系列編碼器的實現。
(2)libavformat: 實現在流協議,容器格式及其本IO訪問。
(3)libavutil: 包括了hash器,解碼器和各利工具函數。
(4)libavfilter: 提供了各種音視頻過濾器。
(5)libavdevice: 提供了訪問捕獲設備和回放設備的接口。
(6)libswresample: 實現了混音和重採樣。
(7)libswscale: 實現了色彩轉換和縮放工能。
在學習了ffmpeg之後,我們知道ffmpeg是使用命令行的格式,響應Cmd來實現相關的操作,因此我們在實現Gif的製作,主要就是通過Cmd控制檯來使用ffmpeg工具,向該工具發送對應的命令來實現各部分操作。

總體設計

本項目共有兩種生成Gif的方式:圖片生成和視頻生成,如圖爲應該實現的流程:
在這裏插入圖片描述
在這裏插入圖片描述
我們需要依靠Duilib庫,它的好處有:
(1)可以基於GDI在窗口上進行重繪,沒有其他依賴,沒有使用其他的系統調用,能夠解決傳統MFC界面的一系列問題;
(2)使用XML來描述界面風格與佈局,實現了UI和邏輯代碼的分離,同時可以實現各種界面效果,例如換膚、透明等;
(3)完全兼容ActiveX控件,也可以和MFC等界面庫配合使用;
(4)可廣泛用於互聯網客戶端、工具軟件客戶端、車載電腦系統等;
Duilib庫是基於Win32系統的一套UI庫,因此我們先來了解一下Win32相關知識。

  • Win32程序介紹
    1、首先建立一個Win32工程,如圖:
    在這裏插入圖片描述
    建立工程,出現下圖:
    在這裏插入圖片描述
    點擊下一步,出現下圖:
    在這裏插入圖片描述
    單擊空項目,然後完成,這樣一個完整的Win32項目創建完成。
    2、實現代碼:
#include <Windows.h>
#include <tchar.h>
//消息回調函數
POINT start;//起點
POINT end;//終點
int state = 0;//剛開始的狀態,來控制畫什麼圖形
//用戶自己定義:函數的格式必須按照系統的固定格式來定義,
LRESULT CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_MOUSEMOVE://鼠標移動,要獲取起點,在移動期間,觸發重繪
		end.x = LOWORD(lParam);//改變終點
		end.y = HIWORD(lParam);
		InvalidateRect(hWnd, NULL, true);//觸發重繪
		break;
	case WM_LBUTTONDOWN://按鼠標就要觸發重繪
		//MessageBox(NULL, _T("LBTNDOWN"), _T("Test"), IDOK);
	{
							start.x = LOWORD(lParam);
							start.y = HIWORD(lParam);
							return 0;
	}
	case WM_LBUTTONUP://放開鼠標
	{
						  end.x = LOWORD(lParam);
						  end.y = HIWORD(lParam);
						  HDC hdc = GetDC(hWnd);
						  switch (state)
						  {
						  case 1://畫直線
							  MoveToEx(hdc, start.x, start.y, NULL);//將光標移到起點的位置
							  LineTo(hdc, end.x, end.y);//畫到終點的位置
							  break;
						  case 2://畫矩形
							  Rectangle(hdc, start.x, start.y, end.x, end.y);//畫矩形
							  break;
						  case 3://畫橢圓
							  Ellipse(hdc, start.x, start.y, end.x, end.y);
							  break;
						  }
						  ReleaseDC(hWnd, hdc);//用完了要進行釋放
						  start = end;//每次畫完後將起點和終點放在同一位置
						  return 0;
	}
	case WM_PAINT://重繪
	{
					  HDC hdc = GetDC(hWnd);
					  switch (state)
					  {
					  case 1://畫直線
						  MoveToEx(hdc, start.x, start.y, NULL);//將光標移到起點的位置
						  LineTo(hdc, end.x, end.y);//畫到終點的位置
						  break;
					  case 2://畫矩形
						  Rectangle(hdc, start.x, start.y, end.x, end.y);//畫矩形
						  break;
					  case 3://畫橢圓
						  Ellipse(hdc, start.x, start.y, end.x, end.y);
						  break;
					  }
					  ReleaseDC(hWnd, hdc);//用完了要進行釋放
					  return 0;
	}
	case WM_CLOSE:
		DestroyWindow(hWnd);
		return 0;
	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int
	nCmdShow)//hInstance相當於這個應用程序
{
	//MessageBox(NULL,_T("hell Win32"),_T("Win32"),IDCANCEL);//IDCANCEL/IDOK/等改變按鈕
	//Step1:註冊一個窗口類
	HWND hwnd; //窗口的句柄
	WNDCLASSEX wc; //窗口類結構
	wc.cbSize = sizeof(WNDCLASSEX);//當前結構體總共佔了多少個字節
	wc.style = CS_VREDRAW | CS_HREDRAW;//窗口刷新機制
	wc.lpszMenuName = 0;//是否有菜單
	wc.lpszClassName = _T("Win32");//標記這個窗口
	wc.lpfnWndProc = WinProc; //消息回調函數,用戶必須提供
	wc.hInstance = hInstance;
	wc.hIcon = NULL;
	wc.hCursor = NULL;
	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wc.cbWndExtra = 0;
	wc.cbClsExtra = 0;
	wc.hIconSm = NULL;
	RegisterClassEx(&wc); //註冊窗口
	//Step2:創建一個窗口
	hwnd = CreateWindow(
		_T("Win32"), //窗口的類名,也就是上面我們自定義的窗口類的名字
		_T("我的第一個Win32程序"), //窗口的標題
		WS_OVERLAPPEDWINDOW, //窗口style
		500, //窗口位置x座標
		300, //窗口位置y座標
		800, //窗口寬度
		600, //窗口高度
		NULL, //父窗口句柄
		NULL,//菜單句柄,沒有時設置爲NULL
		hInstance, //實例句柄
		NULL //創建數據
		);
	if (!hwnd)
	{
		return FALSE;
	}
	ShowWindow(hwnd, SW_SHOW); //顯示窗口
	UpdateWindow(hwnd); //刷新
	//Step3:消息循環
	MSG msg;
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	return 0;
}

其中Win32程序入口函數爲WinMain;
從上面的代碼可以看出,Win32程序一般流程爲:
(1)設計窗口類:完善窗口類的結構體;將WNDCLASSEX結構體轉到定義,爲:
在這裏插入圖片描述
其中cbSize表示該結構體的大小;style表示窗口類的樣子,通常是CS_HREDRAW |
CS_VREDRAW,表示水平和垂直重繪;IpfnWndProc表示窗口的過程處理函數,這個函數攔截用戶需要處理的消息;cbClsExtra和cbWndExtra一般設置爲0;HInstance表示窗口實例;hIcon表示窗口類的圖標;hCursor表示窗口類的鼠標樣式;
(2)註冊窗口類:窗口類的名字、提供窗口過程處理函數,使用RegisterClassEx(&wc)函數註冊窗口,這樣系統才能知道這個窗口的存在;
(3)創建窗口:CreateWindow();
(4)顯示窗口:ShowWindow();
(5)更新窗口:Updatewindow();
(6)消息循環:相當於一個死循環:WM_Close(這樣窗口就不會一閃而過);
(7)消息響應:在用戶自定義的窗口過程處理函數中,用戶對自己需要處理的消息進行攔截響應,對不關心的消息採用系統默認的消息響應函數DefWindowProc()處理即可。
接下來介紹Win32的消息循環;

  • 消息機制
    1、消息概念:是系統內設的一種數據結構;如圖:
    在這裏插入圖片描述
    hwnd是窗口的句柄,這個參數將決定由哪個窗口過程函數對消息進行處理;message是一個消息常量,用來表示消息的類型; wParam表示32 位的附加信息,具體表示什麼內容,要視消息的類型而定;lParam表示32 位的附加信息,具體表示什麼內容,要視消息的類型而定;time是消息發送的時間;pt表示消息發送時鼠標所在的位置
    2、消息分爲系統定義消息和用戶自定義消息
    系統定義消息分爲:窗口消息、命令消息、控件通知消息。
    (1)窗口消息
    與窗口的內部運作有關的消息,如創建窗口,繪製窗口,銷燬窗口等 ,如:WM_CREATE, WM_PAINT,WM_MOUSEMOVE等。
    (2) 命令消息
    當用戶從菜單選中一個命令項目、按下一個快捷鍵、點擊工具欄上的一個按鈕或者點擊控件都將發送
    WM_COMMAND命令消息。通過消息結構中的wParam和lParam成員就能清楚得知道消息的來源。
    LOWORD(wParam):代表菜單ID、或控件ID,或快捷鍵ID; HIWORD(wParam):表示通知碼,當消息是從菜單發出時,則這個值爲0,當消息是從快捷鍵發出時,這個值爲1,當消息是從控件發出時,這個值爲通知碼,比如按鈕的通知碼:BN_CLICKED, BN_DBLCLK等;
    lParam:當消息從菜單和快捷鍵發出時,這個值爲0,當從控件發出時,爲控件的句柄。
    (3) 控件通知消息
    隨着控件的種類越來越多,越來越複雜(如列表控件、樹控件等),僅僅將wParam,lParam將視爲一個32位無符號整數,已經裝不下太多信息了。 爲了給父窗口發送更多的信息,微軟定義了一個新的WM_NOTIFY消息來擴展WM_COMMAND消息。 WM_NOTIFY消息仍然使用MSG消息結構,只是此時wParam爲控件ID,lParam爲一個NMHDR指針,不同的控件可以按照規則對NMHDR進行擴充,因此WM_NOTIFY消息傳送的信息量可以相當的大。
    缺陷:在Windows上進行圖形界面開發,效率低,因此我們需要引進Duilib庫
詳細設計

1、進行環境配置:vs2013將Duilib庫包含進來
(1)Duilib環境搭建:
先對Duilib庫編譯(Duilib庫自行下載),將生成的靜態庫lib和生成的dll文件包含到最外層的Debug目錄下,與.exe文件一層,將庫文件Duilib放在與.cpp文件一層的目錄下,如圖:
在這裏插入圖片描述
在這裏插入圖片描述
然後對環境進行配置,如圖點開項目屬性,在C/C++欄中的附加包含目錄欄中選擇Duilib所在的目錄:
在這裏插入圖片描述
然後在鏈接器欄的附加庫目錄選擇lib文件所在的目錄,如圖:
在這裏插入圖片描述
然後在代碼中加入:

#include "UIlib.h"
using namespace DuiLib;
#pragma comment(lib, "DuiLib_ud.lib")

之後我們就可以使用Duilib庫來進行窗口的創建響應等操作了;
(2)Duilib庫的使用
1> 定義類CDuiFramWnd繼承 CWindowWnd類,在該類中實現:
virtual LPCTSTR GetWindowClassName():返回窗口類的名字(CWindowWnd:Duilib自己封裝的關於窗口類的相關操作:create(…):註冊窗口和創建窗口,顯示窗口,更新窗口;ShowModal():消息循環)
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam):子類如果需要處理系統消息(Windows自己維護的消息)時,需要進行重寫uMsg:獲取到的消息ID—>區分捕獲到的是什麼類型的消息
2> 另外CDuiFramWnd繼承自INotifyUI類(duilib自己定義的類—>抽象類),按鈕創建成功後,添加按鈕控件消息響應到duilib的消息循環中;重寫INotifyUI類的Notify純虛函數,在該函數中用戶捕獲其想要處理的消息,進行自己想要的操作即可。

class INotifyUI
{
	virtual void Notify(TNotifyUI& msg)=0;
};

如果需要攔截duilib自己維護的消息時,只需要在子類中重寫Notify即可;

typedef struct tagTNotifyUI 
{
	CDuiString sType;//消息的類型---"click"鼠標點擊"windowinit"
	CDuiString sVirtualWnd;
	CControlUI* pSender;//消息是由哪個空間觸發的
	DWORD dwTimestamp;
	POINT ptMouse;
	WPARAM wParam;
	LPARAM lParam;
} TNotifyUI;

3> Duilib其實並沒有區分標題欄和客戶區,它的實現方法是屏蔽了系統自帶的標題欄,用客戶區來模擬標題欄,所以想怎麼畫就怎麼畫,非常方便。 在HandleMessage函數裏屏蔽以下三個消息即可 WM_NCACTIVATE、WM_NCCALCSIZE、WM_NCPAINT;
實現代碼:
實現代碼

#include "UIlib.h"
using namespace DuiLib;
#pragma comment(lib, "DuiLib_ud.lib")
class CDuiFramWnd : public CWindowWnd,public INotifyUI
{
public:
	// CWindowWnd類的純虛函數,在該函數中必須返回用戶所定義窗口的類名稱,註冊窗口時需要用到
	virtual LPCTSTR GetWindowClassName() const
	{
		return _T("DuiFramWnd");
	}
	virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)//處理系統消息
	{
		if (WM_CREATE == uMsg)
		{
			////初始化繪畫管理器
			//m_PaintManager.Init(m_hWnd);//m_hWnd:基類中的句柄

			////在窗口創建期間創建一個按鈕
			//CControlUI* pBTN = new CButtonUI;//定義按鈕
			//pBTN->SetText(_T("hello"));//設置窗口按鈕文本
			//pBTN->SetBkColor(0xFF00FF00);//設置窗口按鈕顏色

			//m_PaintManager.AttachDialog(pBTN);//將按鈕關聯到繪製管理器上

			//m_PaintManager.AddNotifier(this);//將按鈕增加到消息循環中
			m_PaintManager.Init(m_hWnd);
			CDialogBuilder builder;
			// duilib.xml需要放到exe目錄下
			CControlUI* pRoot = builder.Create(_T("111.xml"), (UINT)0, NULL, &m_PaintManager);//要設置路徑
			m_PaintManager.AttachDialog(pRoot);
			m_PaintManager.AddNotifier(this);
			return 0;
		}
		else if (uMsg == WM_NCACTIVATE)
		{
			if (!::IsIconic(m_hWnd))
			{
				return (wParam == 0) ? TRUE : FALSE;
			}
		}
		else if (uMsg == WM_NCCALCSIZE)
		{
			return 0;
		}
		else if (uMsg == WM_NCPAINT)
		{
			return 0;
		}
		//攔截會話相關消息
		LRESULT lRse = 0;
		if (m_PaintManager.MessageHandler(uMsg, wParam, lParam, lRse))
		{
			return lRse;
		}
		//其他消息
		return __super::HandleMessage(uMsg, wParam, lParam);
	}
	virtual void Notify(TNotifyUI& msg)
	{
		//響應按鈕單擊消息
		if (msg.sType == _T("click"))//注意這裏不能交換常量和sType
		{
			MessageBox(m_hWnd, _T("按鈕單擊"), _T("Test"), IDOK);
		}
	}
private:
	CPaintManagerUI m_PaintManager;
};

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int
	nCmdShow)
{
	//對應111.xml文件,因爲要找到路徑
	CPaintManagerUI::SetInstance(hInstance);
	// 設置資源的默認路徑(此處設置爲和exe在同一目錄)
	CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath());
	CDuiFramWnd framWnd;
	// Cashier即在窗口右上角顯式的名字
	// UI_WNDSTYLE_FRAME: duilib封裝的宏,代表窗口可視,具有標題欄,最大化最小化,關閉功能等
	// WS_EX_WINDOWEDGE: Win32的窗口風格,帶有邊框
	framWnd.Create(NULL, _T("DulibTest"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
	//顯示窗口,激活消息循環
	framWnd.ShowModal();
	return 0;
}
  • 具體項目實現
    1、我這裏實現的是能夠使用這個窗口來響應對照片或者視頻進行gif動態圖的製作,因此這時需要用到ffmpeg工具(它提供了音視頻的編碼,解碼,轉碼,封裝,解封裝,流,濾鏡,播放等功能),此時就需要用到ffmpeg相關命令,我們先將所需要的圖片和外掛字幕視頻資源以及ffmpeg工具放在Win32工程最外層的Debug目錄下,我們可以在本機的cmd控制檯上執行相關命令先來完成gif動態圖的製作,相關命令如下:
    使用圖片生成gif動態圖

ffmpeg -r 3 -i .\Picture %d.jpg output.gif -y

其中-r後跟上數字表示每一次顯示多少幀,-i表示輸入,.\Picture %d.jpg表示圖片資源路徑,其中%d表示通配以數字開頭的照片,output.gif表示生成的gif名字,-y表示如果路徑下存在與輸出文件相同的文件名,則覆蓋
使用視頻生成gif動態圖

1、從原視頻中截取所需要的片段:ffmpeg -i input.mkv -vcodec copy -acodec copy -ss 00:40:07 -to 00:40:28 11.mkv -y

-vcodec copy -acodec copy表示將視頻和音頻流拷貝過來,-ss … -to …表示截取視頻的長度,11.mkv表示生成的片段;

2、從視頻中提取字幕:ffmpeg -i 11.mkv input.srt -y

input.srt表示生成的字幕;

3、編輯字幕

4、從截取的片段中只抽離視頻(不要字幕和音頻),即提取視頻裸流:ffmpeg -i 11.mkv -vcodec copy -an -sn 22.mkv -y

其中-an表示不要音頻,-sn表示不要字幕,22.mkv是生成的視頻裸流;

5、將修改後的srt字幕內嵌到視頻裸流中,即燒錄成一個整天:ffmpeg -i 22.mkv -vf subtitles = input.srt 33.mkv -y

6、生成動態圖:ffmpeg -i 33.mkv -vf scale = iw/2:ih/2 -f gif

表示按照等比例的方式生成gif;
也可以使用(ffmpeg -i 33.mkv -s 200*200 -v 15 output.gif -y)其中-s控制大小;
這些命令是針對cmd控制檯的,所以必須要使用ShellExecuteEx函數來給控制檯發命令,我們可以看一下這個函數的用法:

//啓動:
 SHELLEXECUTEINFO ShExecInfo;
 ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO);
 ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS ;
 ShExecInfo.hwnd = NULL;
 ShExecInfo.lpVerb = NULL;
 ShExecInfo.lpFile = "xxx.exe"; //can be a file as well
 ShExecInfo.lpParameters = "";
 ShExecInfo.lpDirectory = NULL;
 ShExecInfo.nShow = SW_SHOW;
 ShExecInfo.hInstApp = NULL;
 ShellExecuteEx(&ShExecInfo);
//關閉:

 if( ShExecInfo.hProcess != NULL)
 {
      TerminateProcess(ShExecInfo.hProcess,0);
      ShExecInfo.hProcess = NULL;
 }

2、使用Duilib中的UI界面佈局器進行窗口設計,我們使用UI佈局器來對各個控件(按鈕、編輯框、下拉框等)進行繪製,此時就會生成一個xml文件,但是由於UI界面佈局器自身存在缺陷,例如:
bug—>在使用時記得及時備份和對於控件支持的不是非常全面;
因此我們在使用時必須及時保存,對於不支持的控件我們可以查詢資料對xml文件進行編輯補充,這裏可以推薦相關鏈接:
https://www.cnblogs.com/Alberl/p/3354459.html
如圖:
在這裏插入圖片描述
選擇圖片我們就不需要截取編輯框和視頻加載路徑的控件,諸如截取、提取SRT、寫入SRT、提取視頻、燒錄的按鈕都不用實現,因爲圖片只需要一組ffmpeg命令直接生成Gif就可以,在點擊生成Gif按鈕後,就可以生成Gif了;選擇視頻我們就需要一步步來進行實現,點擊按鈕即可;
3、製作Gif各個部分代碼的實現
(1)功能設置,類CDuiFramWnd繼承WindowImplBase這個類,因爲Duilib已經對常用的操作進行了很好的封裝,正常使用時不需要按照之前的方式實現,只需要讓用戶實現的窗口類繼承自Duilib封裝的:WindowImplBase 類即可,該類是一個duilib的基礎框架類,封裝了常用操作,以方便大家使用。 它是以XML作爲界面描述的,所以用它的時候,我們必須將界面描述寫到XML裏。即通過xml文件描述窗口—窗口創建
重寫這3個純虛函數:
virtual CDuiString GetSkinFolder();
virtual CDuiString GetSkinFile();
virtual LPCTSTR GetWindowClassName(void)const;

class CDuiFramWnd : public WindowImplBase
{
protected:
	virtual CDuiString GetSkinFolder()
	{
		return _T("");//這裏不用給xml文件的路徑,因爲在主函數中已經給過了
	}
	virtual CDuiString GetSkinFile()
	{
		return _T("gifMake.xml");//一定要與目錄放的xml文件文件名稱相同

	}
	virtual LPCTSTR GetWindowClassName(void)const
	{
		return _T("GIFMakeWnd");
	}

然後進行消息處理:
1、Windows系統消息—HandleMessage(這個在CWindowWnd類中)
2、duilib消息—duilib所維護的空間所產生的消息:Notify()(這個在INotifyUI類中);

	virtual void Notify(TNotifyUI& msg)//響應
	{
		CDuiString strName = msg.pSender->GetName();//獲取控件的名字
		if (msg.sType == _T("click"))//如果是鼠標點擊
		{
			if (strName == _T("btn_close"))//如果是關閉按鈕
			{
				Close();
			}
			else if (strName == _T("btn_min"))//如果是最小化按鈕
			{
				//發送最小化消息
				SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0);
			}
			else if (strName == _T("btn_load"))
			{
				LoadFile();
			}
			else if (strName == _T("btn_cut"))//截取視頻
			{
				Cutview();
			}
			else if (strName == _T("btn_get_srt"))//提取字幕
			{
				GetSRTFile();//提取
				LoadSRT();//加載
			}
			else if (strName == _T("btn_commit"))//提交按鈕
			{
				CEditUI* pEdit = (CEditUI*)m_PaintManager.FindControl(_T("edit_word"));
				CDuiString strWord = pEdit->GetText();//獲取這個edit對應的文本

				//將該文本寫回到list中,此時已經在edit中編輯過了
				CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));//先獲取list控件
				CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(pList->GetCurSel());//將當前list的這一行內容選中

				pListItem->SetText(1, strWord);//下標爲1
			}
			else if (strName == _T("btn_write_srt"))//寫入字幕
			{
				WriteSRT();
			}
			else if (strName == _T("btn_view"))//提取視頻
			{
				GenerateView();
			}
			else if (strName == _T("btn_bron"))//燒錄
			{
				BornSRTtoView();
			}
			else if (strName == _T("btn_generate"))//生成gif
			{
				//根據CComboBox控件的name拿到該控件,也就是繪畫管理器中來進行管理的
				CComboBoxUI* pCombo = (CComboBoxUI*)m_PaintManager.FindControl(_T("combo_select"));
				if (0 == pCombo->GetCurSel())//如果當前選中是0
				{
					GenerateGifWithPic();//使用圖片生成
				}
				else
				{
					GenerateGifWithView();//使用視頻生成
				}
			}
		}

(2)設置控件,如果我們選擇的是圖片生成,就需要讓視頻生成的相關控件無效

//在窗口初始化期間,由於默認的是圖片生成,我們需要讓視頻生成的相關控件無效
		else if (msg.sType == _T("windowinit"))
		{
			SetControlEnable(false);
		}
		else if (msg.sType == _T("itemselect"))//如果下拉框Combo是選擇改變了
		{
			if (strName == _T("List_srt"))//表示選擇改變,將改變後的內容放在右側的edit中即可
			{
				//獲取list控件
				CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
				//拿出list中某一項的內容
				CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(pList->GetCurSel());
				//將list選中行中的對應文本信息增加到edit中
				CEditUI* pEdit = (CEditUI*)m_PaintManager.FindControl(_T("edit_word"));
				pEdit->SetText(pListItem->GetText(1));//獲取第一項,並且設置進edit框中
			}
			if (strName == _T("combo_select"))
			{
				CComboBoxUI* pComboUI = (CComboBoxUI*)m_PaintManager.FindControl(_T("combo_select"));
				if (0 == pComboUI->GetCurSel())
				{
					//選擇圖片方式生成,讓以下控件無效
					SetControlEnable(false);
				}
				else//選擇視頻方式生成,讓以下控件有效
				{
					SetControlEnable(true);
				}
			}
		}
	}

(3)各個函數的實現
設置控件是有效還是無效

void SetControlEnable(bool IsValid)//將一些控件設置爲有效或者無效
	{
		((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->SetEnabled(IsValid);//start編輯框設置
		((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->SetEnabled(IsValid);//end編輯框設置
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_cut")))->SetEnabled(IsValid);//截取按鈕設置
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_get_srt")))->SetEnabled(IsValid);//提取SRT按鈕設置
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_write_srt")))->SetEnabled(IsValid);//寫入SRT按鈕設置
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_view")))->SetEnabled(IsValid);//提取視頻按鈕設置
		((CButtonUI*)m_PaintManager.FindControl(_T("btn_bron")))->SetEnabled(IsValid);//燒錄按鈕設置
	}

使用圖片生成Gif:

//1、使用圖片生成gif動態圖
	void GenerateGifWithPic()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//獲取工程的目錄,剛好與exe文件在同一個目錄
		strPath += _T("ffmpeg\\");
		//1.構造命令
		CDuiString strCMD;
		strCMD += _T("/c ");//構造命令期間第一條必須是'/c',要加上/c參數
		strCMD += strPath;
		strCMD += _T("ffmpeg -r 3 -i ");
		strCMD += strPath;
		strCMD += _T(".\\Picture\\%d.jpg ");//這裏是圖片的路徑,注意要進行轉義字符的轉義
		strCMD += strPath;
		strCMD += _T("output.gif -y");//生成動態圖,-y表示如果該目錄下存在output.gif,就覆蓋

		//2.給cmd發命令
		SendCmd(strCMD);
	}

使用視頻生成Gif,分別實現:加載原視頻路徑、截取視頻、提取字幕、寫入字幕、提取視頻裸流、燒錄、生成Gif等函數

//2、使用視頻生成gif動態圖
	//(1)截取所需要的片段
	void Cutview()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//獲取路徑
		strPath += _T("ffmpeg\\");
		//此時我們有兩種方法,因爲加載按鈕響應後,我們可以從加載編輯框中獲取路徑
		CDuiString strViewPath = ((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->GetText();//獲取到編輯框中內容的路徑,也就是拿到了視頻的完整路徑
		//1.構造命令
		CDuiString strCMD;
		strCMD += _T("/c ");//構造命令期間第一條必須是'/c'
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");//要加上\c參數
		//優先通過界面中編輯框來加載視頻路徑
		if (!strViewPath.IsEmpty())
		{
			strCMD += strViewPath;
		}
		else//再到默認路徑下獲取文件
		{
			strCMD += strPath;
			strCMD += _T("input.mkv ");//視頻文件的路徑
		}
		strCMD += _T("-vcodec copy -acodec copy ");
		strCMD += _T("-ss ");

		//獲取起始時間和結尾時間
		CDuiString strStartTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_start")))->GetText();
		if (!IsValidTime(strStartTime))
		{
			MessageBox(NULL, _T("起始時間有誤"), _T("MakeGif"), IDOK);
		}
		CDuiString strEndTime = ((CEditUI*)m_PaintManager.FindControl(_T("edit_end")))->GetText();
		if (!IsValidTime(strEndTime))
		{
			MessageBox(NULL, _T("終止時間有誤"), _T("MakeGif"), IDOK);
		}
		strCMD += strStartTime;
		strCMD += _T(" -to ");
		strCMD += strEndTime;
		strCMD += _T(" ");

		//輸出文件的路徑
		strCMD += strPath;
		strCMD += _T("11.mkv -y");

		//2.向cmd發送命令
		SendCmd(strCMD);
	}
	void LoadFile()//加載視頻文件的路徑
	{
		OPENFILENAME ofn;
		memset(&ofn, 0, sizeof(OPENFILENAME));
		//設置參數
		TCHAR strPath[MAX_PATH] = { 0 };//MAX_PATH--->260
		ofn.lStructSize = sizeof(OPENFILENAME);
		ofn.lpstrFile = strPath;
		ofn.nMaxFile = sizeof(strPath);
		ofn.lpstrFilter = _T("All(*.*)\0 *.*\0mkv(*.mkv)\0 *.mkv\0");//使用這個過濾串,我們這裏是過濾出mkv文件
		ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
		if (GetOpenFileName(&ofn))
		{
			//將文件的路徑設置到edit
			((CEditUI*)m_PaintManager.FindControl(_T("edit_path")))->SetText(strPath);//將路徑設置進編輯框中
		}
	}
	//(2)從截取的視頻中提取字幕(必須是外掛字幕)
	void GetSRTFile()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//獲取路徑
		strPath += _T("ffmpeg\\");
		//1.構造命令
		CDuiString strCMD;
		strCMD += _T("/c ");//構造命令期間第一條必須是'/c'
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");//要加上\c參數
		strCMD += strPath;
		strCMD += _T("11.mkv ");//視頻文件的路徑
		strCMD += strPath;
		strCMD += _T("input.srt -y");//輸出的字幕文件

		//2.向cmd發送命令
		SendCmd(strCMD);
	}

	//(3)編輯字幕,在窗口中編輯
	void LoadSRT()
	{
		//將srt格式的字幕文件,加載到界面中的list控件
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//得到文件的路徑
		strPath += _T("ffmpeg\\input.srt");
		std::ifstream fIn(strPath.GetData());//獲取字符串類型的文件

		char strSRTCon[512] = { 0 };
		CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));//控件都是通過繪畫管理器畫出來的,拿到list控件

		//給list中添加每一行
		pList->RemoveAll();//第二次獲取字幕時要清掉第一次獲取的字幕
		while (!fIn.eof())//文件指針有沒有在文件結尾
		{
			//讀取字幕序號
			fIn.getline(strSRTCon, 512);

			//給出list中的文本元素
			CListTextElementUI* pListItem = new CListTextElementUI;
			pList->Add(pListItem);

			//讀取時間軸
			fIn.getline(strSRTCon, 512);
			pListItem->SetText(0, UTF8ToUniCode(strSRTCon));
			//從0開始,將文本設置進去,但是此時strSRTCon是LPCTSTR類型的,
			//win32項目是基於Unicode的,不是ASCII形式的,因此要進行轉化(UTF-8--->Unicode)

			//讀取字幕
			fIn.getline(strSRTCon, 512);
			pListItem->SetText(1, UTF8ToUniCode(strSRTCon));

			//讀取空行
			fIn.getline(strSRTCon, 512);
		}
		fIn.close();
	}
	void WriteSRT()
	{
		// 獲取SRT文件的路徑
		CDuiString strPath = CPaintManagerUI::GetInstancePath();
		strPath += _T("ffmpeg\\input.srt");
		std::ofstream fOut(strPath.GetData());//寫到fout中

		// 1. 從List控件中獲取文本內容
		CListUI* pList = (CListUI*)m_PaintManager.FindControl(_T("List_srt"));
		int szCount = pList->GetCount();//得到總共的行

		for (int i = 0; i < szCount; ++i)
		{
			CListTextElementUI* pListItem = (CListTextElementUI*)pList->GetItemAt(i);

			// 序號
			CDuiString strNo;
			strNo.Format(_T("%d"), i + 1);//因爲srt文件標號是從1開始的,而i是從0開始的,所以要+1

			// 時間軸
			CDuiString strTime = pListItem->GetText(0);

			// 文本內容
			CDuiString strWord = pListItem->GetText(1);

			// 2. 將獲取到的內容寫會到srt文件中
			string strNewLine = Unicode2UTF8(_T("\n"));

			// 寫行號
			string itemNo = Unicode2UTF8(strNo);
			fOut.write(itemNo.c_str(), itemNo.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());

			// 寫時間軸
			string itemTime = Unicode2UTF8(strTime);
			fOut.write(itemTime.c_str(), itemTime.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());

			// 寫文本
			string itemWord = Unicode2UTF8(strWord);
			fOut.write(itemWord.c_str(), itemWord.size());
			fOut.write(strNewLine.c_str(), strNewLine.size());

			// 字幕和字幕之間都有個換行 
			fOut.write(strNewLine.c_str(), strNewLine.size());
		}

		fOut.close();
	}

	//(4)從截取的片段中提取視頻裸流
	void GenerateView()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//獲取路徑
		strPath += _T("ffmpeg\\");
		//1.構造命令
		CDuiString strCMD;
		strCMD += _T("/c ");//構造命令期間第一條必須是'/c'
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");//要加上\c參數
		strCMD += strPath;
		strCMD += _T("11.mkv -vcodec copy -an -sn ");
		strCMD += strPath;
		strCMD += _T("22.mkv -y");

		SendCmd(strCMD);
	}
	void BornSRTtoView()
	{
		//ffmpeg -i 22.mkv -vf subtitles=input.srt 33.mkv -y
		CDuiString strCMD;
		strCMD += _T("/c ");//構造命令期間第一條必須是'/c'
		strCMD += _T("cd ");
		strCMD += CPaintManagerUI::GetInstancePath() + _T("ffmpeg");//獲取路徑
		strCMD += _T(" & ");
		//構造命令
		strCMD += _T("ffmpeg -i 22.mkv -vf subtitles=input.srt 33.mkv -y");
		SendCmd(strCMD);
	}
	void GenerateGifWithView()
	{
		CDuiString strPath = CPaintManagerUI::GetInstancePath();//獲取路徑
		strPath += _T("ffmpeg\\");
		//1.構造命令
		CDuiString strCMD;
		strCMD += _T("/c ");//構造命令期間第一條必須是'/c'
		strCMD += strPath;
		strCMD += _T("ffmpeg -i ");//要加上\c參數
		strCMD += strPath;
		strCMD += _T("33.mkv -vf scale=iw/2:ih/2 -f gif ");
		strCMD += strPath;
		strCMD += _T("output.gif -y");

		SendCmd(strCMD);
	}

響應Cmd:

    void SendCmd(const CDuiString& strCMD)//向控制檯發命令
	{
		//1.初始化結構體
		SHELLEXECUTEINFO strSEInfo;
		memset(&strSEInfo, 0, sizeof(SHELLEXECUTEINFO));
		strSEInfo.cbSize = sizeof(SHELLEXECUTEINFO);
		strSEInfo.fMask = SEE_MASK_NOCLOSEPROCESS;

		strSEInfo.lpFile = _T("C:\\Windows\\System32\\cmd.exe");
		//windows命令行cmd所在的路徑,就是本機的cmd所在的路徑,一定要給對

		strSEInfo.lpParameters = strCMD;//打開程序的參數
		strSEInfo.nShow = SW_SHOW;//SW_HIDE:隱藏

		//調用命令行窗口,給命令行發消息,
		//在該函數中,會新創建一個進程,來負責調用命令行窗口執行命令
		ShellExecuteEx(&strSEInfo);

		//等待命令響應完成
		WaitForSingleObject(strSEInfo.hProcess, INFINITE);
		MessageBox(m_hWnd, _T("命令操作完成"), _T("MakeGif"), IDOK);
	}

時間判斷:

bool IsValidTime(CDuiString strTime)//判斷給的時間是否有效
	{
		//"00:40:07"---時間格式
		if (strTime.GetLength() != 8)
		{
			return false;
		}
		for (int i = 0; i < strTime.GetLength(); ++i)
		{
			if (strTime[i] == ':')
				continue;
			if (!(strTime[i] >= '0' && strTime[i] <= '9'))//不是數字
			{
				return false;
			}
		}
		return true;
	}

UTF-8與Unicode格式的相互轉換:

    CDuiString UTF8ToUniCode(const char* str)//UTF-8--->Unicode 
	{
		//第一次調用:獲取轉化之後的目標串的長度
		int szLen = ::MultiByteToWideChar(CP_UTF8, 0, str, strlen(str), NULL, 0);

		wchar_t* pContent = new wchar_t[szLen + 1];//爲目標串申請空間,要存儲\0

		//第二次調用:進行真正的轉化
		::MultiByteToWideChar(CP_UTF8, NULL, str, strlen(str), pContent, szLen);
		pContent[szLen] = '\0';
		CDuiString s(pContent);
		delete[]pContent;
		return s;
	}
	string Unicode2UTF8(CDuiString str)
	{
		int len = WideCharToMultiByte(CP_UTF8, 0, str.GetData(), -1, NULL, 0, NULL, NULL);
		CHAR *szUtf8 = new CHAR[len + 1]{0};

		::WideCharToMultiByte(CP_UTF8, 0, str.GetData(), -1, (LPSTR)szUtf8, len, NULL, NULL);
		string s(szUtf8);
		delete[] szUtf8;
		return s;
	}
項目測試

使用單元測試,採用白盒測試方法;
(1)加載文件測試:按鈕中實現加載按鈕,確認是否能夠獲取到文件的路徑;
(2)Cmd命令發送測試:打開控制檯Cmd,驗證能否使用命令行生成Gif成功;
(3)圖片生成Gif測試:按下生成Gif按鈕,在所給文件路徑中查看是否生成想要的Gif;
(4)視頻截取測試:按下按鈕,注意時間的判斷,輸入錯誤時間驗證是否是有效時間;
(5)視頻生成Gif測試:執行每步操作,測試想要的Gif是否生成;
總體測試:全部操作一遍,驗證是否與想要的功能對應。

項目結果

如圖:
主界面:
在這裏插入圖片描述
按相關按鈕即可進行生成

項目源碼

https://github.com/wangbiy/project/tree/master/Win32_2019_12_5_Project/Win32_2019_12_5_Project

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章