mpv-player / mpv

🎥 Command line video player
https://mpv.io
Other
28.54k stars 2.92k forks source link

libmpv event hooking in C# #3810

Closed DeadSix27 closed 5 years ago

DeadSix27 commented 7 years ago

(Sorry for the close-reopen, I accidentally hit Enter before I even began writing the issue and didn't want a unfinished issue to hover in the issue tracker)

I'm currently trying to hook into _mpv_wait_event from c#.

But that's a little over my head, especially since I've never worked much with Invokes from c++ dlls

That's as far as I got so far:

Most of the code is based on the chsharp example, I simply added the event structs of c++ But honestly, I barely have an idea how to convert C++ structs into c# code, so pretty much any help at all is very appreciated.

(If there is a better place to ask this, I'd like to know that as well.)

Crashes with System.AccessViolationException most likely because my attempt is probably completely wrong.

enum mpv_event_id
    {
        MPV_EVENT_NONE = 0,
        MPV_EVENT_SHUTDOWN = 1,
        MPV_EVENT_LOG_MESSAGE = 2,
        MPV_EVENT_GET_PROPERTY_REPLY = 3,
        MPV_EVENT_SET_PROPERTY_REPLY = 4,
        MPV_EVENT_COMMAND_REPLY = 5,
        MPV_EVENT_START_FILE = 6,
        MPV_EVENT_END_FILE = 7,
        MPV_EVENT_FILE_LOADED = 8,
        MPV_EVENT_TRACKS_CHANGED = 9,
        MPV_EVENT_TRACK_SWITCHED = 10,
        MPV_EVENT_IDLE = 11,
        MPV_EVENT_PAUSE = 12,
        MPV_EVENT_UNPAUSE = 13,
        MPV_EVENT_TICK = 14,
        MPV_EVENT_SCRIPT_INPUT_DISPATCH = 15,
        MPV_EVENT_CLIENT_MESSAGE = 16,
        MPV_EVENT_VIDEO_RECONFIG = 17,
        MPV_EVENT_AUDIO_RECONFIG = 18,
        MPV_EVENT_METADATA_UPDATE = 19,
        MPV_EVENT_SEEK = 20,
        MPV_EVENT_PLAYBACK_RESTART = 21,
        MPV_EVENT_PROPERTY_CHANGE = 22,
        MPV_EVENT_CHAPTER_CHANGE = 23,
        MPV_EVENT_QUEUE_OVERFLOW = 24
    }
    struct mpv_event
    {
        mpv_event_id event_id;
        int error;
        UInt64 reply_userdata;
        IntPtr data;
    }
....
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate mpv_event mpv_wait_event(IntPtr mpvHandle, double timeout);
private mpv_wait_event _mpv_wait_event;
....
_mpv_wait_event = (mpv_wait_event)GetDllType(typeof(mpv_wait_event), "mpv_wait_event");
rossy commented 7 years ago

The struct definition looks fine, though it should probably be marked with a StructLayoutAttribute like this:

[StructLayout(LayoutKind.Sequential)]
struct mpv_event
{
    mpv_event_id event_id;
    int error;
    UInt64 reply_userdata;
    IntPtr data;
}

I think the problem might be with the definition of mpv_wait_event. This returns a pointer to an mpv_event, rather than the mpv_event structure itself, so the type should be something like this:

private delegate IntPtr mpv_wait_event(IntPtr mpvHandle, double timeout);

Then the untyped pointer can be marshalled to a C# struct with Marshal.PtrToStructure:

IntPtr ptr = _mpv_wait_event(_mpvHandle, -1);
mpv_event evt = (mpv_event)Marshal.PtrToStructure(ptr, typeof(mpv_event));

At least, I think this is correct. I haven't tested it yet.

This can also be done with real typed pointers (C# totally supports raw pointers in an unsafe context,) but that's a different can of worms.

If there is a better place to ask this, I'd like to know that as well.

I'm not sure. It's hard to find info about C# P/Invoke on the internet, especially for marshalling complicated data structures. I'm happy to answer questions about C# libmpv bindings in this issue tracker.

DeadSix27 commented 7 years ago

EDIT: Offtopic Question: "Error parsing option script (option could not be parsed)" Any idea why this happends? I loaded a conf through libmpv in which i set (tried all of them): script="test.lua" | script="scripts/test.lua" | script="G:/...fullpath/scripts/test.lua" with quotes and without I also tried: script=scripts/test.lua,scripts/test2.lua


You made my day :)!

Worked with your examples and made it function just as I wanted:

loop... {
 IntPtr ptr = _mpv_wait_event(_mpvHandle, -1);
                mpv_event evt = (mpv_event)Marshal.PtrToStructure(ptr, typeof(mpv_event));

                Console.WriteLine(evt.event_id);
                switch (evt.event_id)
                {
                    case mpv_event_id.MPV_EVENT_LOG_MESSAGE:
                        IntPtr iD = evt.data;
                        mpv_event_log_message data = (mpv_event_log_message)Marshal.PtrToStructure(iD, typeof(mpv_event_log_message));
                        Console.WriteLine(data.text);
                        break;
                }
}

Output:

MPV_EVENT_LOG_MESSAGE
AO: [wasapi] 96000Hz stereo 2ch float
....
MPV_EVENT_LOG_MESSAGE
Using hardware decoding (dxva2-copy).

MPV_EVENT_VIDEO_RECONFIG

Renders fine too: image

EDIT: Also added this struct and enum since they were needed:

public enum mpv_log_level
    {
        MPV_LOG_LEVEL_NONE = 0,    /// "no"    - disable absolutely all messages
        MPV_LOG_LEVEL_FATAL = 10,   /// "fatal" - critical/aborting errors
        MPV_LOG_LEVEL_ERROR = 20,   /// "error" - simple errors
        MPV_LOG_LEVEL_WARN = 30,   /// "warn"  - possible problems
        MPV_LOG_LEVEL_INFO = 40,   /// "info"  - informational message
        MPV_LOG_LEVEL_V = 50,   /// "v"     - noisy informational message
        MPV_LOG_LEVEL_DEBUG = 60,   /// "debug" - very noisy technical information
        MPV_LOG_LEVEL_TRACE = 70,   /// "trace" - extremely noisy
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct mpv_event_log_message
    {
        public string prefix;
        public string level;
        public string text;
        public mpv_log_level log_level;
    }
rossy commented 7 years ago

Great. I'm glad it worked. I've been meaning to improve the libmpv C# example to do things like this, but I haven't gotten around to it yet.

As for the second question, I'm not sure what's wrong there. The script option with full Windows paths (eg. script="C:\full\path\script.lua") works on my machine. Do other options work in the .conf file when loaded through libmpv?

DeadSix27 commented 7 years ago

EDIT: Btw, do you know how to properly manage other formats than String in this: _mpvSetProperty(_mpvHandle, GetUtf8Bytes("pause"), mpv_format.MPV_FORMAT_STRING, ref bytes); This code works properly, however

var bin = BitConverter.GetBytes(doub); // (double doub = 0.5;)
_mpvSetProperty(_mpvHandle, GetUtf8Bytes("ao-volume"), mpv_format.MPV_FORMAT_DOUBLE, ref bin);

Sets the volume always to 0 in Windows Audio Mixer

Both use:

...
_mpvSetProperty = (MpvSetProperty)GetDllType(typeof(MpvSetProperty), "mpv_set_property");
...
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate int MpvSetProperty(IntPtr mpvHandle, byte[] name, mpv_format format, ref byte[] data);
        private MpvSetProperty _mpvSetProperty;

@rossy Do other options work in the .conf file when loaded through libmpv?

Just checked, other options work fine, best proof, is "input-ipc-server=mpvpipe" which is working as intended

However, even a proper script option like: script="C:\Users*\Documents..........\test\test.lua" just shows: Error parsing option script (option could not be parsed) Could "load-scripts=yes" be breaking it?

rossy commented 7 years ago

That BitConverter.GetBytes() method looks correct to me. I think the range of ao-volume is 1-100, so it's possible that 0.5 is rounded down to 0. Try setting it to 50.

DeadSix27 commented 7 years ago

@rossy Tried some options, see below:

//SetVolume(double doub)
var bin = BitConverter.GetBytes(doub);
_mpvSetProperty(_mpvHandle, GetUtf8Bytes("ao-volume"), mpv_format.MPV_FORMAT_DOUBLE, ref bin);

......

mpv.SetVolume(90);
Console.WriteLine(mpv.GetVolumeNative());
mpv.SetVolume(90.0);
Console.WriteLine(mpv.GetVolumeNative());
mpv.SetVolume(50.0);
Console.WriteLine(mpv.GetVolumeNative());
mpv.SetVolume(100);
Console.WriteLine(mpv.GetVolumeNative());
mpv.SetVolume(1.0);
Console.WriteLine(mpv.GetVolumeNative());

OUTPUT:
0
0
0
0
0

(Get(/Set)VolumeNative is a waveOutGetVolume-winmm.dll Import I use as work-around until I find out why the libmpv one doesn't work)

ghost commented 7 years ago

mpv doesn't control the system volume.

DeadSix27 commented 7 years ago

@wm4 Well, that just makes the debug up there wrong, however the audio is still 0 (I can't hear it if I set it to 100, so I assume it always sets it 0), or do you mean ao-volume simply will never work? And I have to use my own code for that?

In fact, if i do the ao-volume option it sets it to 0 INSIDE windows, so it does control the system volume... (does it not?)

By system volume I mean the slider specifically for the host of libmpv e.g my program in this case, NOT the master volume. (Master is not what I want to control anyway)

ghost commented 7 years ago

Oh right, ao-volume is only available if audio playback is initialized.

DeadSix27 commented 7 years ago

@wm4 Just loaded a .flac to test that, same issue, sets it to 0 regardless.

DeadSix27 commented 7 years ago

@rossy Been a while since I posted this (2016...) anyway I came back to this recently and got stuck at getting properties of libmpv with specific formats, e.g:

//name = "duration"
var buffer = IntPtr.Zero;
_mpvGetProperty(_mpvHandle, GetUtf8Bytes(name), mpv_format.MPV_FORMAT_DOUBLE, ref buffer);
double outputDouble = (double)Marshal.PtrToStructure(buffer, typeof(double));
_mpvFree(buffer);
return outputDouble;

which crashes with access violation, if I do the same thing with MPV_FORMAT_STRING, which according to client.h: "and access using MPV_FORMAT_STRING usually invokes a string formatter." invokes a string formatter, and it works fine. Is duration not a double? Afaik it should be? or is my pointer to structure incorrect, altho the same PtrToStructure code works fine in MPV_EVENT_PROPERTY_CHANGE events.

Using double as type in the unmanaged function pointer also resolves this, however that would require a function for each type..

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int mpv_get_property(IntPtr mpvHandle, byte[] name, mpv_format format, ref double data);
private mpv_get_property _mpv_get_property;
rossy commented 7 years ago

I think your code might be using the wrong level of indirection. It's using two levels of pointer indirection: It passes a reference to the IntPtr (in C, this would be a double** argument.) libmpv treats the argument as a pointer to double (in C: double*, in C#: ref double) so it will write the double value directly to storage pointed to by the argument.

It's different for MPV_FORMAT_STRING because strings can be variable length, so libmpv allocates storage for them on the heap. With MPV_FORMAT_DOUBLE, the storage is always 8-bytes long, so libmpv writes the double value directly to caller-provided storage on the stack (this also means the _mpvFree() call isn't needed for MPV_FORMAT_DOUBLE.)

Using double as type in the unmanaged function pointer also resolves this, however that would require a function for each type..

IMO this isn't too bad. It's probably the easiest solution. Another solution is to always use MPV_FORMAT_NODE and do the type-conversion on the C# side.

DeadSix27 commented 7 years ago

@rossy I'll most likely use the separate function way ye.. considering its afaik only a few main types anyway.

I still don't understand the issue with double too:

libmpv writes the double value directly to caller-provided storage on the stack

so instead of referencing a pointer i should reference a type, but I can not reference an unknown type (e.g Object) afaik (I tried) (Passing invalid VARIANTs to the CLR can cause unexpected exceptions, corruption or data loss.')

So how would that work?

Also just as reference I gave my best at trying to convert the NODE structs to C# too. But I can't get them to work.. Also saw someone else needs help on NODE_ARRAY, so this could help him a little too: https://github.com/mpv-player/mpv/issues/4380

[StructLayout(LayoutKind.Explicit)]
struct mpv_node
{
    [FieldOffset(4)] public string string_;   /* valid if format==MPV_FORMAT_STRING */
    [FieldOffset(4)] public int flag;       /* valid if format==MPV_FORMAT_FLAG   */
    [FieldOffset(4)] public Int64 int64;  /* valid if format==MPV_FORMAT_INT64  */
    [FieldOffset(4)] public double double_; /* valid if format==MPV_FORMAT_DOUBLE */
    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.Struct)]
    public mpv_node_list list; /* format==MPV_FORMAT_NODE_MAP,format==MPV_FORMAT_NODE_ARRAY */
    [FieldOffset(4)]
    [MarshalAs(UnmanagedType.Struct)]
    public mpv_byte_array ba; /* format==MPV_FORMAT_NODE_MAP,format==MPV_FORMAT_NODE_ARRAY */
    [FieldOffset(0)] public mpv_format format;
}
[StructLayout(LayoutKind.Sequential)]
struct mpv_byte_array
{
    public IntPtr data;
    public UIntPtr size;
}
[StructLayout(LayoutKind.Sequential)]
struct mpv_node_list
{
    public int num;
    public IntPtr values;
    public string keys;
}

IntPtr testptr = IntPtr.Zero;
MpvGetPropertyNode(mpvHandle, GetUtf8Bytes("chapter-list"), mpv_format.MPV_FORMAT_NODE, ref testptr);
mpv_node testlist = (mpv_node)Marshal.PtrToStructure(ptr, typeof(mpv_node));