diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs new file mode 100644 index 0000000000..2dcd37123d --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs @@ -0,0 +1,107 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckAudioInVideoTest + { + private CheckAudioInVideo check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckAudioInVideo(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.mp4", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestRegularVideoFile() + { + Assert.IsEmpty(check.Run(getContext("Videos/test-video.mp4"))); + } + + [Test] + public void TestVideoFileWithAudio() + { + var issues = check.Run(getContext("Videos/test-video-with-audio.mp4")).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack); + } + + [Test] + public void TestVideoFileWithTrackButNoAudio() + { + var issues = check.Run(getContext("Videos/test-video-with-track-but-no-audio.mp4")).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack); + } + + [Test] + public void TestMissingFile() + { + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + + var issues = check.Run(getContext("Videos/missing.mp4", allowMissing: true)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile); + } + + private BeatmapVerifierContext getContext(string resourceName, bool allowMissing = false) + { + Stream resourceStream = string.IsNullOrEmpty(resourceName) ? null : TestResources.OpenResource(resourceName); + if (!allowMissing && resourceStream == null) + throw new FileNotFoundException($"The requested test resource \"{resourceName}\" does not exist."); + + var storyboard = new Storyboard(); + var layer = storyboard.GetLayer("Video"); + layer.Add(new StoryboardVideo("abc123.mp4", 0)); + + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 new file mode 100644 index 0000000000..f41c41d388 Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 new file mode 100644 index 0000000000..000bfc3567 Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4 new file mode 100644 index 0000000000..3d5e3a9953 Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 161e248d96..3d2e08f278 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -22,4 +22,7 @@ + + + diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 768ab3545f..12e8d2e36f 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Edit new CheckMutedObjects(), new CheckFewHitsounds(), new CheckTooShortAudioFiles(), + new CheckAudioInVideo(), // Files new CheckZeroByteFiles(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs new file mode 100644 index 0000000000..467adcdfb6 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckAudioInVideo : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateHasAudioTrack(this), + new IssueTemplateBadFormat(this), + new IssueTemplateMissingFile(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + foreach (var layer in context.WorkingBeatmap.Storyboard.Layers) + { + foreach (var element in layer.Elements) + { + if (!(element is StoryboardVideo video)) + continue; + + string filename = video.Path; + string storagePath = beatmapSet.GetPathForFile(filename); + + if (storagePath == null) + { + // There's an element in the storyboard that requires this resource, so it being missing is worth warning about. + yield return new IssueTemplateMissingFile(this).Create(filename); + + continue; + } + + Stream data = context.WorkingBeatmap.GetStream(storagePath); + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode, fileCallbacks.Callbacks, fileCallbacks.Handle); + if (decodeStream == 0) + continue; + + yield return new IssueTemplateHasAudioTrack(this).Create(filename); + } + } + } + + public class IssueTemplateHasAudioTrack : IssueTemplate + { + public IssueTemplateHasAudioTrack(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" has an audio track.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + + public class IssueTemplateBadFormat : IssueTemplate + { + public IssueTemplateBadFormat(ICheck check) + : base(check, IssueType.Error, "Could not check whether \"{0}\" has an audio track (code \"{1}\").") + { + } + + public Issue Create(string filename) => new Issue(this, filename, Bass.LastError); + } + + public class IssueTemplateMissingFile : IssueTemplate + { + public IssueTemplateMissingFile(ICheck check) + : base(check, IssueType.Warning, "Could not check whether \"{0}\" has an audio track, because it is missing.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + } +}