文章目录
- 前言
- 一、源码
- 二、使用步骤
- 总结
前言
之前写过一个只能合成的m4s的版本,参考C++调用ffmpeg批量合并bilibili缓存视频,实际使用存在很多bug,先以及进行优化,具体如下:
1、增加blv格式的视频合成,实际缓存的视频是m4s和blv混合的,上一版的合成不了blv;
2、优化遍历算法,之前的遍历查找有问题,现在完美过滤未缓存的视频;
3、过滤部分遇到的特殊字符,部分特殊字符会导致合成失败;
4、支持官方动漫视频合成;
一、源码
示例源码如下:
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <thread>
#include <windows.h>
#include <string.h>
#include <io.h>
#include <direct.h>
// 这里是我将jsoncpp封装成一个库了,方便使用
#include "../jsontool/jsontool.h"
#pragma comment(lib, "JsonTool.lib")
using namespace std;
#define AudioName "audio.m4s"
#define VideoName "video.m4s"
#define JsonName "entry.json"
#define BLVVideo ".blv"
vector<string> g_vecTitleNames;
vector<string> g_vecPartNames;
vector<string> g_vecVideoPaths;
vector<string> g_vecAudioPaths;
#define ERR_NO_FIND -1
int GetTitleAndPage(const char* pszJsonPath, char* pszTitle, char* pszPage);
int FindJson(const char* pszVideoPath, char* pszTitle, char* pszPage)
{
// 通过json文件路径获取title和part
char szJsonPath[MAX_PATH] = { 0 };
int iLen = strlen(pszVideoPath);
strcpy(szJsonPath, pszVideoPath);
// 阶段最后面的 '\'
szJsonPath[iLen - 1] = '\0';
// 查找并截断路径到上一层目录
char* pFind = strrchr(szJsonPath, '\\');
if (pFind == NULL)
{
return -1;
}
pFind[1] = '\0';
strcat(szJsonPath, JsonName);
int iRet = GetTitleAndPage(szJsonPath, pszTitle, pszPage);
if (iRet != 0)
{
return -1;
}
return 0;
}
int FindFile(const char* pszPath)
{
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) // 文件
{
// 防止video没有缓存完,json文件肯定有,video不一定,所以先找到video,再读上一层的json,vedio没缓存完,就没必要读json
if (strFileName == VideoName)
{
// video找到了,先返回上一层读json,再通过json文件路径获取title和part
char szTitleName[MAX_PATH] = { 0 };
char szPartName[MAX_PATH] = { 0 };
iRet = FindJson(pszPath, szTitleName, szPartName);
if (iRet != 0)
{
break;
}
// 插入title和章节名
g_vecTitleNames.push_back(szTitleName);
g_vecPartNames.push_back(szPartName);
// 插入video路径
g_vecVideoPaths.push_back(string(pszPath) + VideoName);
g_vecAudioPaths.push_back(string(pszPath) + AudioName);
break;
}
else if (strFileName.find(BLVVideo) != string::npos)
{
// blv格式,需要特殊处理,将文件名写到txt中
string strBlvTxtFile = pszPath;
strBlvTxtFile += "blv.txt";
// 可能存在多个*.blv文件,只要插入一次即可
// 将当前所在路径和上一次插入vector的路径进行匹配,判断是否是第一次
string strLastVideoPath = g_vecVideoPaths.back();
if (strLastVideoPath.find(pszPath) == string::npos)
{
// 没找到,则是第一次进入
// 删除测试生成的文件
DeleteFile(strBlvTxtFile.c_str());
// video找到了,先返回上一层读json,再通过json文件路径获取title和part
char szTitleName[MAX_PATH] = { 0 };
char szPartName[MAX_PATH] = { 0 };
iRet = FindJson(pszPath, szTitleName, szPartName);
if (iRet != 0)
{
break;
}
// 插入title和章节名
g_vecTitleNames.push_back(szTitleName);
g_vecPartNames.push_back(szPartName);
// 简单处理,插入相同路径作为blv格式的判断依据
g_vecAudioPaths.push_back(strBlvTxtFile);
g_vecVideoPaths.push_back(strBlvTxtFile);
}
std::ofstream fout;
fout.open(strBlvTxtFile, std::ios::app);
fout << "file '" << pszPath << strFileName << "'" << endl;
fout.close();
}
}
}
// 查找下一个文件
if (!::FindNextFile(hFindFile, &wfd))
{
break;
}
strFileName = wfd.cFileName;
}
::FindClose(hFindFile);
return iRet;
}
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;
}
void removeSpecialChar(string& strValue)
{
int iFindIndex = 0;
// 去掉空格
while ((iFindIndex = strValue.find(" ")) != string::npos)
{
strValue.replace(iFindIndex, 1, "");
}
// 将 & 替换成 与,不然ffmpeg会报错
while ((iFindIndex = strValue.find("&")) != string::npos)
{
strValue.replace(iFindIndex, 1, "与");
}
// 去掉斜杠'/'
while ((iFindIndex = strValue.find("/")) != string::npos)
{
strValue.replace(iFindIndex, 1, "");
}
// 去掉反斜杠'\'
while ((iFindIndex = strValue.find("\\")) != string::npos)
{
strValue.replace(iFindIndex, 1, "");
}
}
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())
{
memset(szValue, 0, sizeof(szValue));
is.getline(szValue, sizeof(szValue));
strValue += szValue;
}
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);
if (iRet != 0)
{
// up主上传的视频,json文件存放的节点是page_data/part,缓存官方动漫节点是ep/index_title
iRet = json.GetStr("ep", "index_title", szBuff, MAX_PATH);
}
if (iRet != 0)
{
is.close();
return iRet;
}
strValue = UnicodeToANSI(UTF8ToUnicode(szBuff));
// 去掉特殊字符
removeSpecialChar(strValue);
//cout << "GetJsonStr: " << iRet << " " << strValue << endl;
strcpy(pszPage, strValue.c_str());
iRet = json.GetStr("", "title", szBuff, MAX_PATH);
if (iRet != 0)
{
is.close();
return iRet;
}
strValue = UnicodeToANSI(UTF8ToUnicode(szBuff));
// 去掉特殊字符
removeSpecialChar(strValue);
//cout << "GetJsonStr: " << iRet << " " << strValue << endl;
strcpy(pszTitle, strValue.c_str());
is.close();
return 0;
}
void GenerateVideo(const char* pPath)
{
if (g_vecAudioPaths.size() == 0 ||
g_vecAudioPaths.size() != g_vecVideoPaths.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());
// 组装命令
// 不相等则为m4s文件
if (g_vecVideoPaths[i] != g_vecAudioPaths[i])
{
// 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);
}
else // 相等则为blv文件
{
// ffmpeg.exe -f concat -i blv.txt -c copy name
sprintf(szCommand, "%sffmpeg.exe -f concat -safe 0 -i %s -c copy %s",
pPath, g_vecVideoPaths[i].c_str(), szSvaeFileName);
}
cout << szCommand << endl;
// 使用线程处理
std::thread t(funGenerate, szCommand);
t.join();
cout << i << " --- " << "输出:" << szSvaeFileName << endl;
}
}
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;
}
二、使用步骤
直接将手机bilibili的缓存目录下download文件夹拷贝到当前文件夹下,双击Bilibili.exe即可自动合并生成
总结
jsontool没法提供,可直接下载jsoncpp进行替换该功能,最终生成的执行文件上传审核不过,不知道为啥?有时间上传百度云再分享吧!