項目:私“圖”定製——利用ffmpeg製作Gif

目錄:


項目背景

可行性方面

需求分析

詳細設計

測試

維護

項目效果圖展示

完整代碼


項目背景:

文字信息時代,傳統的文字聊天方式已不能滿足大衆的需求,很多時候文字不能表達自己的想法,或者溝通技巧的欠缺,後就成爲了尬聊。"一言不合就鬥圖",能用一張圖說明的。

暴走表情廣泛的遍佈於網絡,網民們大多用作鬥圖。一般常見於QQ、微信。鬥圖活動起始於QQ,羣聊時大家發送搞趣圖片以相互娛樂。後來發展到百度貼吧等各種論壇上,時常有人發帖組織鬥圖活動。

鬥圖發展到現在不僅僅用這些表情圖,還會惡搞明星、電影人物。以圖加文字的自由組合,結合網絡語言,製作搞笑逗比的圖片。

可行性方面:

  • 經濟可行性——低成本
  • 操作可行性——簡單
  • 技術可行性——藉助其他工具

需求分析:

製作Gif動圖,因此選擇兩種生成方式:1.圖片生成;2.視頻生成。

技術方面藉助其他的工具實現:選擇 ffmpeg 工具。

FFmpeg即是一塊音視頻編解碼工具,同時也是一組音視頻編解碼開發套件,爲開發者提供了豐富的音視頻 處理調用接口。FFmpeg中的"FF"指的是"Fast Forward",mpeg則是動態圖像專家組。 它提供了錄製、轉換 以及流化音視頻的完整解決方案。 它包含了非常先進的音頻/視頻編解碼庫 libavcodec, 爲了保證高可移植 性和編解碼質量, libavcodec 裏很多 codec 都是從頭開發的。

FFmpeg項目由以下幾部分組成:

  • 1. ffmpeg 視頻文件轉換命令行工具,也支持經過實時電視卡抓取和編碼成視頻文件。
  • 2. ffserver 基於 HTTP、RTSP 用於實時廣播的多媒體服務器.也支持時間平移。
  • 3. ffplay 用 SDL 和 FFmpeg 庫開發的一個簡單的媒體播放器。
  • 4. libavcodec 一個包含了所有 FFmpeg 音視頻編解碼器的庫.爲了保證優性能和高可複用性,大多數編解 碼器從頭開發的。
  • 5. libavformat 一個包含了所有的普通音視格式的解析器和產生器的庫

學習ffmpeg 工具我們可以知道:ffmpeg 是使用命令行的形式,給予 cmd 一定的命令,實現相應的操作。

因此我們的實驗原理:在程序中通過 cmd 控制檯調用 ffmpeg.exe 工具,並給該工具發送對應的命令,完成所需操作,發命令時,cmd窗口隱藏在後臺。

總體設計:

本項目有兩種生成gif動態圖方式: 1. 使用圖片生成 2. 使用短視頻生成

實現原理:在程序中通過cmd控制檯調用ffmpeg.exe工具,並給該工具發送對應的命令,完成所需操作,發命令時,cmd窗口隱藏在後臺。

我們還需要有一定的操作界面:依靠 duilib 庫。

DuiLib庫是一款由杭州月牙兒網絡技術有限公司開發,輕量級的C++界面開發庫,遵循開源BSD協議,可以免費用於商業項目。Duilib界面庫的優勢在於:

  • 1. 基於GDI在窗口上自繪,無其他依賴,未使用特殊或危險的系統調用,能夠很好的解決傳統MFC界面的一系列問題
  • 2. 使用XML來描述界面風格,界面佈局,將界面和邏輯分離,同時易於實現各種超炫的界面效果如換色,換膚,透明等
  • 3. 完全兼容ActiveX控件(如常見的IE控件和Flash),也可以和MFC等界面庫配合使用
  • 4. 可廣泛用於互聯網客戶端、工具軟件客戶端、管理系統客戶端、多媒體客戶端(如KTV、觸摸屏)、車載電腦系統、gps系統和手機客戶端軟件等。

許多知名公司都採用Duilib作爲界面庫,比如:華爲網盤、PPS、金山快盤、酷我音樂、愛奇藝視頻、百度殺毒、百度衛士等一些列產品。

注意:Duilib僅僅是基於Win32的一套UI庫。因此要了解 Win32 程序相關知識。


Win32 的相關知識:

一個Win32應用程序可以分爲程序代碼和UI資源兩大部分,兩部分終是以rc整合成一個完整的exe可執行程序。所謂UI資源,指的是功能菜單、對話框外貌、程序圖標、光標形狀等東西。

代碼示例,展示Win32 界面:

 #include <Windows.h> 
