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
3 changes: 3 additions & 0 deletions docs/ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,9 @@ streams without a container: FFmpeg reports them as format `hevc` / `h264` (`Med
encoded, never copied, because their frames carry no timestamps (a copy into MP4 had non-monotonic DTS). A stream
without timing information is read at FFmpeg's default 25 fps, so a camera that recorded at another rate plays faster
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 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
2 changes: 2 additions & 0 deletions src/Filee.Core/Formats/FormatRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ public static class FormatRegistry
new("swf", "SWF", FormatCategory.Video, ["swf"], Writable: false),
new("wtv", "WTV", FormatCategory.Video, ["wtv", "dvr-ms", "dvr"], Writable: false),
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),
// 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 = 43;
public const int CurrentSchemaVersion = 44;

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

Expand Down
3 changes: 3 additions & 0 deletions src/Filee.Core/Settings/UserDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,9 @@ public static bool ApplyToLibrary(int from, List<Preset> presets, List<ToolbarPr
// renumbered if another change takes it first.)
if (from < 43)
changed |= LearnExtensions(profiles, out _, ["fits", "fts"]);
// v44: CRI USM game videos (.usm) learn the video donut; custom claims stay put.
if (from < 44)
changed |= LearnExtensions(profiles, out _, ["usm"]);
return changed;
}

Expand Down
34 changes: 34 additions & 0 deletions tests/Filee.Core.Tests/UsmFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// CRI USM game videos (#485): 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 UsmFormatTests
{
[Fact]
public void Usm_is_a_video_filee_reads_but_never_writes()
{
var usm = FormatRegistry.Detect("cutscene.USM");
Assert.NotNull(usm);
Assert.Equal(("usm", FormatCategory.Video, false), (usm.Id, usm.Category, usm.Writable));
Assert.Equal("video", ProfileSelector.Select(BuiltInData.CreateProfiles(), ["movie01.usm"])?.Id);
}

[Fact]
public void Version_43_libraries_learn_usm_once()
{
var presets = BuiltInData.CreatePresets();
var profiles = BuiltInData.CreateProfiles();
var video = profiles.Single(p => p.Id == "video");
video.Extensions.Remove("usm");

Assert.True(SettingsMigrations.ApplyToLibrary(43, presets, profiles));

Assert.Contains("usm", video.Extensions);
Assert.False(SettingsMigrations.ApplyToLibrary(43, presets, profiles));
}
}
11 changes: 11 additions & 0 deletions tests/Filee.Engines.Tests/MediaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@
[Collection(nameof(FfmpegCollection))]
public class MediaTests(EngineFixture fx) : IClassFixture<EngineFixture>
{
[Fact]
public void Cri_usm_game_videos_go_to_mp4_through_ffmpeg()
{
// #485: read only, through FFmpeg's usm demuxer; never a step towards something else.
SkipUnlessInstalled();
var route = fx.Catalog.CreatePlanner().Plan("usm", "mp4");
Assert.NotNull(route);
Assert.Equal("ffmpeg", Assert.Single(route.Steps).Converter.Id);
Assert.Null(fx.Catalog.CreatePlanner().Plan("mp4", "usm"));
}

private static readonly (string Ffmpeg, string Ffprobe)? Tools = FfmpegConverter.Locate();

private static void SkipUnlessInstalled() =>
Expand Down Expand Up @@ -754,7 +765,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 768 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 768 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 768 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