文章目录
- 前言
- 一、先看效果
- 二、开始写代码
- 1.遍历文件
- 2.获取视频标题和视频名称
- 3.生成视频
- 4.主函数
- 总结
前言
手机bilibili缓存了很多视频,想导入电脑看,但发现缓存的视频被分割成两个文件:音频(audio.m4s)和视频(video.m4s),上网查了下教程,说下载安装ffmpeg,在音视频文件夹下,打开dos窗口,输入命令:
ffmpeg -i video.m4s -i audio.m4s -codec copy output.mp4
可行是可行,但是我视频比较多,而且没有文件名,一个个这样来一遍也太麻烦了,自己动手丰衣足食,写一个自动生成的;
备注:手机bilibili的缓存目录位于:/Android/data/tv.danmaku.bili/download
新版本已经上线,修复了部分bug,详情请查靠:C++调用ffmpeg批量合并bilibili缓存视频 2.0
一、先看效果
将bilili缓存视频的文件夹拷贝到程序生成目录下,如下图:
然后直接运行程序,会创建文件夹,并将视频生成到指定文件夹下面,效果如下:
二、开始写代码
1.遍历文件
因为每一个缓存视频的信息都存放在一个文件夹中,故针对每一个视频文件,遍历主要获取信息有:
1)存放视频标题和视频名称的json文件
2)文件audio.m4s路径
3)文件video.m4s文件路径
示例代码如下:
// 使用全局变量了,保存每条视频信息
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <thread>
#include <windows.h>
#include <string.h>
#include <io.h>
#include <direct.h>
using namespace std;
// 这里是我将jsoncpp封装成一个库了,方便使用
#include "../jsontool/jsontool.h"
#pragma comment(lib, "JsonTool.lib")
#define AudioName "audio.m4s"
#define VideoName "video.m4s"
#define JsonName "entry.json"
vector<string> g_vecTitleNames;
vector<string> g_vecPartNames;
vector<string> g_vecVideoPaths;
vector<string> g_vecAudioPaths;
int FindFile(const char* pszPath)
{
if(pszPath == NULL)
{
return -1;
}
int iRet = 0;
char szFindPath[MAX_PATH] = { 0 };
DWORD dwFileAttributes;
string strFileName;
WIN32_FIND_DATA wfd;
sprintf(szFindPath, "%s*.*", pszPath);
HANDLE hFindFile = ::FindFirstFile(szFindPath, &wfd);
if (hFindFile == INVALID_HANDLE_VALUE)
{
// 没有找到任何文件
return ERR_NO_FIND;
}
// 找到文件,开始遍历
strFileName = wfd.cFileName;
while (strFileName.size() > 0)
{
// 过滤 . 和 ..
if (strFileName != "." && strFileName != "..")
{
dwFileAttributes = wfd.dwFileAttributes;
if (dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY) // 目录
{
// 如果是目录,则继续递归查找
// 查找json所在路径
char szSubFindPath[MAX_PATH] = { 0 };
sprintf(szSubFindPath, "%s%s\\", pszPath, strFileName.c_str());
iRet = FindFile(szSubFindPath);
if (iRet != 0)
{
break;
}
}
else if (dwFileAttributes == FILE_ATTRIBUTE_ARCHIVE) // 文件
{
if (strFileName == JsonName)
{
// 通过json文件路径获取title和part
char szJsonPath[MAX_PATH] = { 0 };
char szTitleName[MAX_PATH] = { 0 };
char szPartName[MAX_PATH] = { 0 };
sprintf(szJsonPath, "%s%s", pszPath, JsonName);
GetTitleAndPage(szJsonPath, szTitleName, szPartName);
g_vecTitleNames.push_back(szTitleName);
g_vecPartNames.push_back(szPartName);
// 做个取巧处理,按bilibili的文件存储方式,读vidio会在json之前,防止vedio未下载完成的情况
int iTotal = g_vecTitleNames.size();
// 补齐vector
if (iTotal > g_vecVideoPaths.size())
{
g_vecVideoPaths.push_back("");
}
// 补齐vector
if (iTotal > g_vecAudioPaths.size())
{
g_vecAudioPaths.push_back("");
}
//cout << szTitleName << " " << szPartName << endl;
}
else if (strFileName == VideoName)
{
// video找到了,可以直接返回了,不用再往下找
g_vecVideoPaths.push_back(string(pszPath) + VideoName);
g_vecAudioPaths.push_back(string(pszPath) + AudioName);
//cout << pszPath << " " << VideoName << endl;
//cout << pszPath << " " << AudioName << endl;
break;
}
}
}
// 查找下一个文件
if (!::FindNextFile(hFindFile, &wfd))
{
break;
}
strFileName = wfd.cFileName;
}
::FindClose(hFindFile);
return iRet;
}
2.获取视频标题和视频名称
获取到json文件后,需要获取其中的视频标题和视频名称;但是json文件编码是utf-8,故需要转码;
代码如下(示例):
string UnicodeToANSI(const wstring& str)
{
char* pElementText;
int iTextLen;
// wide char to multi char
iTextLen = WideCharToMultiByte(CP_ACP,
0,
str.c_str(),
-1,
NULL,
0,
NULL,
NULL);
pElementText = new char[iTextLen + 1];
memset((void*)pElementText, 0, sizeof(char) * (iTextLen + 1));
::WideCharToMultiByte(CP_ACP,
0,
str.c_str(),
-1,
pElementText,
iTextLen,
NULL,
NULL);
string strText;
strText = pElementText;
delete[] pElementText;
return strText;
}
wstring UTF8ToUnicode(const string& str)
{
int len = 0;
len = str.length();
int unicodeLen = ::MultiByteToWideChar(CP_UTF8,
0,
str.c_str(),
-1,
NULL,
0);
wchar_t * pUnicode;
pUnicode = new wchar_t[unicodeLen + 1];
memset(pUnicode, 0, (unicodeLen + 1)*sizeof(wchar_t));
::MultiByteToWideChar(CP_UTF8,
0,
str.c_str(),
-1,
(LPWSTR)pUnicode,
unicodeLen);
wstring rt;
rt = (wchar_t*)pUnicode;
delete pUnicode;
return rt;
}
int GetTitleAndPage(const char* pszJsonPath, char* pszTitle, char* pszPage)
{
string strValue;
char szValue[4096] = { 0 };
std::ifstream is;
is.open(pszJsonPath, std::ios::binary);
while (!is.eof())
{
// json数据有空格,只能一行一行读
memset(szValue, 0, sizeof(szValue));
is.getline(szValue, sizeof(szValue));
strValue += szValue;
}
// 将jsoncpp封装成一个类,方便调用,给公司项目用了,不方便提供
CJsonTool json;
int iRet = json.InitJson(strValue.c_str());
//cout << "InitJsonStr: " << iRet << endl;
char szBuff[MAX_PATH] = { 0 };
iRet = json.GetStr("page_data", "part", szBuff, MAX_PATH);
strValue = UnicodeToANSI(UTF8ToUnicode(szBuff));
int iFindIndex = 0;
// 去掉空格
while ((iFindIndex = strValue.find(" ")) != string::npos)
{
strValue.replace(iFindIndex, 1, "");
}
//cout << "GetJsonStr: " << iRet << " " << strValue << endl;
if (iRet == 0)
{
strcpy(pszPage, strValue.c_str());
}
iRet = json.GetStr("", "title", szBuff, MAX_PATH);
strValue = UnicodeToANSI(UTF8ToUnicode(szBuff));
// 去掉空格
while ((iFindIndex = strValue.find(" ")) != string::npos)
{
strValue.replace(iFindIndex, 1, "");
}
//cout << "GetJsonStr: " << iRet << " " << strValue << endl;
if (iRet == 0)
{
strcpy(pszTitle, strValue.c_str());
}
is.close();
return 0;
}
3.生成视频
遍历完所有文件夹后,所有信息都应该保存到全局的4个vector中,直接遍历容器生成文件;
代码如下(示例):
void GenerateVideo(const char* pPath)
{
if (g_vecAudioPaths.size() == 0 ||
g_vecAudioPaths.size() != g_vecAudioPaths.size() ||
g_vecAudioPaths.size() != g_vecTitleNames.size() ||
g_vecAudioPaths.size() != g_vecPartNames.size())
{
cout << "查找视频资源失败" << endl;
return;
}
cout << "--------------------开始合成-------------------------" << endl;
char szSavePath[MAX_PATH] = { 0 };
char szSvaeFileName[MAX_PATH] = { 0 };
char szCommand[1024] = { 0 };
auto funGenerate = [](string strCommand)
{
system(strCommand.c_str());
};
for (int i = 0; i < g_vecTitleNames.size(); i++)
{
// 跳过空
if (g_vecAudioPaths[i].size() == 0 ||
g_vecVideoPaths[i].size() == 0 ||
g_vecTitleNames[i].size() == 0 ||
g_vecPartNames[i].size() == 0)
{
continue;
}
// 创建保存文件夹
sprintf(szSavePath, "%s%s\\", pPath, g_vecTitleNames[i].c_str());
if (_access(szSavePath, 0) == -1) // 如果文件夹不存在
{
_mkdir(szSavePath);
}
// 保存文件名
sprintf(szSvaeFileName, "%s%s.mp4", szSavePath, g_vecPartNames[i].c_str());
// 组装命令
// ffmpeg.exe -i video.m4s -i audio.m4s -codec copy name
sprintf(szCommand, "%sffmpeg.exe -i %s -i %s -codec copy %s",
pPath, g_vecVideoPaths[i].c_str(), g_vecAudioPaths[i].c_str(), szSvaeFileName);
// 使用线程处理,没什么用,直接调用system(strCommand.c_str())更省事
std::thread t(funGenerate, szCommand);
t.join();
cout << i << " --- " << "输出:" << szSvaeFileName << endl;
}
}
4.主函数
获取当前执行文件所在路径,直接调用查找和生成函数;
代码如下(示例)
int main()
{
// 获取当前模块(exe)所在路径
char szModuleFileName[MAX_PATH] = { 0 };
::GetModuleFileName(NULL, szModuleFileName, MAX_PATH);
// 查找目录
char szFindPath[MAX_PATH] = { 0 };
strcpy(szFindPath, szModuleFileName);
char *pPos = strrchr(szFindPath, '\\');
if (pPos == NULL)
{
return -1;
}
// 截断文件名
pPos[1] = '\0';
FindFile(szFindPath);
GenerateVideo(szFindPath);
system("pause");
return 0;
}
总结
写的有点乱,主要怎么方便怎么来,能基本应付我现在的需求