Implement movie surface stop-time queries
This commit is contained in:
@@ -35,6 +35,9 @@ internal sealed class DirectShowMovieDecoder : IDisposable, ISampleGrabberCB
|
||||
private IMediaControl? _control;
|
||||
|
||||
public bool IsCompleted => _completed;
|
||||
/// <summary>The graph's IMediaPosition stop time converted exactly as native op 0x23f does:
|
||||
/// seconds * 1000, truncated toward zero. Null means DirectShow supplied no usable value.</summary>
|
||||
public long? StopTimeMs { get; private set; }
|
||||
|
||||
public DirectShowMovieDecoder(MoviePayload movie)
|
||||
{
|
||||
@@ -112,6 +115,13 @@ internal sealed class DirectShowMovieDecoder : IDisposable, ISampleGrabberCB
|
||||
int stateHr = _control.GetState(5000, out int graphState);
|
||||
if (stateHr < 0) Check(stateHr, "wait for running movie graph");
|
||||
if (graphState != 2) throw new InvalidOperationException($"movie graph entered unexpected state {graphState}");
|
||||
var mediaPosition = (IMediaPosition)_graphObject;
|
||||
int stopHr = mediaPosition.get_StopTime(out double stopTimeSeconds);
|
||||
double stopTimeMilliseconds = stopTimeSeconds * 1000.0;
|
||||
if (stopHr >= 0 && double.IsFinite(stopTimeMilliseconds)
|
||||
&& stopTimeMilliseconds >= int.MinValue
|
||||
&& stopTimeMilliseconds <= int.MaxValue)
|
||||
StopTimeMs = (long)System.Math.Truncate(stopTimeMilliseconds);
|
||||
_ready.Set();
|
||||
var mediaEvent = (IMediaEvent)_graphObject;
|
||||
while (!_stopping)
|
||||
@@ -265,6 +275,21 @@ internal sealed class DirectShowMovieDecoder : IDisposable, ISampleGrabberCB
|
||||
[ComImport, Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
|
||||
private interface IMediaControl
|
||||
{ [PreserveSig] int Run(); [PreserveSig] int Pause(); [PreserveSig] int Stop(); [PreserveSig] int GetState(int timeout, out int state); }
|
||||
[ComImport, Guid("56A868B2-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
|
||||
private interface IMediaPosition
|
||||
{
|
||||
[PreserveSig] int get_Duration(out double seconds);
|
||||
[PreserveSig] int put_CurrentPosition(double seconds);
|
||||
[PreserveSig] int get_CurrentPosition(out double seconds);
|
||||
[PreserveSig] int get_StopTime(out double seconds);
|
||||
[PreserveSig] int put_StopTime(double seconds);
|
||||
[PreserveSig] int get_PrerollTime(out double seconds);
|
||||
[PreserveSig] int put_PrerollTime(double seconds);
|
||||
[PreserveSig] int put_Rate(double rate);
|
||||
[PreserveSig] int get_Rate(out double rate);
|
||||
[PreserveSig] int CanSeekForward(out int canSeekForward);
|
||||
[PreserveSig] int CanSeekBackward(out int canSeekBackward);
|
||||
}
|
||||
[ComImport, Guid("56A868B6-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
|
||||
private interface IMediaEvent
|
||||
{
|
||||
|
||||
@@ -74,6 +74,8 @@ public sealed class GodotAdvHost : IHost
|
||||
_locator = locator; _timeline = timeline;
|
||||
}
|
||||
|
||||
public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
|
||||
|
||||
private string CurrentScene
|
||||
{
|
||||
get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; }
|
||||
@@ -767,12 +769,13 @@ public sealed class GodotAdvHost : IHost
|
||||
return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null;
|
||||
}
|
||||
|
||||
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||
public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||
{
|
||||
string scene = CurrentScene;
|
||||
var asset = _res.Resolve(scene, resourceId);
|
||||
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return; }
|
||||
StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false);
|
||||
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return null; }
|
||||
return StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false,
|
||||
out long? stopTimeMs) ? stopTimeMs : null;
|
||||
}
|
||||
|
||||
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
|
||||
@@ -788,7 +791,8 @@ public sealed class GodotAdvHost : IHost
|
||||
_modalMovieWaiting = true;
|
||||
try
|
||||
{
|
||||
if (!StartMovie(asset, rawResourceId, surfaceSlot, movieFlags, 0, modal: true)) return;
|
||||
if (!StartMovie(asset, rawResourceId, surfaceSlot, movieFlags, 0, modal: true,
|
||||
out _)) return;
|
||||
_timeline?.State("modal-movie-wait", new()
|
||||
{
|
||||
["resource"] = rawResourceId, ["surface"] = surfaceSlot, ["file"] = asset.Name,
|
||||
@@ -818,8 +822,9 @@ public sealed class GodotAdvHost : IHost
|
||||
}
|
||||
|
||||
private bool StartMovie(AssetEntry asset, long resourceId, int surfaceSlot, long movieFlags,
|
||||
long syncMask, bool modal)
|
||||
long syncMask, bool modal, out long? stopTimeMs)
|
||||
{
|
||||
stopTimeMs = null;
|
||||
try
|
||||
{
|
||||
var movie = _res.ReadMovie(asset);
|
||||
@@ -835,8 +840,7 @@ public sealed class GodotAdvHost : IHost
|
||||
["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name,
|
||||
["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal,
|
||||
});
|
||||
_main.CallDeferred("PlayMovie", movie.Bytes, movie.Name, resourceId, asset.RawIndex);
|
||||
return true;
|
||||
return _main.TryPlayMovie(movie.Bytes, movie.Name, resourceId, asset.RawIndex, out stopTimeMs);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
|
||||
@@ -52,6 +52,9 @@ public partial class Main : Godot.Control
|
||||
private IReadOnlyList<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
|
||||
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
||||
private readonly System.Collections.Generic.Dictionary<long, MovieRuntime> _movies = new();
|
||||
// 0x236 creates its graph synchronously on the VM thread so 0x23f can query timing immediately.
|
||||
// Presentation ownership transfers here; _Process adopts staged decoders before sampling frames.
|
||||
private readonly System.Collections.Concurrent.ConcurrentDictionary<long, MovieRuntime> _pendingMovies = new();
|
||||
private readonly System.Collections.Generic.HashSet<long> _movieFrameSeen = new();
|
||||
private GodotTraceSink _trace = null!;
|
||||
private PageLocatorState _locator = null!;
|
||||
@@ -318,6 +321,7 @@ public partial class Main : Godot.Control
|
||||
_timeline?.SetFrame(_timelineFrame, _clock.NowMs);
|
||||
_host?.PulseFrame();
|
||||
UpdateVoicePlaybackState();
|
||||
AdoptPendingMovies();
|
||||
UpdateMovieFrames();
|
||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
@@ -595,6 +599,8 @@ public partial class Main : Godot.Control
|
||||
public override void _ExitTree()
|
||||
{
|
||||
DumpHistogram(); _host?.Stop(); _timeline?.Dispose(); _locator?.Dispose();
|
||||
foreach (var movie in _pendingMovies.Values) movie.Decoder.Dispose();
|
||||
_pendingMovies.Clear();
|
||||
foreach (var movie in _movies.Values) movie.Decoder.Dispose();
|
||||
_movies.Clear();
|
||||
}
|
||||
@@ -1162,19 +1168,35 @@ public partial class Main : Godot.Control
|
||||
CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
|
||||
}
|
||||
|
||||
public void PlayMovie(byte[] mpegBytes, string assetName, long resourceId, int rawIndex)
|
||||
public bool TryPlayMovie(byte[] mpegBytes, string assetName, long resourceId, int rawIndex,
|
||||
out long? stopTimeMs)
|
||||
{
|
||||
if (_movies.Remove(resourceId, out var prior)) prior.Decoder.Dispose();
|
||||
stopTimeMs = null;
|
||||
try
|
||||
{
|
||||
var payload = new Age.Engine.Sys4.MoviePayload(assetName, mpegBytes);
|
||||
_movies[resourceId] = new MovieRuntime(assetName, rawIndex, new DirectShowMovieDecoder(payload));
|
||||
GD.Print($"movie started {assetName} ({mpegBytes.Length} bytes from VFS)");
|
||||
var runtime = new MovieRuntime(assetName, rawIndex, new DirectShowMovieDecoder(payload));
|
||||
stopTimeMs = runtime.Decoder.StopTimeMs;
|
||||
while (!_pendingMovies.TryAdd(resourceId, runtime))
|
||||
if (_pendingMovies.TryRemove(resourceId, out var prior)) prior.Decoder.Dispose();
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
GD.Print($"movie decode failed {assetName}: {e.Message}");
|
||||
_host.NotifyMovieCompleted(resourceId); // release a pending 0x21c boundary on deterministic load failure
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AdoptPendingMovies()
|
||||
{
|
||||
foreach (var (resourceId, _) in _pendingMovies)
|
||||
{
|
||||
if (!_pendingMovies.TryRemove(resourceId, out var movie)) continue;
|
||||
if (_movies.Remove(resourceId, out var prior)) prior.Decoder.Dispose();
|
||||
_movies[resourceId] = movie;
|
||||
GD.Print($"movie started {movie.Name} ({movie.Decoder.StopTimeMs?.ToString() ?? "unknown"} ms from VFS)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1195,6 +1217,7 @@ public partial class Main : Godot.Control
|
||||
|
||||
public void StopMovie(long resourceId)
|
||||
{
|
||||
if (_pendingMovies.TryRemove(resourceId, out var pending)) pending.Decoder.Dispose();
|
||||
if (_movies.Remove(resourceId, out var movie))
|
||||
{
|
||||
movie.Decoder.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user