diff --git a/README.ar.md b/README.ar.md
index 2e83d7ce..539c3197 100644
--- a/README.ar.md
+++ b/README.ar.md
@@ -58,7 +58,7 @@
تظهر الصيغ حيث أنت، ولا يظهر منها إلا ما يناسب الملفات التي تسحبها.
- 263 صيغة دون برامج أخرى
+ 265 صيغة دون برامج أخرى
الصور وPDF وWord وExcel وPowerPoint وHWP والكتب الإلكترونية والأرشيفات والخطوط وCAD والنماذج ثلاثية الأبعاد والبيانات الجغرافية، كلها تعمل دون Office أو Hancom Office أو LibreOffice.
|
diff --git a/README.es.md b/README.es.md
index 1d74252f..3f4bf504 100644
--- a/README.es.md
+++ b/README.es.md
@@ -56,7 +56,7 @@
Los formatos aparecen donde ya estás, y solo los que tienen sentido para los archivos que arrastras.
|
- 263 formatos, sin otros programas
+ 265 formatos, sin otros programas
Imágenes, PDF, Word, Excel, PowerPoint, HWP, libros electrónicos, archivos comprimidos, tipografías, CAD, modelos 3D y datos geoespaciales funcionan sin Office, Hancom Office ni LibreOffice.
|
diff --git a/README.fr.md b/README.fr.md
index 813b3536..4bc46650 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -56,7 +56,7 @@
Les formats apparaissent là où vous êtes déjà, et seulement ceux qui ont un sens pour les fichiers que vous faites glisser.
|
- 263 formats, sans autre logiciel
+ 265 formats, sans autre logiciel
Images, PDF, Word, Excel, PowerPoint, HWP, livres numériques, archives, polices, CAO, modèles 3D et données géospatiales fonctionnent sans Office, Hancom Office ni LibreOffice.
|
diff --git a/README.hi.md b/README.hi.md
index 4d4de153..23a3cdea 100644
--- a/README.hi.md
+++ b/README.hi.md
@@ -56,7 +56,7 @@
फ़ॉर्मैट वहीं दिखते हैं जहाँ आप पहले से हैं, और सिर्फ़ वही जो आपकी खींची जा रही फ़ाइलों के लिए सही हों।
|
- 263 फ़ॉर्मैट, कोई और सॉफ़्टवेयर नहीं
+ 265 फ़ॉर्मैट, कोई और सॉफ़्टवेयर नहीं
इमेज, PDF, Word, Excel, PowerPoint, HWP, ई-बुक, आर्काइव, फ़ॉन्ट, CAD, 3D मॉडल और भू-स्थानिक डेटा Office, Hancom Office या LibreOffice के बिना काम करते हैं।
|
diff --git a/README.ja.md b/README.ja.md
index d060f6d6..5fe2ae96 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -56,7 +56,7 @@
いま作業している場所に、ドラッグ中のファイルに合う形式だけが表示されます。
|
- 263形式、ほかのソフトは不要
+ 265形式、ほかのソフトは不要
画像、PDF、Word、Excel、PowerPoint、HWP、電子書籍、アーカイブ、フォント、CAD、3Dモデル、地理空間データを、Office、Hancom Office、LibreOfficeなしで変換できます。
|
diff --git a/README.ko.md b/README.ko.md
index 2c0d5094..be26be8e 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -57,7 +57,7 @@
끌고 있는 파일에 맞는 형식만 지금 있는 자리에 나타나요.
|
- 263가지 형식, 다른 프로그램 없이
+ 265가지 형식, 다른 프로그램 없이
이미지, PDF, 워드, 엑셀, 파워포인트, HWP, 전자책, 압축 파일, 글꼴, CAD, 3D 모델, 지리 정보 데이터를 오피스·한컴오피스·LibreOffice 없이 변환해요.
|
diff --git a/README.md b/README.md
index fe3b8222..c7c19a61 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@
The formats appear where you already are, only the ones that make sense for the files you drag.
|
- 263 formats, no other software
+ 265 formats, no other software
Images, PDF, Word, Excel, PowerPoint, HWP, e-books, archives, fonts, CAD, 3D models and geospatial data work without Office, Hancom Office or LibreOffice.
|
diff --git a/README.pl.md b/README.pl.md
index 228af92e..5e01b47e 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -56,7 +56,7 @@
Formaty pojawiają się tam, gdzie już jesteś, i tylko te, które pasują do przeciąganych plików.
|
- 263 formatów bez dodatkowych programów
+ 265 formatów bez dodatkowych programów
Obrazy, PDF, Word, Excel, PowerPoint, HWP, e-booki, archiwa, czcionki, CAD, modele 3D i dane geoprzestrzenne działają bez pakietu Office, Hancom Office i LibreOffice.
|
diff --git a/README.pt-BR.md b/README.pt-BR.md
index 148464d7..8433fe6d 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -56,7 +56,7 @@
Os formatos aparecem onde você já está, só os que fazem sentido para os arquivos que você arrasta.
|
- 263 formatos, sem outro programa
+ 265 formatos, sem outro programa
Imagens, PDF, Word, Excel, PowerPoint, HWP, e-books, arquivos compactados, fontes, CAD, modelos 3D e dados geoespaciais funcionam sem Office, Hancom Office ou LibreOffice.
|
diff --git a/README.pt-PT.md b/README.pt-PT.md
index 26abe04c..baa4de59 100644
--- a/README.pt-PT.md
+++ b/README.pt-PT.md
@@ -56,7 +56,7 @@
Os formatos aparecem onde estás, apenas os que fazem sentido para os ficheiros que arrastas.
|
- 263 formatos, sem outro software
+ 265 formatos, sem outro software
Imagens, PDF, Word, Excel, PowerPoint, HWP, livros digitais, arquivos, tipos de letra, CAD, modelos 3D e dados geoespaciais funcionam sem Office, Hancom Office ou LibreOffice.
|
diff --git a/README.ru.md b/README.ru.md
index d71e55d2..c1446d21 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -56,7 +56,7 @@
Форматы появляются там, где вы уже работаете, и только те, что подходят перетаскиваемым файлам.
|
- 263 форматов без лишних программ
+ 265 форматов без лишних программ
Изображения, PDF, Word, Excel, PowerPoint, HWP, электронные книги, архивы, шрифты, чертежи CAD, 3D-модели и геоданные работают без Office, Hancom Office и LibreOffice.
|
diff --git a/README.th.md b/README.th.md
index 9ea1d204..296a1ef2 100644
--- a/README.th.md
+++ b/README.th.md
@@ -56,7 +56,7 @@
ฟอร์แมตที่เหมาะสมจะแสดงขึ้นตรงจุดที่คุณทำงานอยู่ โดยแสดงเฉพาะฟอร์แมตที่ใช้ได้กับไฟล์ที่คุณลากเท่านั้น
|
- 263 ฟอร์แมต โดยไม่ต้องลงโปรแกรมอื่นเพิ่ม
+ 265 ฟอร์แมต โดยไม่ต้องลงโปรแกรมอื่นเพิ่ม
รูปภาพ, PDF, Word, Excel, PowerPoint, HWP, อีบุ๊ก, ไฟล์บีบอัด, ฟอนต์, CAD, โมเดล 3D และข้อมูลเชิงพื้นที่ ทำงานได้โดยไม่ต้องมี Microsoft Office, Hancom Office หรือ LibreOffice
|
diff --git a/README.zh-CN.md b/README.zh-CN.md
index c5615960..59ef8d07 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -56,7 +56,7 @@
只显示适合当前拖动文件的格式,就在你所在的位置。
|
- 263 种格式,无需其他软件
+ 265 种格式,无需其他软件
图片、PDF、Word、Excel、PowerPoint、HWP、电子书、压缩包、字体、CAD、3D 模型和地理信息数据,无需 Office、韩文办公软件或 LibreOffice。
|
diff --git a/README.zh-TW.md b/README.zh-TW.md
index 5ebe922b..df83ecad 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -56,7 +56,7 @@
只顯示適合當前拖曳檔案的格式,就在你所在的位置。
|
- 263 種格式,無需其他軟體
+ 265 種格式,無需其他軟體
圖片、PDF、Word、Excel、PowerPoint、HWP、電子書、壓縮檔、字型、CAD、3D 模型和地理資訊資料,無需 Office、韓文文書軟體或 LibreOffice。
|
diff --git a/docs/ENGINES.md b/docs/ENGINES.md
index cd7db0ac..3b233a9f 100644
--- a/docs/ENGINES.md
+++ b/docs/ENGINES.md
@@ -1440,6 +1440,11 @@ or slower; its frames are all kept.
CRI USM (`.usm`, read only, #485), the video of many games' cutscenes, is read by FFmpeg's `usm` demuxer and goes to
every video and audio target like the other read-only containers. Encrypted USM files (common in recent games) can't
be read and fail with FFmpeg's error.
+A self-contained `.m4s` (read only, #354), a fragmented MP4 that starts with `ftyp` + `moov` and then holds
+`styp` / `sidx` / `moof` / `mdat` fragments (GPAC and some downloaders save them), is read by FFmpeg's mp4 demuxer
+and goes to every video and audio target. A DASH media segment, whose top-level boxes reach a `moof` before any
+`moov`, plays only with a separate init segment; Filee refuses it before FFmpeg runs, with its own message
+(`error.m4s_needs_init`), instead of FFmpeg's demuxer error.
A Video CD's `MPEGAV/AVSEQ01.DAT` (MPEG-1 in a RIFF/CDXA wrapper) counts as MPEG: `.dat` is an extension of that
format, and FFmpeg reads what the file holds, so a `.dat` with no video in it fails instead of making a file.
diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json
index 2381df77..1aded83b 100644
--- a/src/Filee.App/Assets/i18n/ar.json
+++ b/src/Filee.App/Assets/i18n/ar.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "ملف FIT هذا ليس نشاطًا (يتم تحويل الأنشطة فقط)",
"error.fit_no_positions": "لا يحتوي النشاط على مواقع مسجلة: حوّله إلى CSV أو XLSX أو JSON",
"error.fit_no_records": "لا يحتوي النشاط على بيانات مسجلة",
+ "error.m4s_needs_init": "ملف .m4s هذا جزء من فيديو متدفّق، ولا يمكن قراءته دون مقطع البداية (init) المنفصل الخاص به",
"notice.pdf_not_smaller": "تعذّر تقليل الحجم، لذا نُسخ الملف كما هو",
"notice.cad_newer_left_out": "حُذفت كائنات لا يدعمها إصدار AutoCAD المختار (أسهم توضيحية متعددة، شبكات …)",
"notice.original_kept": "تم الاحتفاظ بالأصل وحُفظت النتيجة كنسخة",
diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json
index 6e770d91..658c92d2 100644
--- a/src/Filee.App/Assets/i18n/de.json
+++ b/src/Filee.App/Assets/i18n/de.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "Diese FIT-Datei ist keine Aktivität (nur Aktivitäten werden konvertiert)",
"error.fit_no_positions": "Die Aktivität enthält keine aufgezeichneten Positionen. Konvertiere sie in CSV, XLSX oder JSON",
"error.fit_no_records": "Die Aktivität enthält keine aufgezeichneten Daten",
+ "error.m4s_needs_init": "Diese .m4s-Datei ist ein Stück eines gestreamten Videos und lässt sich ohne ihr separates Startsegment (init) nicht lesen",
"notice.pdf_not_smaller": "Die Datei ließ sich nicht verkleinern und wurde unverändert kopiert",
"notice.cad_newer_left_out": "Objekte, die die gewählte AutoCAD-Version nicht unterstützt (Multiführungslinien, Netze …), wurden weggelassen",
"notice.original_kept": "Das Original wurde beibehalten und das Ergebnis als Kopie gespeichert",
diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json
index 4e2adbbf..9a2a65bc 100644
--- a/src/Filee.App/Assets/i18n/en.json
+++ b/src/Filee.App/Assets/i18n/en.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "This FIT file isn't an activity (only activities are converted)",
"error.fit_no_positions": "The activity has no recorded positions: convert it to CSV, XLSX or JSON",
"error.fit_no_records": "The activity has no recorded data",
+ "error.m4s_needs_init": "This .m4s is one piece of a streamed video: it can't be read without its separate start (init) segment",
"notice.pdf_not_smaller": "Couldn't make it smaller, so it was copied as it is",
"notice.cad_newer_left_out": "Objects the chosen AutoCAD version doesn't have (multileaders, meshes …) were left out",
"notice.original_kept": "The original was kept, the result was saved as a copy",
diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json
index 3ca5f061..0ba17203 100644
--- a/src/Filee.App/Assets/i18n/es.json
+++ b/src/Filee.App/Assets/i18n/es.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "Este archivo FIT no es una actividad (solo se convierten actividades)",
"error.fit_no_positions": "La actividad no tiene posiciones registradas: conviértela a CSV, XLSX o JSON",
"error.fit_no_records": "La actividad no tiene datos registrados",
+ "error.m4s_needs_init": "Este .m4s es un fragmento de un vídeo en streaming y no se puede leer sin su segmento de inicio (init) aparte",
"notice.pdf_not_smaller": "No se pudo reducir su tamaño, por lo que se copió tal cual",
"notice.cad_newer_left_out": "Se omitieron objetos que la versión de AutoCAD elegida no tiene (directrices múltiples, mallas…)",
"notice.original_kept": "Se conservó el original, el resultado se guardó como copia",
diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json
index a4add3cb..5d2025c2 100644
--- a/src/Filee.App/Assets/i18n/fr.json
+++ b/src/Filee.App/Assets/i18n/fr.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "Ce fichier FIT n'est pas une activité (seules les activités sont converties)",
"error.fit_no_positions": "L'activité ne contient aucune position enregistrée : convertissez-la en CSV, XLSX ou JSON",
"error.fit_no_records": "L'activité ne contient aucune donnée enregistrée",
+ "error.m4s_needs_init": "Ce .m4s est un morceau d'une vidéo diffusée en streaming : il ne peut pas être lu sans son segment de départ (init) séparé",
"notice.pdf_not_smaller": "Impossible de réduire sa taille : le fichier a été copié tel quel",
"notice.cad_newer_left_out": "Des objets que la version d'AutoCAD choisie n'a pas (multilignes de repère, maillages…) ont été omis",
"notice.original_kept": "L'original a été conservé, le résultat a été enregistré en copie",
diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json
index 41ac49b1..47a311ec 100644
--- a/src/Filee.App/Assets/i18n/hi.json
+++ b/src/Filee.App/Assets/i18n/hi.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "यह FIT फ़ाइल गतिविधि नहीं है (सिर्फ़ गतिविधियाँ कन्वर्ट होती हैं)",
"error.fit_no_positions": "गतिविधि में दर्ज स्थान नहीं हैं: इसे CSV, XLSX या JSON में बदलें",
"error.fit_no_records": "गतिविधि में दर्ज डेटा नहीं है",
+ "error.m4s_needs_init": "यह .m4s स्ट्रीम किए गए वीडियो का एक हिस्सा है: इसके अलग शुरुआती (init) सेगमेंट के बिना इसे पढ़ा नहीं जा सकता",
"notice.pdf_not_smaller": "इसे छोटा नहीं किया जा सका, इसलिए जैसा है वैसा ही कॉपी कर दिया गया",
"notice.cad_newer_left_out": "चुने गए AutoCAD संस्करण में न होने वाले ऑब्जेक्ट (मल्टीलीडर, मेश …) छोड़ दिए गए",
"notice.original_kept": "मूल फ़ाइल रखी गई, परिणाम कॉपी के रूप में सहेजा गया",
diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json
index fdac2598..ee6e979f 100644
--- a/src/Filee.App/Assets/i18n/ja.json
+++ b/src/Filee.App/Assets/i18n/ja.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "このFITファイルはアクティビティではありません(アクティビティのみ変換できます)",
"error.fit_no_positions": "アクティビティに位置情報が記録されていません。CSV、XLSX、JSONに変換してください",
"error.fit_no_records": "アクティビティに記録データがありません",
+ "error.m4s_needs_init": "この .m4s はストリーミング動画の一部で、別にある開始(init)セグメントがないと読み込めません",
"notice.pdf_not_smaller": "サイズを小さくできなかったため、元のままコピーしました",
"notice.cad_newer_left_out": "選んだ AutoCAD のバージョンにないオブジェクト(マルチ引出線、メッシュ …)を省きました",
"notice.original_kept": "元のファイルを残し、結果をコピーとして保存しました",
diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json
index 8e89ad9b..2c4b016b 100644
--- a/src/Filee.App/Assets/i18n/ko.json
+++ b/src/Filee.App/Assets/i18n/ko.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "활동 기록이 아닌 FIT 파일이에요 (활동만 변환해요)",
"error.fit_no_positions": "위치 기록이 없는 활동이에요. CSV, XLSX, JSON으로 변환하세요",
"error.fit_no_records": "활동에 기록된 데이터가 없어요",
+ "error.m4s_needs_init": "스트리밍 동영상의 한 조각인 .m4s 파일이에요. 따로 있는 시작(init) 세그먼트 없이는 읽을 수 없어요",
"notice.pdf_not_smaller": "더 작게 만들 수 없어서 그대로 복사했어요",
"notice.cad_newer_left_out": "선택한 AutoCAD 버전에 없는 개체(다중 지시선, 메시 …)는 빼고 저장했어요",
"notice.original_kept": "원본은 그대로 두고 결과를 사본으로 저장했어요",
diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json
index 73395a70..3c5a84ad 100644
--- a/src/Filee.App/Assets/i18n/ne.json
+++ b/src/Filee.App/Assets/i18n/ne.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "यो FIT फाइल गतिविधि होइन (गतिविधिहरू मात्र रूपान्तरण हुन्छन्)",
"error.fit_no_positions": "गतिविधिमा रेकर्ड गरिएका स्थान छैनन्: यसलाई CSV, XLSX वा JSON मा रूपान्तरण गर्नुहोस्",
"error.fit_no_records": "गतिविधिमा रेकर्ड गरिएको डाटा छैन",
+ "error.m4s_needs_init": "यो .m4s स्ट्रिम गरिएको भिडियोको एउटा टुक्रा हो: छुट्टै सुरुवाती (init) खण्डबिना यसलाई पढ्न सकिँदैन",
"notice.pdf_not_smaller": "यसलाई सानो बनाउन सकिएन, त्यसैले जस्तो छ त्यस्तै प्रतिलिपि गरियो",
"notice.cad_newer_left_out": "छानिएको AutoCAD संस्करणमा नभएका वस्तुहरू (मल्टिलिडर, मेस …) छोडिए",
"notice.original_kept": "मूल फाइल राखियो, नतिजा प्रतिलिपिको रूपमा सुरक्षित भयो",
diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json
index 4eb84c4d..b52d7fa0 100644
--- a/src/Filee.App/Assets/i18n/pl.json
+++ b/src/Filee.App/Assets/i18n/pl.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "Ten plik FIT nie jest aktywnością (konwertowane są tylko aktywności)",
"error.fit_no_positions": "Aktywność nie zawiera zapisanych położeń: przekonwertuj ją do CSV, XLSX lub JSON",
"error.fit_no_records": "Aktywność nie zawiera zapisanych danych",
+ "error.m4s_needs_init": "Ten plik .m4s to fragment wideo strumieniowego i nie da się go odczytać bez osobnego segmentu początkowego (init)",
"notice.pdf_not_smaller": "Nie udało się zmniejszyć pliku, więc skopiowano go bez zmian",
"notice.cad_newer_left_out": "Pominięto obiekty, których wybrana wersja AutoCAD nie ma (multiodnośniki, siatki…)",
"notice.original_kept": "Oryginał został zachowany, wynik zapisano jako kopię",
diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json
index 8e99ed33..98c714f0 100644
--- a/src/Filee.App/Assets/i18n/pt-BR.json
+++ b/src/Filee.App/Assets/i18n/pt-BR.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "Este arquivo FIT não é uma atividade (só atividades são convertidas)",
"error.fit_no_positions": "A atividade não tem posições registradas: converta-a para CSV, XLSX ou JSON",
"error.fit_no_records": "A atividade não tem dados registrados",
+ "error.m4s_needs_init": "Este .m4s é um pedaço de um vídeo transmitido por streaming e não pode ser lido sem o seu segmento inicial (init) separado",
"notice.pdf_not_smaller": "Não deu para deixar menor, então foi copiado como está",
"notice.cad_newer_left_out": "Objetos que a versão do AutoCAD escolhida não tem (multilinhas de chamada, malhas…) ficaram de fora",
"notice.original_kept": "O original foi mantido, o resultado foi salvo como cópia",
diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json
index 9cc6e393..2539e0e4 100644
--- a/src/Filee.App/Assets/i18n/pt-PT.json
+++ b/src/Filee.App/Assets/i18n/pt-PT.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "Este ficheiro FIT não é uma atividade (só são convertidas atividades)",
"error.fit_no_positions": "A atividade não tem posições registadas: converte-a para CSV, XLSX ou JSON",
"error.fit_no_records": "A atividade não tem dados registados",
+ "error.m4s_needs_init": "Este .m4s é um pedaço de um vídeo em streaming e não pode ser lido sem o respetivo segmento inicial (init) separado",
"notice.pdf_not_smaller": "Não foi possível reduzir o tamanho, pelo que o ficheiro foi copiado sem alterações",
"notice.cad_newer_left_out": "Os objetos que a versão de AutoCAD escolhida não suporta (linhas de chamada múltiplas, malhas…) foram omitidos",
"notice.original_kept": "O original foi mantido e o resultado foi guardado como cópia",
diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json
index c0b3a2fc..01227031 100644
--- a/src/Filee.App/Assets/i18n/ru.json
+++ b/src/Filee.App/Assets/i18n/ru.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "Этот файл FIT не является активностью (конвертируются только активности)",
"error.fit_no_positions": "В активности нет записанных координат: конвертируйте её в CSV, XLSX или JSON",
"error.fit_no_records": "В активности нет записанных данных",
+ "error.m4s_needs_init": "Этот .m4s — фрагмент потокового видео: без отдельного начального сегмента (init) его не прочитать",
"notice.pdf_not_smaller": "Уменьшить не получилось, файл скопирован как есть",
"notice.cad_newer_left_out": "Объекты, которых нет в выбранной версии AutoCAD (мультивыноски, сети…), не сохранены",
"notice.original_kept": "Оригинал оставлен, результат сохранён как копия",
diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json
index 83b3c7a5..c5f44a77 100644
--- a/src/Filee.App/Assets/i18n/th.json
+++ b/src/Filee.App/Assets/i18n/th.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "ไฟล์ FIT นี้ไม่ใช่กิจกรรม (แปลงได้เฉพาะกิจกรรมเท่านั้น)",
"error.fit_no_positions": "กิจกรรมนี้ไม่มีตำแหน่งที่บันทึกไว้ โปรดแปลงเป็น CSV, XLSX หรือ JSON",
"error.fit_no_records": "กิจกรรมนี้ไม่มีข้อมูลที่บันทึกไว้",
+ "error.m4s_needs_init": "ไฟล์ .m4s นี้เป็นชิ้นส่วนหนึ่งของวิดีโอสตรีมมิง และอ่านไม่ได้หากไม่มีเซกเมนต์เริ่มต้น (init) ที่แยกไว้",
"notice.pdf_not_smaller": "ไม่สามารถบีบอัดให้เล็กลงได้ จึงคัดลอกไฟล์ตามเดิม",
"notice.cad_newer_left_out": "ละวัตถุที่ AutoCAD เวอร์ชันที่เลือกไม่มี (มัลติลีดเดอร์ เมช …) ไว้",
"notice.original_kept": "เก็บไฟล์ต้นฉบับไว้ และบันทึกผลลัพธ์เป็นสำเนา",
diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json
index 09c66d35..a4280d0f 100644
--- a/src/Filee.App/Assets/i18n/tr.json
+++ b/src/Filee.App/Assets/i18n/tr.json
@@ -121,6 +121,7 @@
"error.fit_not_activity": "Bu FIT dosyası bir etkinlik değil (yalnızca etkinlikler dönüştürülür)",
"error.fit_no_positions": "Etkinlikte kaydedilmiş konum yok: CSV, XLSX veya JSON'a dönüştürün",
"error.fit_no_records": "Etkinlikte kaydedilmiş veri yok",
+ "error.m4s_needs_init": "Bu .m4s, akışla gelen bir videonun bir parçası: ayrı başlangıç (init) segmenti olmadan okunamaz",
"notice.pdf_not_smaller": "Küçültülemediği için olduğu gibi kopyalandı",
"notice.cad_newer_left_out": "Seçilen AutoCAD sürümünün desteklemediği nesneler (çoklu kılavuzlar, ağlar…) çıkarıldı",
"notice.original_kept": "Özgün dosya korundu, sonuç kopya olarak kaydedildi",
diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json
index fa125720..debf9378 100644
--- a/src/Filee.App/Assets/i18n/zh-CN.json
+++ b/src/Filee.App/Assets/i18n/zh-CN.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "此 FIT 文件不是活动记录(只能转换活动记录)",
"error.fit_no_positions": "活动没有记录位置信息,请转换为 CSV、XLSX 或 JSON",
"error.fit_no_records": "活动没有记录数据",
+ "error.m4s_needs_init": "此 .m4s 是流媒体视频的一个片段,没有单独的起始(init)分段就无法读取",
"notice.pdf_not_smaller": "无法再缩小,已原样复制",
"notice.cad_newer_left_out": "已省略所选 AutoCAD 版本没有的对象(多重引线、网格……)",
"notice.original_kept": "已保留原文件,结果另存为副本",
diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json
index 03c538be..640912ba 100644
--- a/src/Filee.App/Assets/i18n/zh-TW.json
+++ b/src/Filee.App/Assets/i18n/zh-TW.json
@@ -127,6 +127,7 @@
"error.fit_not_activity": "此 FIT 檔案不是活動記錄(只能轉換活動記錄)",
"error.fit_no_positions": "活動沒有記錄位置資訊,請轉換為 CSV、XLSX 或 JSON",
"error.fit_no_records": "活動沒有記錄資料",
+ "error.m4s_needs_init": "此 .m4s 是串流影片的一個片段,沒有另外的起始(init)區段就無法讀取",
"notice.pdf_not_smaller": "無法再縮小,已原樣複製",
"notice.cad_newer_left_out": "已略過所選 AutoCAD 版本沒有的物件(複線引線、網格……)",
"notice.original_kept": "已保留原始檔案,結果另存為副本",
diff --git a/src/Filee.Core/Formats/FormatRegistry.cs b/src/Filee.Core/Formats/FormatRegistry.cs
index c036e172..41ea7c2d 100644
--- a/src/Filee.Core/Formats/FormatRegistry.cs
+++ b/src/Filee.Core/Formats/FormatRegistry.cs
@@ -226,6 +226,9 @@ public static class FormatRegistry
new("cavs", "CAVS", FormatCategory.Video, ["cavs"], Writable: false),
// CRI Middleware's video for game cutscenes (#485): FFmpeg's usm demuxer reads unencrypted ones.
new("usm", "USM", FormatCategory.Video, ["usm"], Writable: false),
+ // Fragmented MP4 (#354): an .m4s with its own moov (ftyp + moov, then styp / sidx / moof / mdat) is read by FFmpeg;
+ // a DASH media segment that needs a separate init segment is refused (DashSegment).
+ new("m4s", "M4S", FormatCategory.Video, ["m4s"], Writable: false),
// Raw H.265 / H.264 streams with no container, as security cameras and VMS software export them (issue #301).
new("h265", "H.265", FormatCategory.Video, ["h265", "265", "hevc"], Writable: false),
new("h264", "H.264", FormatCategory.Video, ["h264", "264"], Writable: false),
diff --git a/src/Filee.Core/Settings/AppSettings.cs b/src/Filee.Core/Settings/AppSettings.cs
index 380a4898..bede3d9c 100644
--- a/src/Filee.Core/Settings/AppSettings.cs
+++ b/src/Filee.Core/Settings/AppSettings.cs
@@ -123,7 +123,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable gest
public sealed class AppSettings
{
/// Current schema version. Increase when the format changes and add a migration.
- public const int CurrentSchemaVersion = 47;
+ public const int CurrentSchemaVersion = 48;
public int SchemaVersion { get; set; } = CurrentSchemaVersion;
diff --git a/src/Filee.Core/Settings/UserDataStore.cs b/src/Filee.Core/Settings/UserDataStore.cs
index 2d23b460..10363960 100644
--- a/src/Filee.Core/Settings/UserDataStore.cs
+++ b/src/Filee.Core/Settings/UserDataStore.cs
@@ -886,6 +886,9 @@ public static bool ApplyToLibrary(int from, List presets, ListTop-level boxes looked at before giving up; a moov or moof comes well before this in real files.
+ private const int MaxBoxes = 4096;
+
+ ///
+ /// Refuses an .m4s that is a media segment without its init segment; any other file (or one that isn't
+ /// ISO BMFF at all) is left to FFmpeg.
+ ///
+ public static void ThrowIfNeedsInitSegment(string path)
+ {
+ if (!Path.GetExtension(path).Equals(".m4s", StringComparison.OrdinalIgnoreCase))
+ return;
+ bool needsInit;
+ try
+ {
+ using var file = File.OpenRead(path);
+ needsInit = NeedsInitSegment(file);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ return; // FFmpeg reports what is wrong with the file.
+ }
+ if (needsInit)
+ throw new ConversionRefusedException("error.m4s_needs_init");
+ }
+
+ ///
+ /// True when the top-level boxes reach a moof (a movie fragment) before any moov: the fragments
+ /// can't be decoded without the moov of a separate init segment. False when a moov comes first, and for data that
+ /// isn't a box structure.
+ ///
+ public static bool NeedsInitSegment(Stream file)
+ {
+ try
+ {
+ for (var i = 0; i < MaxBoxes && file.Length - file.Position >= 8; i++)
+ {
+ var box = MovieBoxes.ReadHeader(file);
+ switch (box.Type)
+ {
+ case "moov":
+ return false;
+ case "moof":
+ return true;
+ }
+ file.Position = box.End;
+ }
+ }
+ catch (Exception ex) when (ex is InvalidDataException or EndOfStreamException or OverflowException)
+ {
+ // Not ISO BMFF, or damaged: FFmpeg decides.
+ }
+ return false;
+ }
+}
diff --git a/src/Filee.Engines/Media/MediaInfo.cs b/src/Filee.Engines/Media/MediaInfo.cs
index af2d2895..0bd023d3 100644
--- a/src/Filee.Engines/Media/MediaInfo.cs
+++ b/src/Filee.Engines/Media/MediaInfo.cs
@@ -93,9 +93,13 @@ internal sealed record MediaInfo(TimeSpan? Duration, VideoStreamInfo? Video, Aud
private static readonly TimeSpan ProbeTimeout = TimeSpan.FromMinutes(1);
- /// Runs ffprobe on ; throws with FFmpeg's message when the file cannot be read.
+ ///
+ /// Runs ffprobe on ; throws with FFmpeg's message when the file cannot be read, or refuses an
+ /// .m4s media segment that needs its init segment ().
+ ///
public static async Task ProbeAsync(string ffprobe, string input, CancellationToken cancellationToken)
{
+ DashSegment.ThrowIfNeedsInitSegment(input);
var result = await ProcessRunner.RunAsync(ffprobe,
["-hide_banner", "-v", "error", "-print_format", "json", "-show_format", "-show_streams", "-show_data_hash", "MD5", input],
ProbeTimeout, cancellationToken);
diff --git a/tests/Filee.Core.Tests/M4sFormatTests.cs b/tests/Filee.Core.Tests/M4sFormatTests.cs
new file mode 100644
index 00000000..bc2219e4
--- /dev/null
+++ b/tests/Filee.Core.Tests/M4sFormatTests.cs
@@ -0,0 +1,34 @@
+// Fragmented MP4 pieces (.m4s, #354): a read-only video source on the video donut, learned by existing libraries.
+
+using Filee.Core.Formats;
+using Filee.Core.Presets;
+using Filee.Core.Profiles;
+using Filee.Core.Settings;
+
+namespace Filee.Core.Tests;
+
+public class M4sFormatTests
+{
+ [Fact]
+ public void M4s_is_a_video_filee_reads_but_never_writes()
+ {
+ var m4s = FormatRegistry.Detect("video-1.M4S");
+ Assert.NotNull(m4s);
+ Assert.Equal(("m4s", FormatCategory.Video, false), (m4s.Id, m4s.Category, m4s.Writable));
+ Assert.Equal("video", ProfileSelector.Select(BuiltInData.CreateProfiles(), ["movie.m4s"])?.Id);
+ }
+
+ [Fact]
+ public void Version_47_libraries_learn_m4s_once()
+ {
+ var presets = BuiltInData.CreatePresets();
+ var profiles = BuiltInData.CreateProfiles();
+ var video = profiles.Single(p => p.Id == "video");
+ video.Extensions.Remove("m4s");
+
+ Assert.True(SettingsMigrations.ApplyToLibrary(47, presets, profiles));
+
+ Assert.Contains("m4s", video.Extensions);
+ Assert.False(SettingsMigrations.ApplyToLibrary(47, presets, profiles));
+ }
+}
diff --git a/tests/Filee.Engines.Tests/DashSegmentTests.cs b/tests/Filee.Engines.Tests/DashSegmentTests.cs
new file mode 100644
index 00000000..2f5b9e33
--- /dev/null
+++ b/tests/Filee.Engines.Tests/DashSegmentTests.cs
@@ -0,0 +1,76 @@
+// .m4s pieces (#354): which ones need a separate init segment, decided from the top-level ISO BMFF boxes alone.
+
+using System.Buffers.Binary;
+using System.Text;
+using Filee.Core.Conversion;
+using Filee.Engines.Media;
+
+namespace Filee.Engines.Tests;
+
+public class DashSegmentTests
+{
+ private static byte[] Box(string type, int payload = 8)
+ {
+ var box = new byte[8 + payload];
+ BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
+ Encoding.Latin1.GetBytes(type, box.AsSpan(4));
+ return box;
+ }
+
+ private static bool NeedsInit(params string[] boxes) =>
+ DashSegment.NeedsInitSegment(new MemoryStream(boxes.SelectMany(b => Box(b)).ToArray()));
+
+ [Fact]
+ public void A_media_segment_without_a_moov_needs_its_init_segment()
+ {
+ Assert.True(NeedsInit("styp", "sidx", "moof", "mdat", "styp", "sidx", "moof", "mdat"));
+ Assert.True(NeedsInit("moof", "mdat"));
+ }
+
+ [Fact]
+ public void A_fragmented_mp4_with_its_own_moov_is_left_to_ffmpeg()
+ {
+ // The reporter's file: ftyp + moov, then styp / sidx / moof / mdat sets (GPAC).
+ Assert.False(NeedsInit("ftyp", "moov", "styp", "sidx", "moof", "mdat", "styp", "sidx", "moof", "mdat"));
+ // A plain MP4 renamed to .m4s, also with the moov at the end.
+ Assert.False(NeedsInit("ftyp", "moov", "mdat"));
+ Assert.False(NeedsInit("ftyp", "free", "mdat", "moov"));
+ }
+
+ [Fact]
+ public void Data_that_is_not_a_box_structure_is_left_to_ffmpeg()
+ {
+ Assert.False(DashSegment.NeedsInitSegment(new MemoryStream([])));
+ Assert.False(DashSegment.NeedsInitSegment(new MemoryStream(Encoding.ASCII.GetBytes("#EXTM3U\n#EXT-X-VERSION:7\n"))));
+ // A box that claims more bytes than the file has.
+ var cut = Box("styp").Concat(Box("moof", 64)).Take(40).ToArray();
+ Assert.False(DashSegment.NeedsInitSegment(new MemoryStream(cut)));
+ }
+
+ [Fact]
+ public void Only_m4s_files_are_checked_and_a_bare_segment_is_refused_with_its_own_message()
+ {
+ var folder = Directory.CreateTempSubdirectory("filee-m4s-").FullName;
+ try
+ {
+ var segment = Box("styp").Concat(Box("moof")).Concat(Box("mdat")).ToArray();
+ var m4s = Path.Combine(folder, "video-3.m4s");
+ File.WriteAllBytes(m4s, segment);
+ var refused = Assert.Throws(() => DashSegment.ThrowIfNeedsInitSegment(m4s));
+ Assert.Equal("error.m4s_needs_init", refused.ErrorKey);
+
+ // The same bytes under another name are FFmpeg's to judge.
+ var mp4 = Path.Combine(folder, "video-3.mp4");
+ File.WriteAllBytes(mp4, segment);
+ DashSegment.ThrowIfNeedsInitSegment(mp4);
+
+ var whole = Path.Combine(folder, "whole.m4s");
+ File.WriteAllBytes(whole, Box("ftyp").Concat(Box("moov")).Concat(segment).ToArray());
+ DashSegment.ThrowIfNeedsInitSegment(whole);
+ }
+ finally
+ {
+ Directory.Delete(folder, recursive: true);
+ }
+ }
+}
diff --git a/tests/Filee.Engines.Tests/MediaTests.cs b/tests/Filee.Engines.Tests/MediaTests.cs
index 0bfe086f..e2321351 100644
--- a/tests/Filee.Engines.Tests/MediaTests.cs
+++ b/tests/Filee.Engines.Tests/MediaTests.cs
@@ -34,6 +34,62 @@ public void Cri_usm_game_videos_go_to_mp4_through_ffmpeg()
Assert.Null(fx.Catalog.CreatePlanner().Plan("mp4", "usm"));
}
+ [Fact]
+ public void M4s_pieces_are_read_but_never_written()
+ {
+ // #354: a fragmented MP4 FFmpeg reads directly; never a target.
+ var route = Planner().Plan("m4s", "mp4");
+ Assert.NotNull(route);
+ Assert.Equal("ffmpeg", Assert.Single(route.Steps).Converter.Id);
+ Assert.Null(Planner().Plan("mp4", "m4s"));
+ }
+
+ [Fact]
+ public async Task A_self_contained_m4s_converts_and_a_bare_dash_segment_is_refused()
+ {
+ SkipUnlessInstalled();
+ var folder = fx.NewFolder();
+ // Several fragments after an empty moov, as DASH packagers write them.
+ var fragmented = await MakeClipAsync(folder, "fragmented.mp4", seconds: 2, size: "160x120",
+ extra: ["-movflags", "+frag_keyframe+empty_moov+default_base_moof", "-frag_duration", "500000"]);
+ var boxes = TopLevelBoxes(await File.ReadAllBytesAsync(fragmented, TestContext.Current.CancellationToken));
+ Assert.Equal(["ftyp", "moov"], boxes.Take(2).Select(b => b.Type));
+ Assert.True(boxes.Count(b => b.Type == "moof") >= 2, string.Join(" ", boxes.Select(b => b.Type)));
+
+ // The reporter's layout (GPAC): ftyp + moov, then a styp before every moof / mdat pair.
+ byte[] styp = [0, 0, 0, 24, .. "stypmsdh"u8, 0, 0, 0, 0, .. "msdhmsix"u8];
+ var whole = Path.Combine(folder, "whole.m4s");
+ await File.WriteAllBytesAsync(whole, boxes.SelectMany(b => b.Type == "moof" ? [.. styp, .. b.Bytes] : b.Bytes).ToArray(),
+ TestContext.Current.CancellationToken);
+ var probed = await ProbeAsync(await ConvertAsync(whole, "mp4"));
+ Assert.NotNull(probed.Video);
+ Assert.NotNull(probed.Audio);
+ Assert.InRange(probed.Duration, 1.7, 2.4);
+
+ // Without ftyp and moov the fragments need an init segment: Filee's message, not FFmpeg's error.
+ var segment = Path.Combine(folder, "segment.m4s");
+ await File.WriteAllBytesAsync(segment, styp.Concat(boxes.Skip(2).SelectMany(b => b.Bytes)).ToArray(),
+ TestContext.Current.CancellationToken);
+ var job = await fx.ConvertAsync([segment], new Preset { TargetFormat = "mp4" });
+ var file = Assert.Single(job.Files);
+ Assert.Equal((FileState.Failed, "error.m4s_needs_init"), (file.State, file.ErrorKey));
+ Assert.Empty(Directory.GetFiles(folder, "segment*.mp4"));
+ }
+
+ /// The top-level boxes of a short MP4 (32-bit sizes, as FFmpeg writes them for small files).
+ private static List<(string Type, byte[] Bytes)> TopLevelBoxes(byte[] data)
+ {
+ var boxes = new List<(string, byte[])>();
+ for (var at = 0; at + 8 <= data.Length;)
+ {
+ var size = (int)System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(at));
+ Assert.InRange(size, 8, data.Length - at);
+ boxes.Add((System.Text.Encoding.Latin1.GetString(data, at + 4, 4), data[at..(at + size)]));
+ at += size;
+ }
+ return boxes;
+ }
+
private static readonly (string Ffmpeg, string Ffprobe)? Tools = FfmpegConverter.Locate();
private static void SkipUnlessInstalled() =>
|