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() =>