文章目录

  • 前言
  • 一、先看效果
  • 二、开始写代码
  • 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缓存视频的文件夹拷贝到程序生成目录下,如下图:

python 中用FFMPEG 合并两个视频文件_ffmpeg

然后直接运行程序,会创建文件夹,并将视频生成到指定文件夹下面,效果如下:

python 中用FFMPEG 合并两个视频文件_json_02

python 中用FFMPEG 合并两个视频文件_ffmpeg_03

二、开始写代码

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;
}

总结

写的有点乱,主要怎么方便怎么来,能基本应付我现在的需求