Technical Notes for Developers
This section is optional, for maintainers and contributors familiar with the Shotcut / MLT codebase.
Shotcut is a Qt-based C++ application using the MLT framework (mltframework/shotcut). The .mlt project is an XML document that references media via file paths. The proposed “Save As and Consolidate” feature can be implemented as a fairly self‑contained workflow around:
- Enumerating active media sources in the current MLT project
- Creating a new folder structure
- Copying media into that structure (optionally with progress UI and error handling)
- Updating MLT resource paths to point to the new locations
- Saving the updated
.mlt file in the root of the new project folder
1. Media Enumeration
Goal: Collect all active media items (video, audio, images, proxies) referenced by the current project.
In MLT, clip URIs usually appear as:
producer.resource properties
- Playlist/track entries referencing those producers
- Possibly filters or transitions referencing external files (e.g., images, LUTs)
Simple approach:
-
Iterate over all producers in the current MLT profile / tractor:
- For each producer, read the
resource property.
- Skip non-file schemes (
http://, https://, ftp://, etc.) or special MLT producers.
- Normalize and resolve to absolute paths.
-
(Optional but safer) Scan for:
- External image masks or overlays used by filters (
lut3d, mask, etc.)
- External subtitle or text templates, if any.
From a C++/Qt perspective, pseudocode might look like:
// PSEUDOCODE – illustrates general flow only
QSet<QString> mediaFiles;
for (auto *producer : project->allProducers()) {
QString resource = QString::fromUtf8(mlt_producer_get_string(producer, "resource"));
if (resource.isEmpty())
continue;
QUrl url(resource);
if (!url.isLocalFile())
continue; // skip non-local or non-file resources
QString path = QFileInfo(url.toLocalFile()).absoluteFilePath();
mediaFiles.insert(path);
}
This set powers the subsequent copy & relink steps.
2. Folder Structure Creation
When the user chooses “Save As and Consolidate…”:
-
Ask for a new project root directory (e.g., via QFileDialog::getExistingDirectory or a custom dialog).
-
Inside that directory, create:
ProjectRoot/
Clips/
Audio/
Images and Animations/
Proxies/
ProjectName.mlt
-
You can map each discovered file to one of those folders based on:
- Extension (e.g.,
.mp4, .mov, .mkv → Clips; .wav, .mp3 → Audio; .png, .jpg → Images)
- Whether it’s known to be a proxy (more below).
Example folder mapping (simplified):
enum class MediaType { Clip, Audio, Image, Proxy };
MediaType classifyFile(const QString &path) {
QString ext = QFileInfo(path).suffix().toLower();
static const QSet<QString> videoExt = { "mp4", "mov", "mkv", "avi" };
static const QSet<QString> audioExt = { "wav", "mp3", "flac", "aac", "ogg" };
static const QSet<QString> imageExt = { "png", "jpg", "jpeg", "gif", "tiff", "bmp", "webp" };
if (/* check if path matches Shotcut proxy pattern */) {
return MediaType::Proxy;
} else if (videoExt.contains(ext)) {
return MediaType::Clip;
} else if (audioExt.contains(ext)) {
return MediaType::Audio;
} else if (imageExt.contains(ext)) {
return MediaType::Image;
} else {
// fallback: treat unknown media as Clip
return MediaType::Clip;
}
}
Destination path:
QString destinationFor(const QString &projectRoot, const QString &originalPath) {
MediaType type = classifyFile(originalPath);
QString subdir;
switch (type) {
case MediaType::Clip: subdir = "Clips"; break;
case MediaType::Audio: subdir = "Audio"; break;
case MediaType::Image: subdir = "Images and Animations"; break;
case MediaType::Proxy: subdir = "Proxies"; break;
}
QDir dir(projectRoot);
dir.mkpath(subdir);
return dir.filePath(subdir + "/" + QFileInfo(originalPath).fileName());
}
3. Copying Assets (Including Proxies)
Core logic:
- For each file in
mediaFiles, compute destPath = destinationFor(projectRoot, originalPath).
- If
destPath already exists and is identical (size + hash or mtime), skip or confirm with the user.
- Copy via
QFile::copy(originalPath, destPath) (optionally robust with temp files then rename).
- Track mapping:
originalPath → destPath.
Pseudocode:
QMap<QString, QString> pathMap; // oldPath -> newPath
for (const QString &src : mediaFiles) {
QString dst = destinationFor(projectRoot, src);
if (src == dst) {
pathMap.insert(src, dst);
continue;
}
if (!QFileInfo::exists(src)) {
// log or show a warning about missing source
continue;
}
QDir().mkpath(QFileInfo(dst).absolutePath());
if (!QFile::copy(src, dst)) {
// log error, maybe allow "Skip / Retry / Abort"
continue;
}
pathMap.insert(src, dst);
}
Proxy handling
Shotcut already has a proxy system (with a proxy directory and naming pattern). For proxies:
- Detect proxy files from the MLT or Shotcut’s internal proxy manager.
- Prefer to copy the existing proxies if available, so the consolidated project opens quickly.
- Store proxies under
Proxies/ in the new project.
Option A (simple): Only handle proxies that appear as resource entries (i.e., whatever the current project is actually referencing).
Option B (more thorough): For each original clip in Clips/, check if a proxy exists in the global proxy directory using Shotcut’s existing naming convention, and copy it to Proxies/, updating any relevant MLT resource or proxy metadata.
Developers can hook into Shotcut’s existing proxy manager instead of reinventing detection logic.
4. Relinking / Path Rewriting in the MLT XML
Once all copies are completed and pathMap is filled, update the project’s resource references.
Two ways to do this:
-
MLT-Level Rebinding (preferred in C++ code path):
For each producer, see if its resource maps to a new path; if so, update the property, then serialize the modified MLT project.
-
XML-Level Rewriting (as a fallback concept):
Load the .mlt XML as text, update resource="..." attributes using pathMap, and save.
MLT-level example (pseudocode):
for (auto *producer : project->allProducers()) {
QString resource = QString::fromUtf8(mlt_producer_get_string(producer, "resource"));
if (resource.isEmpty())
continue;
QUrl url(resource);
if (!url.isLocalFile())
continue;
QString oldPath = QFileInfo(url.toLocalFile()).absoluteFilePath();
if (!pathMap.contains(oldPath))
continue;
QString newPath = pathMap.value(oldPath);
QString newResource = QUrl::fromLocalFile(newPath).toString();
mlt_producer_set(producer, "resource", newResource.toUtf8().constData());
}
After all updates:
// Save modified MLT to new project root:
QString newMltPath = projectRoot + "/" + projectName + ".mlt";
project->saveToFile(newMltPath); // assuming Shotcut has a wrapper method
5. Proxy Path Logic
If the project uses proxies, Shotcut likely stores:
- Original path
- Proxy path
- A boolean flag like
use_proxy or internal state
Consolidation options:
- Keep proxy resolution logic intact and simply ensure that any
resource paths currently pointing at proxies are updated to their new locations inside Proxies/.
- Store proxy paths relative to the project root, so moving the whole folder works on other systems.
Typical behavior flow:
- When consolidating, if a producer is currently using a proxy, its
resource might already be the proxy file.
- Copy that proxy file to
Proxies/.
- Update
resource to Proxies/... (relative to project root).
- Keep a producer property (or Shotcut metadata) that still knows the original high-res source path in case the user regenerates proxies or toggles proxy usage later.
A very simple proxy rewrite could be:
if (isProxyResource(resource)) {
// oldProxyPath -> newProxyPath
QString oldProxyPath = QFileInfo(url.toLocalFile()).absoluteFilePath();
QString newProxyPath = pathMap.value(oldProxyPath, oldProxyPath);
mlt_producer_set(producer, "resource",
QUrl::fromLocalFile(newProxyPath).toString().toUtf8().constData());
}
Where isProxyResource() uses Shotcut’s existing naming pattern or internal flags.
6. Relative vs Absolute Paths
To make the project portable:
- After consolidation, all
resource paths can be made relative to the .mlt file location.
- This can be done after the copy step, just before saving, by replacing the absolute path with
QDir(projectRoot).relativeFilePath(newPath) and then prefixing with file: if desired.
Example:
QString relative = QDir(projectRoot).relativeFilePath(newPath);
QString newResource = QUrl::fromLocalFile(relative).toString(); // or custom scheme
mlt_producer_set(producer, "resource", newResource.toUtf8().constData());
This ensures the entire project folder can be moved to a new drive or sent to another machine without breaking links, as long as the internal structure stays the same.
7. UI Placement and Workflow
Suggested UX: