文章目录

  • 前言
  • 一、方案
  • 二、编程批量调整同步
  • 总结



前言

最近下载了10多个G的学习视频,结果一播放,发现声音和画面不同步,原本以为大脑能自动调整画面和和画面,勉强能看下去,结果一分钟不到崩溃了,因为声音比画面快了20秒😓;
原本想删了算了,但是下载了几个礼拜才下好,所以只能想想办法



一、方案

方案一:众所周知,声音的传播速度是 340m/s,找跟长的线,只要把音响放的足够远,理论上是能达到声音和画面同步;果然是学好数理化,走遍天下都不怕啊;实际体验效果我就不尝试了,哈哈🤣

方案二:如果是电脑,同时打开两个视频播放器,一个只听声音,一个静音只看画面;如果是手机,没办法同时打开两个视频播放器,那就只能两个手机了;亲测可用😎

方案三:之前接触过ffmpeg,专门处理音视频流的,查了下果然可以,相关命令如下:

// 音频相对于视频后移20秒(00:00:20),如果前移就是(-00:00:20)
ffmpeg -y -itsoffset 00:00:20 -i 源视频.mp4 -i 源视频.mp4 -map 0:a -map 1:v -vcodec copy -acodec copy -f mp4 -threads 2 -v warning 调整后的视频.mp4

// 用得到的参数:
//		-y 可覆盖,如果文件已存在强制替换;
//		-itsoffset offset 设置以秒为基准的时间偏移,该选项影响所有后面的输入文件。该偏移被加到输入文件的时戳,定义一个正偏移意味着相应的流被延迟了offset秒。 [-]hh:mm:ss[.xxx]的格式也支持
//		-f fmt 强迫采用格式fmt
//		-v 调试信息级别(quiet、panic、fatal、error、warning、info、verbose、debug)
//		选择媒体流,AVI,mkv,mp4等,可以包含不同种类的多个流



二、编程批量调整同步

每个视频都手动敲一行命令太麻烦了,写代码批量生成岂不快哉;废话不多说,直接上代码:

#include <string>
#include <iostream>
#include <vector>
#include <thread>
#include <windows.h>
#include <string.h>
#include <io.h>
#include <direct.h>
#include <shlwapi.h>
#pragma comment(lib,"shlwapi.lib")

using namespace std;

vector<string> g_vecVideoPaths;

#define ERR_NO_FIND		-1

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)	// 目录
			{
				// 如果是目录,则继续递归查找
				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) // 文件
			{
				// 检查文件是否为指定的类型,测试只能判断mp4文件
				//bool bRet = PathIsContentType(strFileName.c_str(), "video/mp4");

				// 根据文件后缀判断文件是否是视频文件,我只用到*.mp4、*.mkv、*.rmvb
				// 获取文件路径后缀
				const char* pExtension = PathFindExtension(strFileName.c_str());
				if(pExtension != nullptr && 
					(stricmp(pExtension, ".mp4") == 0 || stricmp(pExtension, ".mkv") == 0 || stricmp(pExtension, ".rmvb") == 0))
				{
					// 视频文件找到了,插入video路径
					g_vecVideoPaths.push_back(string(pszPath) + strFileName);
				}
			}
		}
		// 查找下一个文件
		if (!::FindNextFile(hFindFile, &wfd))
		{
			break;
		}
		strFileName = wfd.cFileName;
	}
	::FindClose(hFindFile);
	return iRet;
}

void GenerateVideo(const char* pPath)
{
	if (g_vecVideoPaths.size() == 0)
	{
		cout << "查找视频资源失败" << endl;
		return;
	}

	cout << "--------------------开始调整-------------------------" << endl;

	int iAdjustTime = 0;
	cout << "请输入要调整的时间(单位:秒),音频后移为正数,前移为负数:";
	cin >> iAdjustTime;

	char szSavePath[MAX_PATH] = { 0 };
	char szFileName[MAX_PATH] = { 0 };
	char szSaveFileName[MAX_PATH] = { 0 };
	char szCommand[1024] = { 0 };
	char szAdjustTime[32] = {0};
	char* pPrefix = "";
	if (iAdjustTime < 0)
	{
		pPrefix = "-";
		iAdjustTime = -iAdjustTime;
	}
	int iHour = iAdjustTime / 3600;
	int iMinute = (iAdjustTime % 3600) / 60;
	int iSecond = iAdjustTime % 60;
	sprintf(szAdjustTime, "%s%02d:%02d:%02d", pPrefix, iHour, iMinute, iSecond);

	// 执行ffmpeg函数
	auto funAdjust = [](string strCommand)
	{
		system(strCommand.c_str());
	};

	for (int i = 0; i < g_vecVideoPaths.size(); i++)
	{
		// 跳过空
		if (g_vecVideoPaths[i].size() == 0)
		{
			continue;
		}

		memset(szSavePath, 0, sizeof(szSavePath));
		strcpy(szSavePath, g_vecVideoPaths[i].c_str());

		// 获得路径中的文件名
		const char* pFileName = PathFindFileName(szSavePath);
		
		// PathFindFileName返回的是指向传入参数szSavePath字符串中文件名所在的地址值,必须用其他变量保存
		memset(szFileName, 0, sizeof(szFileName));
		strcpy(szFileName, pFileName);

		// 删除路径后面的文件名以及\\,可以得到一个文件的路径
		PathRemoveFileSpec(szSavePath);

		strcat(szSavePath, "\\output");

		// 创建保存文件夹
		if (_access(szSavePath, 0) == -1)		// 如果文件夹不存在
		{
			_mkdir(szSavePath);
		}

		// 保存文件名
		sprintf(szSaveFileName, "%s\\%s", szSavePath, szFileName);

		// 组装命令
		// ffmpeg -y -itsoffset 00:00:20 -i 源视频.mp4 -i 源视频.mp4 -map 0:a -map 1:v -vcodec copy -acodec copy -f mp4 -threads 2 -v warning 调整后的视频.mp4
		sprintf(szCommand, "%sffmpeg.exe -y -itsoffset %s -i %s -i %s -map 0:a -map 1:v -vcodec copy -acodec copy -f mp4 -threads 2 -v warning %s",
			pPath, szAdjustTime, g_vecVideoPaths[i].c_str(), g_vecVideoPaths[i].c_str(), szSaveFileName);

		cout << szCommand << endl;
		// 使用线程处理
		std::thread t(funAdjust, szCommand);
		t.join();

		cout << i << "	---	" << "输出:" << szSaveFileName << 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;
}