diff --git a/README.ar.md b/README.ar.md
index d4ddf7c2e..8c000f8e9 100644
--- a/README.ar.md
+++ b/README.ar.md
@@ -58,7 +58,7 @@
تظهر الصيغ حيث أنت، ولا يظهر منها إلا ما يناسب الملفات التي تسحبها.
- 266 صيغة دون برامج أخرى
+ 267 صيغة دون برامج أخرى
الصور وPDF وWord وExcel وPowerPoint وHWP والكتب الإلكترونية والأرشيفات والخطوط وCAD والنماذج ثلاثية الأبعاد والبيانات الجغرافية، كلها تعمل دون Office أو Hancom Office أو LibreOffice.
|
diff --git a/README.es.md b/README.es.md
index fb39aa7f6..13358a122 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.
|
- 266 formatos, sin otros programas
+ 267 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 f82ce86f0..fc04c878d 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.
|
- 266 formats, sans autre logiciel
+ 267 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 e05973088..29373cf82 100644
--- a/README.hi.md
+++ b/README.hi.md
@@ -56,7 +56,7 @@
फ़ॉर्मैट वहीं दिखते हैं जहाँ आप पहले से हैं, और सिर्फ़ वही जो आपकी खींची जा रही फ़ाइलों के लिए सही हों।
|
- 266 फ़ॉर्मैट, कोई और सॉफ़्टवेयर नहीं
+ 267 फ़ॉर्मैट, कोई और सॉफ़्टवेयर नहीं
इमेज, PDF, Word, Excel, PowerPoint, HWP, ई-बुक, आर्काइव, फ़ॉन्ट, CAD, 3D मॉडल और भू-स्थानिक डेटा Office, Hancom Office या LibreOffice के बिना काम करते हैं।
|
diff --git a/README.ja.md b/README.ja.md
index ea211204a..0af9d0981 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -56,7 +56,7 @@
いま作業している場所に、ドラッグ中のファイルに合う形式だけが表示されます。
|
- 266形式、ほかのソフトは不要
+ 267形式、ほかのソフトは不要
画像、PDF、Word、Excel、PowerPoint、HWP、電子書籍、アーカイブ、フォント、CAD、3Dモデル、地理空間データを、Office、Hancom Office、LibreOfficeなしで変換できます。
|
diff --git a/README.ko.md b/README.ko.md
index 01740b605..3bb2a685d 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -57,7 +57,7 @@
끌고 있는 파일에 맞는 형식만 지금 있는 자리에 나타나요.
|
- 266가지 형식, 다른 프로그램 없이
+ 267가지 형식, 다른 프로그램 없이
이미지, PDF, 워드, 엑셀, 파워포인트, HWP, 전자책, 압축 파일, 글꼴, CAD, 3D 모델, 지리 정보 데이터를 오피스·한컴오피스·LibreOffice 없이 변환해요.
|
diff --git a/README.md b/README.md
index 29a6c995e..44f912cde 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.
|
- 266 formats, no other software
+ 267 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 35bfc3a11..e2d213928 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.
|
- 266 formatów bez dodatkowych programów
+ 267 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 91fc978ae..d5761d023 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.
|
- 266 formatos, sem outro programa
+ 267 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 23d027e4f..8993a13fb 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.
|
- 266 formatos, sem outro software
+ 267 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 0f62d18d3..c7ca7777b 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -56,7 +56,7 @@
Форматы появляются там, где вы уже работаете, и только те, что подходят перетаскиваемым файлам.
|
- 266 форматов без лишних программ
+ 267 форматов без лишних программ
Изображения, PDF, Word, Excel, PowerPoint, HWP, электронные книги, архивы, шрифты, чертежи CAD, 3D-модели и геоданные работают без Office, Hancom Office и LibreOffice.
|
diff --git a/README.th.md b/README.th.md
index 2c4c456f9..fcd73a938 100644
--- a/README.th.md
+++ b/README.th.md
@@ -56,7 +56,7 @@
ฟอร์แมตที่เหมาะสมจะแสดงขึ้นตรงจุดที่คุณทำงานอยู่ โดยแสดงเฉพาะฟอร์แมตที่ใช้ได้กับไฟล์ที่คุณลากเท่านั้น
|
- 266 ฟอร์แมต โดยไม่ต้องลงโปรแกรมอื่นเพิ่ม
+ 267 ฟอร์แมต โดยไม่ต้องลงโปรแกรมอื่นเพิ่ม
รูปภาพ, 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 149de6a97..2608a02d3 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -56,7 +56,7 @@
只显示适合当前拖动文件的格式,就在你所在的位置。
|
- 266 种格式,无需其他软件
+ 267 种格式,无需其他软件
图片、PDF、Word、Excel、PowerPoint、HWP、电子书、压缩包、字体、CAD、3D 模型和地理信息数据,无需 Office、韩文办公软件或 LibreOffice。
|
diff --git a/README.zh-TW.md b/README.zh-TW.md
index bda85f7d4..1c459297b 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -56,7 +56,7 @@
只顯示適合當前拖曳檔案的格式,就在你所在的位置。
|
- 266 種格式,無需其他軟體
+ 267 種格式,無需其他軟體
圖片、PDF、Word、Excel、PowerPoint、HWP、電子書、壓縮檔、字型、CAD、3D 模型和地理資訊資料,無需 Office、韓文文書軟體或 LibreOffice。
|
diff --git a/docs/ENGINES.md b/docs/ENGINES.md
index e3e5c6462..e0da463e7 100644
--- a/docs/ENGINES.md
+++ b/docs/ENGINES.md
@@ -1463,6 +1463,11 @@ A self-contained `.m4s` (read only, #354), a fragmented MP4 that starts with `ft
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.
+Dahua DVR / NVR recordings (`.dav`, read only, #594) are read by FFmpeg's `dhav` demuxer, which finds them by the
+`DHAV` chunks they are made of, and go to every video and audio target. Their H.264 / H.265 video is copied where
+the target and preset allow it (`CanCopyVideo`), like any other container's; G.711 sound is encoded for targets that
+can't hold it. The chunks carry the camera's wall-clock time, so FFmpeg reports the recording date as the start time;
+the output starts at zero.
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.Core/Formats/FormatRegistry.cs b/src/Filee.Core/Formats/FormatRegistry.cs
index b5d77853b..ad752f1c1 100644
--- a/src/Filee.Core/Formats/FormatRegistry.cs
+++ b/src/Filee.Core/Formats/FormatRegistry.cs
@@ -230,6 +230,9 @@ public static class FormatRegistry
// 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),
+ // Dahua and similar DVR / NVR recordings (#594): FFmpeg's dhav demuxer reads them (H.264 / H.265 video, often
+ // with G.711 or AAC sound).
+ new("dav", "DAV", FormatCategory.Video, ["dav"], 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 b5f953c47..10736b215 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 = 49;
+ public const int CurrentSchemaVersion = 50;
public int SchemaVersion { get; set; } = CurrentSchemaVersion;
diff --git a/src/Filee.Core/Settings/UserDataStore.cs b/src/Filee.Core/Settings/UserDataStore.cs
index d4ae8582c..76eeee2f1 100644
--- a/src/Filee.Core/Settings/UserDataStore.cs
+++ b/src/Filee.Core/Settings/UserDataStore.cs
@@ -890,9 +890,12 @@ public static bool ApplyToLibrary(int from, List presets, List p.Id == "video");
+ video.Extensions.Remove("dav");
+
+ Assert.True(SettingsMigrations.ApplyToLibrary(49, presets, profiles));
+
+ Assert.Contains("dav", video.Extensions);
+ Assert.False(SettingsMigrations.ApplyToLibrary(49, presets, profiles));
+ }
+}
diff --git a/tests/Filee.Engines.Tests/MediaTests.cs b/tests/Filee.Engines.Tests/MediaTests.cs
index e23213518..30a558bcd 100644
--- a/tests/Filee.Engines.Tests/MediaTests.cs
+++ b/tests/Filee.Engines.Tests/MediaTests.cs
@@ -2,6 +2,7 @@
// when Filee's FFmpeg is installed (`pwsh build/fetch-engines.ps1 -Only ffmpeg`; CI does not install it). Inputs are
// generated by FFmpeg itself (test pattern + sine tone) and every output is checked with ffprobe.
+using System.Buffers.Binary;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
@@ -44,6 +45,117 @@ public void M4s_pieces_are_read_but_never_written()
Assert.Null(Planner().Plan("mp4", "m4s"));
}
+ [Fact]
+ public void Dav_recordings_are_read_but_never_written()
+ {
+ // #594: Dahua DVR recordings, read by FFmpeg's dhav demuxer; never a target.
+ var route = Planner().Plan("dav", "mp4");
+ Assert.NotNull(route);
+ Assert.Equal("ffmpeg", Assert.Single(route.Steps).Converter.Id);
+ Assert.Null(Planner().Plan("mp4", "dav"));
+ }
+
+ [Fact]
+ public async Task A_dahua_dav_recording_converts_to_mp4()
+ {
+ // FFmpeg has no DHAV muxer, so the recording is put together here the way Dahua cameras write it: H.264 frames
+ // and G.711 A-law sound in DHAV chunks.
+ SkipUnlessInstalled();
+ var folder = fx.NewFolder();
+ var h264 = await MakeClipAsync(folder, "camera.h264", size: "160x120", audio: false,
+ extra: ["-c:v", "libx264", "-g", "25", "-bsf:v", "h264_metadata=aud=insert"]);
+ var alaw = Path.Combine(folder, "camera.alaw");
+ await RunAsync(Tools!.Value.Ffmpeg, ["-hide_banner", "-nostdin", "-v", "error", "-y", "-f", "lavfi",
+ "-i", "sine=frequency=440:sample_rate=8000:duration=2", "-ac", "1", "-f", "alaw", alaw]);
+ var dav = Path.Combine(folder, "ch01_main.dav");
+ await File.WriteAllBytesAsync(dav, Dhav.Recording(await File.ReadAllBytesAsync(h264, TestContext.Current.CancellationToken),
+ await File.ReadAllBytesAsync(alaw, TestContext.Current.CancellationToken), 160, 120), TestContext.Current.CancellationToken);
+
+ var probe = await ProcessRunner.RunAsync(Tools!.Value.Ffprobe, ["-v", "error", "-show_entries", "format=format_name", "-of", "csv=p=0", dav],
+ TimeSpan.FromMinutes(1), CancellationToken.None);
+ Assert.Equal("dhav", probe.StandardOutput.Trim());
+
+ var probed = await ProbeAsync(await ConvertAsync(dav, "mp4"));
+ var video = probed.Video;
+ Assert.NotNull(video);
+ Assert.Equal(("h264", 160, 120, 50), (video.Codec, video.Width, video.Height, video.Frames));
+ Assert.Equal("aac", probed.Audio?.Codec);
+ Assert.InRange(probed.Duration, 1.7, 2.4);
+ }
+
+ /// Dahua's DHAV container, as far as FFmpeg's dhav demuxer reads it.
+ private static class Dhav
+ {
+ ///
+ /// One video chunk per H.264 access unit (each starting with an access unit delimiter) at 25 fps, each followed
+ /// by 40 ms of 8 kHz mono A-law sound.
+ ///
+ public static byte[] Recording(byte[] annexB, byte[] alaw, int width, int height)
+ {
+ var units = AccessUnits(annexB);
+ var output = new MemoryStream();
+ for (var i = 0; i < units.Count; i++)
+ {
+ var ms = i * 40;
+ // 0x80: size in units of 8 pixels; 0x81: codec 8 (H.264) at 25 fps.
+ byte[] video = [0x80, 0, (byte)(width / 8), (byte)(height / 8), 0x81, 0, 0x08, 25];
+ Chunk(output, IsKeyFrame(units[i]) ? (byte)0xFD : (byte)0xFC, i + 1, ms, video, units[i]);
+ var sound = alaw.AsSpan().Slice(Math.Min(i * 320, alaw.Length), Math.Clamp(alaw.Length - i * 320, 0, 320));
+ // 0x83: one channel, codec 0x0E (A-law), sample rate 0 (8 kHz).
+ if (!sound.IsEmpty)
+ Chunk(output, 0xF0, i + 1, ms, [0x83, 1, 0x0E, 0], sound);
+ }
+ return output.ToArray();
+ }
+
+ private static void Chunk(MemoryStream output, byte type, int number, int ms, ReadOnlySpan extension, ReadOnlySpan payload)
+ {
+ var length = 24 + extension.Length + payload.Length + 8;
+ var seconds = ms / 1000;
+ // Wall-clock time of the chunk: 2026-10-05 15:mm:ss.
+ var date = (uint)(seconds % 60 | seconds / 60 % 60 << 6 | 15 << 12 | 5 << 17 | 10 << 22 | (2026 - 2000) << 26);
+ Span header = stackalloc byte[24];
+ "DHAV"u8.CopyTo(header);
+ header[4] = type;
+ BinaryPrimitives.WriteInt32LittleEndian(header[8..], number);
+ BinaryPrimitives.WriteInt32LittleEndian(header[12..], length);
+ BinaryPrimitives.WriteUInt32LittleEndian(header[16..], date);
+ BinaryPrimitives.WriteUInt16LittleEndian(header[20..], (ushort)ms);
+ header[22] = (byte)extension.Length;
+ output.Write(header);
+ output.Write(extension);
+ output.Write(payload);
+ output.Write("dhav"u8);
+ Span trailer = stackalloc byte[4];
+ BinaryPrimitives.WriteInt32LittleEndian(trailer, length);
+ output.Write(trailer);
+ }
+
+ private static List AccessUnits(byte[] annexB)
+ {
+ byte[] delimiter = [0, 0, 0, 1, 9];
+ var starts = new List();
+ for (var at = annexB.AsSpan().IndexOf(delimiter); at >= 0;)
+ {
+ starts.Add(at);
+ var next = annexB.AsSpan(at + 1).IndexOf(delimiter);
+ at = next < 0 ? -1 : at + 1 + next;
+ }
+ return starts.Select((start, i) => annexB[start..(i + 1 < starts.Count ? starts[i + 1] : annexB.Length)]).ToList();
+ }
+
+ /// An access unit with an IDR slice (NAL type 5).
+ private static bool IsKeyFrame(byte[] unit)
+ {
+ for (var i = 0; i + 3 < unit.Length; i++)
+ {
+ if (unit[i] == 0 && unit[i + 1] == 0 && unit[i + 2] == 1 && (unit[i + 3] & 0x1F) == 5)
+ return true;
+ }
+ return false;
+ }
+ }
+
[Fact]
public async Task A_self_contained_m4s_converts_and_a_bare_dash_segment_is_refused()
{
|