diff --git a/src/handlers/FFmpeg.ts b/src/handlers/FFmpeg.ts index 33a62c2a..34621915 100644 --- a/src/handlers/FFmpeg.ts +++ b/src/handlers/FFmpeg.ts @@ -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" diff --git a/src/handlers/avchd.ts b/src/handlers/avchd.ts new file mode 100644 index 00000000..4d3cd88d --- /dev/null +++ b/src/handlers/avchd.ts @@ -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 { + + 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; diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 0c6fae4b..607cd498 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -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 (_) { }; @@ -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;