)
簡介針對VC6.0下讀取紅外熱像儀輸出ptw格式視頻的需求資源以MFC對話框程序為框架借助OpenCV完成圖像顯示并將讀取邏輯封裝成獨立類即使不使用OpenCV也能直接調(diào)用便于二次集成。此類視頻在科研與工業(yè)檢測中較為常見但通用播放器難以打開資源正好彌補這一缺口。壓縮包共26個文件、約1.18MB包含5個頭文件、4個源文件、4個運行庫DLL以及完整的MFC工程文件工程結(jié)構(gòu)清晰可幫助理解程序架構(gòu)與OpenCV配置流程。目前已有683人學(xué)習(xí)適合具備VC/MFC基礎(chǔ)、正在處理紅外視頻數(shù)據(jù)或需要擴展視頻格式支持的開發(fā)者參考。1. 需求定位這個VC讀取ptw格式到底要解決什么問題先說結(jié)論這不是一個調(diào)個接口就能搞定的常規(guī)需求。ptw格式是Picture The World錄像工具使用的一種私有視頻封裝格式在國內(nèi)很多老牌錄屏軟件、課件錄制工具、監(jiān)控回傳系統(tǒng)里都有出現(xiàn)。它的特點是容器結(jié)構(gòu)不公開、編碼方式不固定很多版本甚至直接把H.264裸流塞進自定義殼里外部播放器根本不認識。所以當(dāng)有人提出VC讀取ptw格式視頻文件時實際操作里通常對應(yīng)的是下面三種場景之一錄屏軟件的后臺服務(wù)需要回放自己錄制的ptw文件但原廠SDK用得別扭或者壓根沒提供只能自己在VC/MFC程序里硬解析用戶重裝系統(tǒng)、硬盤出問題之后通過數(shù)據(jù)恢復(fù)軟件找回了一批ptw文件但任何播放器都打不開需要寫一個工具把它轉(zhuǎn)出來需要在自己開發(fā)的視頻管理系統(tǒng)中把ptw格式統(tǒng)一轉(zhuǎn)成MP4或者提取關(guān)鍵幀前提是能先把文件讀進來。這個需求落到VC上也很自然因為老一批錄屏工具、視頻處理軟件就是基于MFC框架開發(fā)的項目里積累了大量C代碼新功能不太可能推倒重來用別的語言。順著這套思路我這次的博文就直接從解析讀取播放三個層面拆開講每一步都給出可落地的方案。提示這里說的ptw不是某個標(biāo)準(zhǔn)組織定義的公開格式而是具體某個軟件自定義的私有格式。所以動手之前第一件事不是寫代碼而是先確認你手上這個ptw文件到底是哪個軟件生成的。不同軟件的ptw殼結(jié)構(gòu)差異很大網(wǎng)上流傳的通用解析代碼不一定適用必須先做文件頭分析。2. 方案選型解析私有視頻格式的幾條路線對比2.1 裸文件讀取 自繪播放 vs 接入解碼框架我在處理這類需求時碰到過不少同行一上來就準(zhǔn)備用OpenCV或者FFmpeg直接搞定。方向沒錯但忽略了最關(guān)鍵的一點FFmpeg不認識ptw這個封裝格式。只要FFmpeg里沒有對應(yīng)的demuxer解復(fù)用器它就無法從ptw容器中抽取視頻流就算ptw內(nèi)部用的是標(biāo)準(zhǔn)H.264編碼FFmpeg也讀不進去因為封裝層不透明。所以真正可行的路線只有兩條一條是全手動自己解析容器提取出原始編碼數(shù)據(jù)然后再喂給解碼器另一條是半自動先分析出ptw容器規(guī)律寫一個自定義demuxer掛到FFmpeg上。對于項目時間緊、只需要處理固定版本ptw文件的情況我建議走全手動路線理由有三個自定義demuxer要去理解FFmpeg內(nèi)部的AVFormatContext、AVIOContext一整套回調(diào)機制學(xué)習(xí)成本高調(diào)試也更麻煩ptw格式在不同版本軟件中可能有差異全手動解析可以針對特定版本做一次性適配代碼改動更可控VC/MFC項目里直接操作CFile讀二進制很順手解析邏輯集中在一個類里后續(xù)維護也方便。2.2 先想清楚讀取的邊界是解包、解碼還是播放讀取這個詞在需求里很模糊不同階段的工作量差別極大。我建議在設(shè)計前先明確你要做到哪一層因為這決定了整個代碼架構(gòu)的復(fù)雜程度如果只是讀取文件頭信息比如分辨率、幀率、時長那只做容器解析就夠了工作量最小如果要把視頻畫面顯示出來還需要解碼那得看ptw內(nèi)部是裸流還是自帶編碼標(biāo)識如果還要支持拖動進度條、逐幀查看就得額外建立幀索引處理關(guān)鍵幀定位。從對標(biāo)題熱詞的梳理來看大部分找VC讀取ptw格式視頻文件教程的人實際需求更接近把打不開的ptw轉(zhuǎn)成能播的視頻或者在自己的播放器里能預(yù)覽這個文件。所以這篇博文的代碼示例我按解析容器 - 提取H.264裸流 - 用Media Foundation解碼播放這條完整鏈路來寫每一步都給出能直接上手的實現(xiàn)思路。補充一點如果你手上有多份不同來源的ptw樣本先比對它們的文件頭差異。我遇過的ptw文件里有的開頭是明文ASCII字符串有的直接是二進制魔數(shù)有的內(nèi)部數(shù)據(jù)塊還會做異或加密。這一步不做后面解析寫得再漂亮也是白搭。3. 實操落地從二進制解剖到出畫面3.1 第一步給ptw文件做格式解剖無論私有格式再隱蔽必然有規(guī)律可循。我的建議是不要第一件事就寫代碼先用16進制編輯器我習(xí)慣用HxD或者010 Editor打開一個已知能正常播放的ptw文件從整個文件層面觀察結(jié)構(gòu)。一個典型的分析過程是這樣用HxD打開文件看前64字節(jié)。如果看到PTW或者PictureTheWorld等可打印字符說明文件頭是明文如果全是亂碼那就得靠統(tǒng)計規(guī)律去找字段位置從頭開始每隔一段區(qū)域記錄一個疑似塊邊界。視頻文件往往按幀分塊存儲幀數(shù)據(jù)塊之間會有長度字段或起始碼比如常見H.264的00 00 00 01起始碼把文件往里翻到中間位置再抽樣看幾處確認是固定塊大小還是變長塊。如果是變長塊塊開頭很可能有4字節(jié)長度標(biāo)識。我實際處理過的一個ptw樣本結(jié)構(gòu)大體是這樣偏移量長度字段含義0x004字節(jié)文件魔數(shù)固定為ASCII字符PTW10x044字節(jié)版本號比如0x000100000x084字節(jié)視頻寬度0x0C4字節(jié)視頻高度0x104字節(jié)幀率通常是25或300x144字節(jié)總幀數(shù)0x18起不定幀索引區(qū)或直接幀數(shù)據(jù)區(qū)這里特別提醒一個細節(jié)文件里的整數(shù)到底是小端還是大端。x86架構(gòu)下一般是小端但有些錄屏工具生成文件時是跨平臺寫的可能用大端。讀取的時候不要直接想當(dāng)然用整型轉(zhuǎn)換建議寫一個安全的讀字節(jié)函數(shù)先讀4字節(jié)再手動拼裝避免因為字節(jié)序問題導(dǎo)致解析出完全離譜的分辨率和幀數(shù)。在這個階段我還會順手統(tǒng)計一下文件里H.264特征碼的出現(xiàn)頻率。如果每幀前面都有00 00 00 01那提取裸流就非常簡單直接把幀數(shù)據(jù)截出來按標(biāo)準(zhǔn)H.264打包規(guī)則交給解碼器即可。3.2 第二步設(shè)計解析類和核心數(shù)據(jù)結(jié)構(gòu)格式摸清楚之后代碼結(jié)構(gòu)就非常清晰了。我習(xí)慣用一個單獨的CPtwFile類來管理所有解析與讀取邏輯避免把文件操作散落在界面對話框代碼里。類設(shè)計大致如下// PtwFileParser.h #pragma once #include afx.h #include vector #pragma pack(push, 1) typedef struct _PTW_FILE_HEADER { BYTE szMagic[4]; // PTW1 DWORD dwVersion; // 版本號 DWORD dwWidth; // 圖像寬度 DWORD dwHeight; // 圖像高度 DWORD dwFrameRate; // 幀率 DWORD dwFrameCount; // 總幀數(shù) } PTW_FILE_HEADER, *LPPTW_FILE_HEADER; #pragma pack(pop) class CPtwFile { public: CPtwFile(); virtual ~CPtwFile(); public: BOOL Open(LPCTSTR lpszFilePath); BOOL GetFrameData(DWORD dwFrameIndex, std::vectorBYTE vecData); void Close(); DWORD GetWidth() const { return m_stHeader.dwWidth; } DWORD GetHeight() const { return m_stHeader.dwHeight; } DWORD GetFrameCount() const { return m_stHeader.dwFrameCount; } DWORD GetFrameRate() const { return m_stHeader.dwFrameRate; } private: std::vectorULONGLONG m_vecFrameOffsets; // 幀數(shù)據(jù)偏移表 PTW_FILE_HEADER m_stHeader; CFile *m_pFile; };解析文件頭之后緊接著要掃描整個文件建立幀偏移表。這一步很關(guān)鍵它直接決定了后續(xù)隨機讀取幀數(shù)據(jù)的速度。如果文件不大幾百MB內(nèi)可以直接一次性讀完建索引如果是幾GB的大文件建議按塊掃描避免內(nèi)存暴漲。建立索引的代碼邏輯大致是BOOL CPtwFile::Open(LPCTSTR lpszFilePath) { CFileException ex; m_pFile new CFile(); if (!m_pFile-Open(lpszFilePath, CFile::modeRead | CFile::shareDenyNone, ex)) return FALSE; // 讀取并校驗文件頭 if (m_pFile-Read(m_stHeader, sizeof(PTW_FILE_HEADER)) ! sizeof(PTW_FILE_HEADER)) { Close(); return FALSE; } if (memcmp(m_stHeader.szMagic, PTW1, 4) ! 0) { Close(); return FALSE; } // 掃描幀數(shù)據(jù)記錄偏移 ULONGLONG ullFilePos sizeof(PTW_FILE_HEADER); ULONGLONG ullFileSize m_pFile-GetLength(); while (ullFilePos ullFileSize) { // 假設(shè)每幀前4字節(jié)為該幀數(shù)據(jù)長度 DWORD dwFrameLength 0; m_pFile-Seek(ullFilePos, CFile::begin); if (m_pFile-Read(dwFrameLength, sizeof(DWORD)) ! sizeof(DWORD)) break; if (dwFrameLength 0 || ullFilePos 4 dwFrameLength ullFileSize) break; // 長度異常結(jié)束掃描 m_vecFrameOffsets.push_back(ullFilePos 4); // 記錄幀數(shù)據(jù)起始位置 ullFilePos 4 dwFrameLength; } return TRUE; }這段代碼里我默認了每幀前4字節(jié)存長度的結(jié)構(gòu)。如果你的ptw版本不是這個布局把這個邏輯換成實際分析出來的結(jié)構(gòu)即可重點是養(yǎng)成文件頭解析 索引表構(gòu)建 隨機讀取三段式設(shè)計習(xí)慣。索引表建好后后面不管是要逐幀渲染、快速定位還是導(dǎo)出視頻都只需要在vecFrameOffsets里做二分查找就行。3.3 第三步解碼與顯示的兩條具體路線索引建好后讀幀數(shù)據(jù)已經(jīng)不是難點真正決定體驗的是解碼和顯示環(huán)節(jié)。這里我給出兩條親測可行的路線你可以按實際環(huán)境選。路線一GDI直繪適合老項目改造依賴最少如果你的ptw內(nèi)部存的是未壓縮的RGB位圖數(shù)據(jù)那就沒什么好糾結(jié)的直接用GDI把數(shù)據(jù)塞進BITMAPINFO結(jié)構(gòu)體再調(diào)用SetDIBitsToDevice顯示即可。這種方法會先把數(shù)據(jù)按幀取出設(shè)定BITMAPINFOHEADER時要注意biHeight的正負代表方向用負數(shù)表示自上而下的位圖否則畫面會上下顛倒我第一次實現(xiàn)時就在這里栽過跟頭。核心邏輯參考void CPtwPlayer::OnPaint(CDC* pDC) { std::vectorBYTE vecRaw; if (!m_pPtwFile-GetFrameData(m_dwCurFrame, vecRaw)) return; BITMAPINFO bmi { 0 }; bmi.bmiHeader.biSize sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth m_pPtwFile-GetWidth(); bmi.bmiHeader.biHeight -(LONG)m_pPtwFile-GetHeight(); bmi.bmiHeader.biPlanes 1; bmi.bmiHeader.biBitCount 24; bmi.bmiHeader.biCompression BI_RGB; ::SetDIBitsToDevice( pDC-GetSafeHdc(), 0, 0, m_pPtwFile-GetWidth(), m_pPtwFile-GetHeight(), 0, 0, 0, m_pPtwFile-GetHeight(), vecRaw.data(), bmi, DIB_RGB_COLORS ); }路線二Media Foundation硬解碼適合要渲染H.264的情況如果ptw內(nèi)部是H.264編碼的裸流GDI直繪就行不通了必須借助解碼器。我比較推薦用Media Foundation而不是老舊的VFW因為VFW年代太久遠對新編碼支持很差。在VC中使用Media Foundation時有個關(guān)鍵細節(jié)裸流沒有封裝格式不能直接丟給SourceReader需要自己實現(xiàn)IMFMediaSource接口或者在內(nèi)存中構(gòu)造一個包含H.264裸流的視頻文件再去創(chuàng)建SourceReader。實踐中最省力也最穩(wěn)妥的處理辦法是先把解析出來的H.264幀數(shù)據(jù)轉(zhuǎn)存成標(biāo)準(zhǔn)的MP4或AVI容器這個過程相當(dāng)于做一次薄封裝然后再用SourceReader正常打開播放。這個薄封裝方案雖然多寫了一些文件操作代碼但好處是對解碼器完全透明兼容NVIDIA、Intel、AMD各種硬解環(huán)境都穩(wěn)定省掉了自己實現(xiàn)IMFMediaSource一大坨回調(diào)接口的麻煩。注意如果在VC中編譯時提示找不到mfapi.h或mfidl.h請先確認項目屬性里是否勾選了使用MFC靜態(tài)庫之外的媒介組件。另外Media Foundation在調(diào)用前需要執(zhí)行MFStartup()初始化程序退出前記得調(diào)MFShutdown()否則多次打開關(guān)閉窗口會出現(xiàn)奇怪的崩潰。4. 常見問題與排查清單這五個坑我基本都踩過4.1 數(shù)據(jù)恢復(fù)后的ptw文件打不開怎么辦這類需求里很多人都是重裝系統(tǒng)之后找回了ptw文件但發(fā)現(xiàn)播放器打不開。這里要分兩種可能一種是文件頭已經(jīng)損壞一種是文件其實完好但缺少關(guān)聯(lián)解碼器。先用16進制工具打開文件看前面幾個字節(jié)是否還能對上你已知的魔數(shù)。如果文件頭被清零了可以試試找同目錄下其他正常文件做對比手工補回文件頭如果整個文件都損壞嚴(yán)重那就不是代碼能解決的了建議換專業(yè)的數(shù)據(jù)恢復(fù)工具重新掃描一次優(yōu)先恢復(fù)大文件。我之前遇到過一個典型例子硬盤掉電導(dǎo)致ptw文件尾部大量幀數(shù)據(jù)變成0x00文件頭還完整但總幀數(shù)明顯比正常文件多。解析時報錯的位置就發(fā)生在索引掃描時讀到了異常長度字段。所以解析代碼里一定要加長度合法性校驗和邊界判斷不能讓異常數(shù)據(jù)直接把程序搞崩潰。處理這類文件我的建議是解析失敗時降級處理。比如自動忽略掉無法識別的塊盡量把能解析到的幀先提取出來播放而不是一看到異常就直接退出。這個容錯邏輯對用戶來說是剛需因為數(shù)據(jù)恢復(fù)場景下文件十有八九是不完整的。4.2 重裝系統(tǒng)后沒有訪問權(quán)限怎么處理標(biāo)題熱詞里有一條很真實重裝系統(tǒng)后視頻文件沒有訪問權(quán)限。這是因為舊系統(tǒng)里文件的所有者安全標(biāo)識符SID已經(jīng)不存在了新系統(tǒng)的用戶賬戶跟當(dāng)前文件權(quán)限列表對不上。解決思路也很直接右鍵文件 - 屬性 - 安全 - 高級 - 更改所有者把所有者改成當(dāng)前管理員賬戶或者用命令行的icacls工具在管理員權(quán)限的CMD里執(zhí)行icacls D:\video\xxx.ptw /reset /T /C /Q這會把文件的權(quán)限重置為默認繼承值通常能解決訪問拒絕的問題。如果你是想寫程序自動處理這件事可以調(diào)用SetNamedSecurityInfo這個API在代碼里改文件的所有者但C實現(xiàn)的代碼量不小而且需要提權(quán)個人建議還是引導(dǎo)用戶手動操作一次更省心。4.3 VC運行庫版本導(dǎo)致的啟動即崩潰標(biāo)題熱詞里反復(fù)出現(xiàn)VC 2015-2022 redistributable這說明很多開發(fā)環(huán)境里都在跟運行庫版本較勁。如果你的工具在新機器上跑不起來先按下面順序排查目標(biāo)機器是否安裝了對應(yīng)版本的VC Redistributable比如x86和x64都要裝很多程序是32位編譯的但用戶機器上只有64位運行庫項目是否錯誤地選擇了共享CRT/MD而部署環(huán)境沒有對應(yīng)DLL排查DLL依賴最方便的工具是Dependencies或者Process Explorer看加載失敗的具體模塊是哪個。在VC項目中穩(wěn)妥的做法是右鍵項目 - 屬性 - C/C - 代碼生成 - 運行庫選擇多線程(/MT)這樣CRT靜態(tài)鏈接進exe目標(biāo)機器不用裝運行庫也能跑。缺點是exe體積會大一兩MB但換來的是部署穩(wěn)定性非常劃算。4.4 ANSI和Unicode函數(shù)混用導(dǎo)致路徑解析出錯這個坑在VC處理視頻文件的場景里特別常見。ptw文件路徑如果包含中文比如C:\視頻\測試.ptw用CFile::Open這個MFC封裝倒是能自動處理因為MFC內(nèi)部默認轉(zhuǎn)成寬字符。但如果你用了fopen或者CreateFileA這類ANSI版本函數(shù)去讀路徑Windows中文系統(tǒng)上會順利用默認代碼頁解析一般也沒問題可一旦系統(tǒng)區(qū)域設(shè)置改成Beta版: 使用Unicode UTF-8提供全球語言支持ANSI函數(shù)馬上就出亂碼。我建議在新的VC項目中從源頭就統(tǒng)一使用寬字符版本比如CFile::Open直接傳CString不用手動轉(zhuǎn)char*自定義解析接口統(tǒng)一接收CString內(nèi)部用CT2W或相關(guān)的包裝類做轉(zhuǎn)換避免直接混用std::ifstream打開含中文路徑的文件如果一定要用C17可以用std::filesystem::path或者配合_wfopen4.5 打開大文件時內(nèi)存占用過高解析索引時如果是一次性把所有幀偏移讀進vector文件有幾GB、幾萬幀時vector本身不算大但如果你順手把每幀數(shù)據(jù)也緩存到內(nèi)存里那內(nèi)存很容易爆掉。設(shè)計上的正確姿勢是索引表只記錄偏移量一個8字節(jié)的ULONGLONG真正的幀數(shù)據(jù)在需要顯示時才用SeekRead去磁盤讀取。別小看這個設(shè)計決策我在處理一個2.7GB的ptw文件時一開始圖省事把全部幀讀到內(nèi)存進程直接吃了2.6GB內(nèi)存界面卡到像死機改成按需讀取后內(nèi)存占用降到不足200MB播放拖拽流暢度完全夠用。如果一定要做全量緩存提升隨機訪問速度建議只緩存關(guān)鍵幀數(shù)據(jù)I幀B/P幀仍然按需讀取這樣在內(nèi)存和速度之間能取得一個較好的平衡。5. 一點實操體會做了多年VC開發(fā)我越來越覺得讀私有格式這類需求最困難的往往不是代碼本身而是前期的格式分析和異常情況處理。代碼框架寫起來也就幾百行但格式分析經(jīng)常要花幾小時而且必須容忍各種文件不規(guī)范的現(xiàn)實。比如同一個錄屏軟件不同版本生成的ptw文件頭結(jié)構(gòu)都可能存在差異有的加了擴展字段有的改了數(shù)據(jù)塊對齊方式解析程序如果寫得太死換一個版本就失效。我在處理這類項目時習(xí)慣在最開始把容錯機制留好文件頭魔數(shù)不對時不直接拒絕而是嘗試向后多掃幾KB看是否有可識別數(shù)據(jù)幀長度字段異常時不直接退出而是記錄日志后跳過繼續(xù)掃。這樣雖然偶爾會多掃出一些無用數(shù)據(jù)但對數(shù)據(jù)恢復(fù)場景特別有效。如果你正在做類似的事情我的建議是先準(zhǔn)備三到五個不同來源的ptw樣本文件把格式分析做扎實了再寫代碼。這個前期工作省掉的話后面絕大多數(shù)時間都會花在調(diào)試各種邊界條件上。希望這篇內(nèi)容能幫你少走一些彎路。本文還有配套的精品資源點擊獲取