Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.ar.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
تظهر الصيغ حيث أنت، ولا يظهر منها إلا ما يناسب الملفات التي تسحبها.
</td>
<td width="33%" valign="top">
<b>266 صيغة دون برامج أخرى</b><br>
<b>267 صيغة دون برامج أخرى</b><br>
الصور وPDF وWord وExcel وPowerPoint وHWP والكتب الإلكترونية والأرشيفات والخطوط وCAD والنماذج ثلاثية الأبعاد والبيانات الجغرافية، كلها تعمل دون Office أو Hancom Office أو LibreOffice.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
Los formatos aparecen donde ya estás, y solo los que tienen sentido para los archivos que arrastras.
</td>
<td width="33%" valign="top">
<b>266 formatos, sin otros programas</b><br>
<b>267 formatos, sin otros programas</b><br>
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.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</td>
<td width="33%" valign="top">
<b>266 formats, sans autre logiciel</b><br>
<b>267 formats, sans autre logiciel</b><br>
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.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.hi.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
फ़ॉर्मैट वहीं दिखते हैं जहाँ आप पहले से हैं, और सिर्फ़ वही जो आपकी खींची जा रही फ़ाइलों के लिए सही हों।
</td>
<td width="33%" valign="top">
<b>266 फ़ॉर्मैट, कोई और सॉफ़्टवेयर नहीं</b><br>
<b>267 फ़ॉर्मैट, कोई और सॉफ़्टवेयर नहीं</b><br>
इमेज, PDF, Word, Excel, PowerPoint, HWP, ई-बुक, आर्काइव, फ़ॉन्ट, CAD, 3D मॉडल और भू-स्थानिक डेटा Office, Hancom Office या LibreOffice के बिना काम करते हैं।
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
いま作業している場所に、ドラッグ中のファイルに合う形式だけが表示されます。
</td>
<td width="33%" valign="top">
<b>266形式、ほかのソフトは不要</b><br>
<b>267形式、ほかのソフトは不要</b><br>
画像、PDF、Word、Excel、PowerPoint、HWP、電子書籍、アーカイブ、フォント、CAD、3Dモデル、地理空間データを、Office、Hancom Office、LibreOfficeなしで変換できます。
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
끌고 있는 파일에 맞는 형식만 지금 있는 자리에 나타나요.
</td>
<td width="33%" valign="top">
<b>266가지 형식, 다른 프로그램 없이</b><br>
<b>267가지 형식, 다른 프로그램 없이</b><br>
이미지, PDF, 워드, 엑셀, 파워포인트, HWP, 전자책, 압축 파일, 글꼴, CAD, 3D 모델, 지리 정보 데이터를 오피스·한컴오피스·LibreOffice 없이 변환해요.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
The formats appear where you already are, only the ones that make sense for the files you drag.
</td>
<td width="33%" valign="top">
<b>266 formats, no other software</b><br>
<b>267 formats, no other software</b><br>
Images, PDF, Word, Excel, PowerPoint, HWP, e-books, archives, fonts, CAD, 3D models and geospatial data work without Office, Hancom Office or LibreOffice.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
Formaty pojawiają się tam, gdzie już jesteś, i tylko te, które pasują do przeciąganych plików.
</td>
<td width="33%" valign="top">
<b>266 formatów bez dodatkowych programów</b><br>
<b>267 formatów bez dodatkowych programów</b><br>
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.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
Os formatos aparecem onde você já está, só os que fazem sentido para os arquivos que você arrasta.
</td>
<td width="33%" valign="top">
<b>266 formatos, sem outro programa</b><br>
<b>267 formatos, sem outro programa</b><br>
Imagens, PDF, Word, Excel, PowerPoint, HWP, e-books, arquivos compactados, fontes, CAD, modelos 3D e dados geoespaciais funcionam sem Office, Hancom Office ou LibreOffice.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.pt-PT.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
Os formatos aparecem onde estás, apenas os que fazem sentido para os ficheiros que arrastas.
</td>
<td width="33%" valign="top">
<b>266 formatos, sem outro software</b><br>
<b>267 formatos, sem outro software</b><br>
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.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
Форматы появляются там, где вы уже работаете, и только те, что подходят перетаскиваемым файлам.
</td>
<td width="33%" valign="top">
<b>266 форматов без лишних программ</b><br>
<b>267 форматов без лишних программ</b><br>
Изображения, PDF, Word, Excel, PowerPoint, HWP, электронные книги, архивы, шрифты, чертежи CAD, 3D-модели и геоданные работают без Office, Hancom Office и LibreOffice.
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.th.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
ฟอร์แมตที่เหมาะสมจะแสดงขึ้นตรงจุดที่คุณทำงานอยู่ โดยแสดงเฉพาะฟอร์แมตที่ใช้ได้กับไฟล์ที่คุณลากเท่านั้น
</td>
<td width="33%" valign="top">
<b>266 ฟอร์แมต โดยไม่ต้องลงโปรแกรมอื่นเพิ่ม</b><br>
<b>267 ฟอร์แมต โดยไม่ต้องลงโปรแกรมอื่นเพิ่ม</b><br>
รูปภาพ, PDF, Word, Excel, PowerPoint, HWP, อีบุ๊ก, ไฟล์บีบอัด, ฟอนต์, CAD, โมเดล 3D และข้อมูลเชิงพื้นที่ ทำงานได้โดยไม่ต้องมี Microsoft Office, Hancom Office หรือ LibreOffice
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
只显示适合当前拖动文件的格式,就在你所在的位置。
</td>
<td width="33%" valign="top">
<b>266 种格式,无需其他软件</b><br>
<b>267 种格式,无需其他软件</b><br>
图片、PDF、Word、Excel、PowerPoint、HWP、电子书、压缩包、字体、CAD、3D 模型和地理信息数据,无需 Office、韩文办公软件或 LibreOffice。
</td>
<td width="33%" valign="top">
Expand Down
2 changes: 1 addition & 1 deletion README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
只顯示適合當前拖曳檔案的格式,就在你所在的位置。
</td>
<td width="33%" valign="top">
<b>266 種格式,無需其他軟體</b><br>
<b>267 種格式,無需其他軟體</b><br>
圖片、PDF、Word、Excel、PowerPoint、HWP、電子書、壓縮檔、字型、CAD、3D 模型和地理資訊資料,無需 Office、韓文文書軟體或 LibreOffice。
</td>
<td width="33%" valign="top">
Expand Down
5 changes: 5 additions & 0 deletions docs/ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions src/Filee.Core/Formats/FormatRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion src/Filee.Core/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable<TriggerGesture> gest
public sealed class AppSettings
{
/// <summary>Current schema version. Increase when the format changes and add a migration.</summary>
public const int CurrentSchemaVersion = 49;
public const int CurrentSchemaVersion = 50;

public int SchemaVersion { get; set; } = CurrentSchemaVersion;

Expand Down
5 changes: 4 additions & 1 deletion src/Filee.Core/Settings/UserDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -890,9 +890,12 @@ public static bool ApplyToLibrary(int from, List<Preset> presets, List<ToolbarPr
if (from < 48)
changed |= LearnExtensions(profiles, out _, ["m4s"]);
// v49: SQL (#607, which came without a migration), AutoHotkey, INI, RouterOS exports and .cmd batch scripts learn
// the text donut; custom claims stay put. (Schema 49 may be renumbered if another change takes it first.)
// the text donut; custom claims stay put.
if (from < 49)
changed |= LearnExtensions(profiles, out _, ["sql", "ahk", "ini", "rsc", "cmd"]);
// v50: Dahua DVR recordings (.dav) learn the video donut; custom claims stay put.
if (from < 50)
changed |= LearnExtensions(profiles, out _, ["dav"]);
return changed;
}

Expand Down
34 changes: 34 additions & 0 deletions tests/Filee.Core.Tests/DavFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Dahua DVR recordings (.dav, #594): 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 DavFormatTests
{
[Fact]
public void Dav_is_a_video_filee_reads_but_never_writes()
{
var dav = FormatRegistry.Detect("ch10_main_20261005150000.DAV");
Assert.NotNull(dav);
Assert.Equal(("dav", FormatCategory.Video, false), (dav.Id, dav.Category, dav.Writable));
Assert.Equal("video", ProfileSelector.Select(BuiltInData.CreateProfiles(), ["camera.dav"])?.Id);
}

[Fact]
public void Version_49_libraries_learn_dav_once()
{
var presets = BuiltInData.CreatePresets();
var profiles = BuiltInData.CreateProfiles();
var video = profiles.Single(p => 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));
}
}
112 changes: 112 additions & 0 deletions tests/Filee.Engines.Tests/MediaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +45,117 @@
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);
}

