Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/handlers/FFmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,32 @@ class FFmpegHandler implements FormatHandler {
category: "video"
});

// Add .mts (AVCHD) support - camcorder footage using the MPEG-TS container.
// FFmpeg auto-discovers "mpegts" but assigns the ".ts" extension, leaving
// ".mts" files (JVC, Sony, Panasonic AVCHD camcorders) unrecognised.
this.supportedFormats.push({
name: "AVCHD Video",
format: "mts",
extension: "mts",
mime: "video/mp2t",
from: true,
to: false,
internal: "mpegts",
category: "video"
});

// Add .m2ts (Blu-ray BDMV) support - same MPEG-TS container, different extension.
this.supportedFormats.push({
name: "Blu-ray BDMV Video",
format: "m2ts",
extension: "m2ts",
mime: "video/mp2t",
from: true,
to: false,
internal: "mpegts",
category: "video"
});

// Normalize Bink metadata to ensure ".bik" files are detected by extension.
const binkFormats = this.supportedFormats.filter(f =>
f.internal === "bink"
Expand Down
80 changes: 80 additions & 0 deletions src/handlers/avchd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";
import JSZip from "jszip";

/**
* AVCHD ZIP handler.
*
* macOS silently zips an AVCHD folder when you drag it into a browser —
* the user ends up uploading a .zip instead of the individual .mts files
* inside BDMV/STREAM/. This handler unpacks such a ZIP and returns every
* .mts / .m2ts file it finds, so the graph can route them through FFmpeg.
*
* Conversion path: ZIP (avchd) → MTS (then FFmpeg takes it to MP4/etc.)
*/
class avchdHandler implements FormatHandler {

public name: string = "AVCHD Extractor";
public supportedFormats: FileFormat[] = [];
public ready: boolean = false;

async init () {
this.supportedFormats = [
// Input: a ZIP that wraps an AVCHD package
{
name: "AVCHD Package (ZIP)",
format: "avchd-zip",
extension: "zip",
mime: "application/zip",
from: true,
to: false,
internal: "avchd-zip",
category: "video"
},
// Output: raw MTS files extracted from the ZIP
{
name: "AVCHD Video",
format: "mts",
extension: "mts",
mime: "video/mp2t",
from: false,
to: true,
internal: "mts",
category: "video"
}
];
this.ready = true;
}

async doConvert (
inputFiles: FileData[],
_inputFormat: FileFormat,
_outputFormat: FileFormat
): Promise<FileData[]> {

const results: FileData[] = [];

for (const file of inputFiles) {
const zip = await JSZip.loadAsync(file.bytes);
const mtsEntries = Object.values(zip.files).filter(entry =>
!entry.dir &&
/\.(mts|m2ts)$/i.test(entry.name)
);

if (mtsEntries.length === 0) {
throw `No .mts or .m2ts files found inside ${file.name}`;
}

for (const entry of mtsEntries) {
const bytes = await entry.async("uint8array");
// Use just the filename, not the full path inside the ZIP
const name = entry.name.split("/").pop()!;
results.push({ name, bytes });
}
}

return results;
}

}

export default avchdHandler;
2 changes: 2 additions & 0 deletions src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import xcursorHandler from "./xcursor.ts";
import shToElfHandler from "./shToElf.ts";
import cssHandler from "./css.ts";
import TypstHandler from "./typst.ts";
import avchdHandler from "./avchd.ts";

const handlers: FormatHandler[] = [];
try { handlers.push(new svgTraceHandler()) } catch (_) { };
Expand Down Expand Up @@ -150,5 +151,6 @@ try { handlers.push(new xcursorHandler()) } catch (_) { };
try { handlers.push(new shToElfHandler()) } catch (_) { };
try { handlers.push(new cssHandler()) } catch (_) { };
try { handlers.push(new TypstHandler()) } catch (_) { };
try { handlers.push(new avchdHandler()) } catch (_) { };

export default handlers;