Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2025 MuseScore Limited
9 : *
10 : * This program is free software; you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "ffmpegutils.h"
24 :
25 : #include <algorithm>
26 :
27 : #include "io/fileinfo.h"
28 : #include "io/path.h"
29 :
30 : #if defined(Q_OS_WIN)
31 : #include <QSettings>
32 : #endif
33 :
34 : using namespace muse::media;
35 : using namespace muse;
36 :
37 : namespace muse::media {
38 0 : io::paths_t defaultSearchPaths()
39 : {
40 0 : io::paths_t paths;
41 : #if defined(Q_OS_WIN)
42 : QSettings settings(
43 : "HKEY_LOCAL_MACHINE\\Software\\FFmpeg\\Providers",
44 : QSettings::NativeFormat
45 : );
46 :
47 : for (const QString& child : settings.childGroups()) {
48 : settings.beginGroup(child);
49 : QVariant value = settings.value("InstallPath");
50 : if (value.isValid()) {
51 : QString path = value.toString();
52 : if (!path.isEmpty()) {
53 : paths.push_back(path.replace('\\', '/') + "/bin");
54 : }
55 : }
56 : settings.endGroup(); // Back to parent group.
57 : }
58 : #elif defined(Q_OS_MAC)
59 : RetVal<io::paths_t> providers = io::Dir::scanFiles("/Library/Application Support/FFmpeg/Providers", { "*" },
60 : io::ScanMode::FoldersInCurrentDir);
61 : if (providers.ret) {
62 : for (const io::path_t& dir : providers.val) {
63 : paths.push_back(dir + "/lib");
64 : }
65 : }
66 : paths.push_back("/opt/homebrew/lib");
67 : paths.push_back("/usr/local/lib");
68 : #else
69 0 : paths.push_back("/usr/lib/x86_64-linux-gnu");
70 0 : paths.push_back("/usr/lib64");
71 0 : paths.push_back("/usr/lib");
72 : #endif
73 0 : return paths;
74 0 : }
75 :
76 72 : static FFmpegLibPaths libraryPathsForVersion(int ffmpegVer, const io::path_t& searchDir, FFmpegCandidateOrigin origin)
77 : {
78 72 : FFmpegVersionInfo ffmpegVersionInfo;
79 252 : for (const auto& [ffmpegVersion, componentsVersions] : FFMPEG_COMPONENTS_VERSIONS) {
80 252 : if (ffmpegVersion == ffmpegVer) {
81 72 : ffmpegVersionInfo = componentsVersions;
82 72 : break;
83 : }
84 : }
85 :
86 72 : if (!ffmpegVersionInfo.isValid()) {
87 0 : return {};
88 : }
89 :
90 72 : io::path_t avutilName, avcodecName, avformatName, swscaleName, swresampleName;
91 : #if defined(Q_OS_MAC)
92 : avutilName = io::path_t("libavutil." + std::to_string(ffmpegVersionInfo.avUtilVersion) + ".dylib");
93 : avcodecName = io::path_t("libavcodec." + std::to_string(ffmpegVersionInfo.avCodecVersion) + ".dylib");
94 : avformatName = io::path_t("libavformat." + std::to_string(ffmpegVersionInfo.avFormatVersion) + ".dylib");
95 : swscaleName = io::path_t("libswscale." + std::to_string(ffmpegVersionInfo.swScaleVersion) + ".dylib");
96 : swresampleName = io::path_t("libswresample." + std::to_string(ffmpegVersionInfo.swResampleVersion) + ".dylib");
97 : #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
98 72 : avutilName = io::path_t("libavutil.so." + std::to_string(ffmpegVersionInfo.avUtilVersion));
99 72 : avcodecName = io::path_t("libavcodec.so." + std::to_string(ffmpegVersionInfo.avCodecVersion));
100 72 : avformatName = io::path_t("libavformat.so." + std::to_string(ffmpegVersionInfo.avFormatVersion));
101 72 : swscaleName = io::path_t("libswscale.so." + std::to_string(ffmpegVersionInfo.swScaleVersion));
102 72 : swresampleName = io::path_t("libswresample.so." + std::to_string(ffmpegVersionInfo.swResampleVersion));
103 : #elif defined(Q_OS_WIN)
104 : avutilName = io::path_t("avutil-" + std::to_string(ffmpegVersionInfo.avUtilVersion) + ".dll");
105 : avcodecName = io::path_t("avcodec-" + std::to_string(ffmpegVersionInfo.avCodecVersion) + ".dll");
106 : avformatName = io::path_t("avformat-" + std::to_string(ffmpegVersionInfo.avFormatVersion) + ".dll");
107 : swscaleName = io::path_t("swscale-" + std::to_string(ffmpegVersionInfo.swScaleVersion) + ".dll");
108 : swresampleName = io::path_t("swresample-" + std::to_string(ffmpegVersionInfo.swResampleVersion) + ".dll");
109 : #endif
110 :
111 72 : FFmpegLibPaths result;
112 72 : io::path_t avutilPath = searchDir.appendingComponent(avutilName);
113 72 : io::path_t avcodecPath = searchDir.appendingComponent(avcodecName);
114 72 : io::path_t avformatPath = searchDir.appendingComponent(avformatName);
115 72 : io::path_t swscalePath = searchDir.appendingComponent(swscaleName);
116 72 : io::path_t swresamplePath = searchDir.appendingComponent(swresampleName);
117 84 : if (io::FileInfo::exists(avutilPath) && io::FileInfo::exists(avcodecPath)
118 12 : && io::FileInfo::exists(avformatPath) && io::FileInfo::exists(swscalePath)
119 84 : && io::FileInfo::exists(swresamplePath)) {
120 11 : result.ffmpegVersion = ffmpegVer;
121 11 : result.origin = origin;
122 11 : result.searchDir = searchDir;
123 11 : result.avUtilPath = avutilPath;
124 11 : result.avCodecPath = avcodecPath;
125 11 : result.avFormatPath = avformatPath;
126 11 : result.swScalePath = swscalePath;
127 11 : result.swResamplePath = swresamplePath;
128 11 : return result;
129 : }
130 :
131 61 : return result;
132 72 : }
133 :
134 1 : FFmpegVersion versionFromAVFormatPath(const io::path_t& path)
135 : {
136 1 : std::string name = io::filename(path, true).toStdString();
137 1 : int avVer = -1;
138 : #if defined(Q_OS_MAC)
139 : // libavformat.60.dylib
140 : size_t dot = name.rfind('.');
141 : if (dot != std::string::npos && dot > 0) {
142 : size_t verStart = name.rfind('.', dot - 1);
143 : if (verStart != std::string::npos && verStart + 1 < dot) {
144 : try {
145 : avVer = std::stoi(name.substr(verStart + 1, dot - verStart - 1));
146 : } catch (...) {}
147 : }
148 : }
149 : #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
150 : // libavformat.so.60
151 1 : size_t so = name.rfind(".so.");
152 1 : if (so != std::string::npos && so + 4 < name.size()) {
153 : try {
154 1 : avVer = std::stoi(name.substr(so + 4));
155 0 : } catch (...) {}
156 : }
157 : #elif defined(Q_OS_WIN)
158 : // avformat-60.dll
159 : size_t dash = name.find('-');
160 : if (dash != std::string::npos && dash + 1 < name.size()) {
161 : size_t dot = name.find('.', dash);
162 : if (dot != std::string::npos) {
163 : try {
164 : avVer = std::stoi(name.substr(dash + 1, dot - dash - 1));
165 : } catch (...) {}
166 : }
167 : }
168 : #endif
169 :
170 1 : for (const auto& [ver, componentsVersions] : FFMPEG_COMPONENTS_VERSIONS) {
171 1 : if (componentsVersions.avFormatVersion == avVer) {
172 1 : return ver;
173 : }
174 : }
175 :
176 0 : return FFMPEG_INVALID_VERSION;
177 1 : }
178 :
179 15 : static io::path_t normalizedSearchPath(const io::path_t& path)
180 : {
181 15 : const io::FileInfo fileInfo(path);
182 15 : const String canonicalPath = fileInfo.canonicalFilePath();
183 30 : return canonicalPath.empty() ? io::path_t(fileInfo.absoluteFilePath()) : io::path_t(canonicalPath);
184 15 : }
185 :
186 40 : static String searchPathKey(const io::path_t& path)
187 : {
188 : #if defined(Q_OS_WIN)
189 : return path.toString().toLower();
190 : #else
191 40 : return path.toString();
192 : #endif
193 : }
194 :
195 13 : static bool containsSearchPath(const io::paths_t& paths, const io::path_t& path)
196 : {
197 13 : const String pathKey = searchPathKey(path);
198 13 : return std::find_if(paths.cbegin(), paths.cend(), [&pathKey](const io::path_t& candidate) {
199 7 : return searchPathKey(candidate) == pathKey;
200 39 : }) != paths.cend();
201 13 : }
202 :
203 6 : static io::paths_t normalizedSearchPaths(const io::paths_t& paths)
204 : {
205 6 : io::paths_t result;
206 6 : result.reserve(paths.size());
207 :
208 16 : for (const io::path_t& path : paths) {
209 10 : const io::path_t normalizedPath = normalizedSearchPath(path);
210 10 : if (normalizedPath.empty() || containsSearchPath(result, normalizedPath)) {
211 0 : continue;
212 : }
213 :
214 10 : result.push_back(normalizedPath);
215 10 : }
216 :
217 6 : std::sort(result.begin(), result.end(), [](const io::path_t& first, const io::path_t& second) {
218 6 : return searchPathKey(first) < searchPathKey(second);
219 : });
220 6 : return result;
221 0 : }
222 :
223 12 : static void appendLibraryPathsForAllVersions(FFmpegLibPathsList& result, const io::path_t& searchPath,
224 : FFmpegCandidateOrigin origin)
225 : {
226 84 : for (const auto& [ffmpegVersion, _] : FFMPEG_COMPONENTS_VERSIONS) {
227 72 : FFmpegLibPaths paths = libraryPathsForVersion(ffmpegVersion, searchPath, origin);
228 72 : if (!paths.avFormatPath.empty()) {
229 11 : result.push_back(std::move(paths));
230 : }
231 72 : }
232 12 : }
233 :
234 0 : FFmpegLibPathsList findLibraryPaths(const io::path_t& configPath)
235 : {
236 0 : return findLibraryPaths(configPath, defaultSearchPaths());
237 : }
238 :
239 6 : FFmpegLibPathsList findLibraryPaths(const io::path_t& configPath, const io::paths_t& defaultPaths)
240 : {
241 6 : FFmpegLibPathsList result;
242 6 : const io::paths_t automaticSearchPaths = normalizedSearchPaths(defaultPaths);
243 :
244 6 : if (!configPath.empty() && io::FileInfo::exists(configPath)) {
245 : const io::path_t configuredSearchPath = normalizedSearchPath(
246 3 : io::FileInfo(configPath).entryType() == io::EntryType::Dir ? configPath : io::dirpath(configPath));
247 :
248 3 : if (!containsSearchPath(automaticSearchPaths, configuredSearchPath)) {
249 2 : appendLibraryPathsForAllVersions(result, configuredSearchPath, FFmpegCandidateOrigin::Configured);
250 : }
251 3 : }
252 :
253 6 : FFmpegLibPathsList automaticCandidates;
254 16 : for (const io::path_t& path : automaticSearchPaths) {
255 10 : appendLibraryPathsForAllVersions(automaticCandidates, path, FFmpegCandidateOrigin::Automatic);
256 : }
257 :
258 6 : std::sort(automaticCandidates.begin(), automaticCandidates.end(), [](const FFmpegLibPaths& first, const FFmpegLibPaths& second) {
259 4 : if (first.ffmpegVersion != second.ffmpegVersion) {
260 2 : return first.ffmpegVersion > second.ffmpegVersion;
261 : }
262 2 : return searchPathKey(first.searchDir) < searchPathKey(second.searchDir);
263 : });
264 :
265 6 : result.insert(result.end(), automaticCandidates.cbegin(), automaticCandidates.cend());
266 :
267 6 : return result;
268 6 : }
269 :
270 3 : std::optional<io::path_t> configuredPathToPersist(const io::path_t& requestedPath, const FFmpegLibPaths& loadedCandidate)
271 : {
272 3 : if (requestedPath.empty()) {
273 1 : return io::path_t();
274 : }
275 :
276 2 : const io::FileInfo requestedFileInfo(requestedPath);
277 2 : if (!requestedFileInfo.exists()) {
278 0 : return std::nullopt;
279 : }
280 :
281 : const io::path_t requestedSearchPath = normalizedSearchPath(
282 2 : requestedFileInfo.entryType() == io::EntryType::Dir ? requestedPath : io::dirpath(requestedPath));
283 2 : if (searchPathKey(requestedSearchPath) != searchPathKey(loadedCandidate.searchDir)) {
284 1 : return std::nullopt;
285 : }
286 :
287 1 : return requestedSearchPath;
288 2 : }
289 : }
|