/// <summary>Dahua's DHAV container, as far as FFmpeg's dhav demuxer reads it.</summary>
private static class Dhav
{
/// <summary>
/// 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.
/// </summary>
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<byte> extension, ReadOnlySpan<byte> 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<byte> 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<byte> trailer = stackalloc byte[4];
BinaryPrimitives.WriteInt32LittleEndian(trailer, length);
output.Write(trailer);
}

private static List<byte[]> AccessUnits(byte[] annexB)
{
byte[] delimiter = [0, 0, 0, 1, 9];
var starts = new List<int>();
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();
}

/// <summary>An access unit with an IDR slice (NAL type 5).</summary>
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()
{
Expand Down Expand Up @@ -821,7 +933,7 @@
// The coded pictures are the source's, byte for byte.
async Task<string> Hash(string file)
{
var hash = await ProcessRunner.RunAsync(Tools.Value.Ffmpeg,

Check warning on line 936 in tests/Filee.Engines.Tests/MediaTests.cs

View workflow job for this annotation

GitHub Actions / build

Nullable value type may be null.

Check warning on line 936 in tests/Filee.Engines.Tests/MediaTests.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04, linux-x64)

Nullable value type may be null.

Check warning on line 936 in tests/Filee.Engines.Tests/MediaTests.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04-arm, linux-arm64)

Nullable value type may be null.
["-hide_banner", "-v", "error", "-i", file, "-map", "0:v", "-c", "copy", "-f", "hash", "-hash", "md5", "-"],
TimeSpan.FromMinutes(1), CancellationToken.None);
return hash.StandardOutput.Trim();
Expand Down
Loading