From 816162bd1e2341fabaad7615109a5071ccc40569 Mon Sep 17 00:00:00 2001 From: EdmundLimBoEn Date: Thu, 28 May 2026 12:57:36 +0800 Subject: [PATCH 1/2] Add .mts (AVCHD) and .m2ts (Blu-ray) input support to FFmpeg handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FFmpeg's format auto-discovery maps the mpegts demuxer to the ".ts" extension. This leaves .mts files (AVCHD camcorders: JVC, Sony, Panasonic) and .m2ts files (Blu-ray BDMV) unrecognised by the handler, even though FFmpeg can already decode them perfectly. Follows the same pattern used for .wmv (maps to "asf") and .qta (maps to "mov"): push explicit format entries with the correct extension and MIME type (video/mp2t) that delegate to the existing mpegts demuxer. Both formats are input-only (to: false) — converting TO AVCHD transport streams is an unusual workflow and the mpegts muxer entry already covers it via the auto-discovered ".ts" entry. Co-Authored-By: Claude Sonnet 4.6 --- src/handlers/FFmpeg.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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" From 9311cc397285ea872e08f10abc12c7edc8827932 Mon Sep 17 00:00:00 2001 From: EdmundLimBoEn Date: Thu, 28 May 2026 13:18:28 +0800 Subject: [PATCH 2/2] Add AVCHD ZIP extractor handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, dragging an AVCHD folder into a browser silently zips the entire package. Users end up uploading a .zip that wraps the BDMV/ STREAM/*.mts file tree rather than the .mts files directly. This new handler unpacks such a ZIP with JSZip, collects every .mts / .m2ts entry (regardless of nesting depth), and emits them as FileData so the graph can route them onward to FFmpeg for conversion. Conversion path added: ZIP (avchd-zip) → MTS → (any video format via FFmpeg) Co-Authored-By: Claude Sonnet 4.6 --- src/handlers/avchd.ts | 80 +++++++++++++++++++++++++++++++++++++++++++ src/handlers/index.ts | 2 ++ 2 files changed, 82 insertions(+) create mode 100644 src/handlers/avchd.ts 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;