#include <tchar.h>
 
 
//消息回調函數 
LRESULT CALLBACK WinProc(HWND hWnd, UINT message, 
                            WPARAM wParam, LPARAM lParam) {    
    switch (message) {    
        case WM_CLOSE:        
            if (IDOK == MessageBox(hWnd, _T("你確定退出?"), 
                    _T("退出"), MB_OKCANCEL))  {            
                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) {
    //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, //菜單句柄      
		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,WinMain的四個參數由操作系統負責傳遞,main是控制檯程序的入 口點。

從上文可以看出,一個簡單的Win32程序包括以下步驟:

  • 1. 註冊窗口類:RegisterClass 窗口創建前,對窗口的屬性進行一些設置,主要是窗口的外貌以及行爲,比如:窗口的邊框、顏色、位 置、標題、顏色、大小等就是窗口的外貌,窗口接受到消息後如何響應,就是爲該窗口綁定窗口處理函數,窗口在創建前,必須使用RegisterClass函數告訴系統。
  • 2. 創建窗口:CreateWindow 按照設置的窗口大小、窗口風格、窗口標題、窗口位置等將窗口創建成功。注意:CreateWindow
  • 3. 顯示窗口和更新窗口將窗口在界面中展示出來。
  • 4. 消息循環不斷從應用程序消息隊列中獲取消息,交給註冊窗口時設定的消息響應函數進行處理。

 Win32的消息循環:

什麼是消息?

Windows程序的運行是依靠外部的事件來驅動。換句話說,程序不斷等待,等待任何可能的輸入,然後做出 判斷,再做適當的處理。上述的“輸入”是由操作系統捕獲到後,以消息的形式發送給應用程序。消息,其實 就是系統內設的一種數據結構:

typedef struct MSG {     
    HWND hwnd;//hwnd 是窗口的句柄,這個參數將決定由哪個窗口過程函數對消息進行處理      
    UINT message;      //message是一個消息常量,用來表示消息的類型      
    WPARAM wParam;     //32 位的附加信息,具體表示什麼內容,要視消息的類型而定      
    LPARAM lParam;     //32 位的附加信息,具體表示什麼內容,要視消息的類型而定      
    DWORD time;       //time 是消息發送的時間      
    POINT pt;         //消息發送時鼠標所在的位置 
}

從上面的消息定義可以看出,消息類型其實就是一個UINT類型的變量。系統定義消息值的範圍是:0x00000x03ff,用戶自定義消息值的範圍是:0x0400-0x07ff,爲了便於使用,系統定義了一個宏WM_USER來表示 用戶自定義消息的起始值,#define WM_USER 0x0400。

消息的分類

消息類型可以分爲兩大類:系統定義消息和用戶自定義消息。

系統定義消息分爲:窗口消息、命令消息、控件通知消息。

  • 1. 窗口消息 與窗口的內部運作有關的消息,如創建窗口,繪製窗口,銷燬窗口等  可以是一般的窗口,也可以是 MainFrame,Dialog,控件等。 如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等。
  • 2. 命令消息 當用戶從菜單選中一個命令項目、按下一個快捷鍵、點擊工具欄上的一個按鈕或者點擊控件都將發送 WM_COMMAND命令消息。通過消息結構中的wParam和lParam成員就能清楚得知道消息的來源。
  • LOWORD(wParam):代表菜單ID、或控件ID,或快捷鍵ID;
  • HIWORD(wParam):表示通知碼,當消息是 從菜單發出時,則這個值爲0,當消息是從快捷鍵發出時,這個值爲1,當消息是從控件發出時,這個值 爲通知碼,比如按鈕的通知碼:BN_CLICKED, BN_DBLCLK等。

消息隊列:

應用程序獲取到各種消息,由硬件產生的消息(如:鼠標移動或鍵盤按下),放在系統消息隊列中,windows 系統或其他windows程序傳送過來的消息,放在應用程序的消息隊列中,然後由應用程序不斷的將消息取走 進行響應。其實就是,程序中有一個獲取消息的循環代碼,會不斷的從操作系統中獲取消息。

消息是否被放進消息隊列,可將消息分爲:

  • 隊列消息:一般,程序都是從消息隊列中獲取消息。消息會先保存在消息隊列中,消息循環會從此隊列中取出消息並分發到各窗口處理 ,如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及鼠標,鍵盤消息等。其中,WM_PAINT,WM_TIMER只有在隊列中沒有其他消息的時候纔會被處理,WM_PAINT消息 還會被合併以提高效率。其他所有消息以先進先出(FIFO)的方式被處理。 系統中維護着一個全局的系統消息隊列,還會爲每一個UI線程維護一個UI線程消息隊列。當系統消息隊列中存在消息時,系統會根據消息所屬的UI線程,分發到應用程序對應的UI線程消息隊列中去。
  • 非隊列消息:但是還有一部分消息會繞過消息隊列,直接發送到窗口過程進行處理 。如WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR,WM_WINDOWPOSCHANGED。

詳細設計:

1.相關環境的配置。VS2013導入 duilib 庫。

參考網上專業博客教程:https://www.cnblogs.com/Alberl/p/3342030.html

2.學習duilib庫的使用。

學習專業博客:https://www.xuebuyuan.com/1656742.html

3.各個部分的代碼實現:

  • 功能設置部分:
class CDuiFrameWnd : public WindowImplBase
{
public:
	virtual LPCTSTR    GetWindowClassName() const   { return _T("DUIMainFrame"); }
	virtual CDuiString GetSkinFile()                { return _T("duilib.xml"); }
	virtual CDuiString GetSkinFolder()              { return _T(""); }

	//MessageBox(NULL, _T("路徑"), _T("測試"), IDOK);
	virtual void Notify(TNotifyUI& msg){
		CDuiString sCtrlName = msg.pSender->GetName();
		if (msg.sType == _T("click")) {
			if (sCtrlName == _T("closebtn")) {
				Close();
				return;
			}
			else if (sCtrlName == _T("minbtn")) {
				SendMessage(WM_SYSCOMMAND, SC_MINIMIZE, 0);
				return;
			}
			else if (sCtrlName == _T("maxbtn")) {
				SendMessage(WM_SYSCOMMAND, SC_MAXIMIZE, 0);
				return;
			}
			else if (sCtrlName == _T("restorebtn")) {
				SendMessage(WM_SYSCOMMAND, SC_RESTORE, 0);
				return;
			}
			else if (sCtrlName == _T("Look")) {
				LoadFile();
				return;
			}
			else if (sCtrlName == _T("CreatGif")) {
				CComboBoxUI* pCombox = (CComboBoxUI*)m_PaintManager.FindControl(_T("Combox_Select"));
				int i = pCombox->GetCurSel();
				if (i==0) {		//選擇方式生成方式:視頻 or 圖片
					GenerateGifWithPic();
				}
				else {
					GenerateGifWithView();
				}
				return;
			}
			else if (sCtrlName == _T("cut")) {
				Cut();
				return;
			}
			return;
		}
	}
  • 加載文件
	//加載文件
	void LoadFile() {
			TCHAR  szPeFileExt[1024] = TEXT("*.*");  //打開任意類型的文件
			TCHAR szPathName[80*MAX_PATH];               //文件路徑大小
			OPENFILENAME ofn = { sizeof(OPENFILENAME) };
			//ofn.hwndOwner = hWnd;// 打開OR保存文件對話框的父窗口
			ofn.lpstrFilter = szPeFileExt;
			lstrcpy(szPathName, TEXT(""));
			ofn.lpstrFile = szPathName;
			ofn.nMaxFile = sizeof(szPathName);//存放用戶選擇文件的 路徑及文件名 緩衝區
			ofn.lpstrTitle = TEXT("選擇文件");//選擇文件對話框標題
			ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT;//如果需要選擇多個文件 則必須帶有  OFN_ALLOWMULTISELECT標誌
			BOOL bOk = GetOpenFileName(&ofn);
			if (bOk) {
				CEditUI* pPathEdit = (CEditUI*)m_PaintManager.FindControl(_T("Edit"));
				pPathEdit->SetText(szPathName);
			}
	}
  • 給cmd發送命令

	void GenerateGifWithPic() {
		//CDuiString strCMD(_T(" ffmpeg -r 1 -i D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\out.gif"));
		//構造命令
		CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
		CDuiString strPictruePath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg ");
		CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\out.gif");
		CDuiString strCMD = (_T("/c "));
		strCMD += strFFmpegPath;
		strCMD += _T("-r 1 -i ");
		strCMD += strPictruePath + strOutPath;
		SendCmd(strCMD);
		MessageBox(m_hWnd, _T("圖片方式生成Gif成功!"), _T("GIFF"), IDOK);

	}
  • 視頻截取部分
void Cut() {
		CDuiString strStartTime = ((CEditUI *)m_PaintManager.FindControl(_T("BeginTime")))->GetText();
		if (!IsVaildTime(strStartTime)) {
			MessageBox(m_hWnd, _T("輸入起始時間有誤!"), _T("GIFF"), IDOK);
			return;
		}
		CDuiString strFinishTime = ((CEditUI *)m_PaintManager.FindControl(_T("EndTime")))->GetText();
		if (!IsVaildTime(strFinishTime)) {
			MessageBox(m_hWnd, _T("輸入結束時間有誤!"), _T("GIFF"), IDOK);
			return;
		}
		//構造截取視頻的命令
		CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
		CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.mp4");
		CDuiString strCMD = (_T("/c "));
		strCMD += strFFmpegPath;
		strCMD += _T(" -ss ");
		strCMD += strStartTime;
		strCMD += _T(" -to ");
		strCMD += strFinishTime;
		strCMD += _T(" -i ");
		CDuiString strViewPath = ((CEditUI *)m_PaintManager.FindControl(_T("Edit")))->GetText();
		strCMD += strViewPath;
		strCMD += _T(" -vcodec copy -acodec copy ");
		strCMD += strOutPath;
		SendCmd(strCMD);
		MessageBox(m_hWnd, _T("視頻截取成功!"), _T("GIFF"), IDOK);
	}
  • 發送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");

		////構造命令
		//CDuiString strPictruePath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg ");
		//CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\out.gif");
		//CDuiString strCMD = (_T("/c "));
		//strCMD += strFFmpegPath;
		//strCMD += _T("-r 1 -i ");
		//strCMD += strPictruePath + strOutPath;
		strSEInfo.lpParameters = strCMD;
		strSEInfo.nShow = SW_HIDE; //隱藏cmd'窗口

		//2. 發送cmd命令
		ShellExecuteEx(&strSEInfo);
		WaitForSingleObject(strSEInfo.hProcess, INFINITE);
		//D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg ffmpeg -r 1 -i D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\Pictrue\\%d.jpg D:\項目\項目1\ProjectZhou\Debug\\ffmpeg\\ffmpeg\\out.gif
	}
  • 時間判斷:
bool IsVaildTime(const CDuiString& strTime) {
		if (strTime.GetLength() != 8) {
			return false;
		}
		for (int i = 0; i < 8;i++) {
			if (':'==strTime[i]) {
				continue;
			}
			else if (isdigit(strTime[i])) {
				continue;
			}
			else {
				return false;
			}
		}
		return true;
	}
  • 視頻生成

	//視頻生成
	void GenerateGifWithView() {	
		//構造命令
		CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
		CDuiString strViewPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.mp4 ");
		CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\outView.gif");
		CDuiString strCMD = (_T("/c "));
		strCMD += strFFmpegPath;
		strCMD += _T("-r 50 -i ");
		strCMD += strViewPath + strOutPath;
		SendCmd(strCMD);
		MessageBox(m_hWnd, _T("視頻方式生成Gif成功!"), _T("GIFF"), IDOK);
	}
  • ASS生成:
	//生成ASS
	void CreatASS() {
		//構造命令
		CDuiString strFFmpegPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\ffmpeg ");
		CDuiString strViewPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.mp4 ");
		CDuiString strOutPath = CPaintManagerUI::GetInstancePath() + _T("\\ffmpeg\\ffmpeg\\output.ass ");
		CDuiString strCMD = (_T("/c "));
		strCMD += strFFmpegPath;
		strCMD += _T("-i ");
		strCMD += strViewPath;
		strCMD += _T("-an -vn -scodec copy ");
		strCMD += strOutPath;
		SendCmd(strCMD);
		MessageBox(m_hWnd, _T("視頻方式生成Gif成功!"), _T("GIFF"), IDOK);
	}
  • 主函數部分:
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
	CPaintManagerUI::SetInstance(hInstance);

	CDuiFrameWnd duiFrame;
	duiFrame.Create(NULL, _T("DUIWnd"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
	duiFrame.CenterWindow();
	duiFrame.ShowModal();
	return 0;
}
  • 依靠duilib中的工具進行界面設計:

 Duilib的界面佈局器:使用Duilib自帶的界面佈局器打開XML文件,進行自己編輯。

注:這個界面只是自己單獨實現的,字幕功能還沒有實現,完善,是爲需求分析中所實現的功能接口,還並未實現,在不斷的完善。

測試:

測試方式:單元測試,且測試方法採用白盒測試

文件加載測試:按鈕中實現選擇文件,確認是否能夠選擇。

cmd 命令發送測試:打開 cmd 命令行,在程序中查看輸出的命令,將輸出命令輸入命令行,驗證命令輸出組裝是否成功。

視頻剪切測試:和 cmd 命令測試類似,打開 cmd 命令行,在程序中查看輸出的命令,將輸出命令輸入命令行,驗證命令輸出組裝是否成功。注意時間的判斷,輸入錯誤時間驗證是否有效時間。

視頻生成測試:打開 cmd 命令行,在程序中查看輸出的命令,將輸出命令輸入命令行,驗證命令輸出組裝是否成功。

總體測試:完整操作,驗證是否和功能對應。


維護:

主要完善性維護。功能的完善,在字幕。

其中有適應性維護,測試中圖片的規格過於大,顯示上會有一點兒的延遲。


項目效果圖展示:

主界面:


圖片方式生成:

生成位置:工程中ffmpeg位置的默認位置—— 那個就是最終圖片生成的Gif圖。


視頻方式生成效果展示圖:

這裏字幕實現忽略,後續完善,直接生成Gif:

完整代碼:

https://github.com/Zzzhouxiaochen/Gif

 

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