GStreamer-Sharp signal with string array return value

Using GStreamer 1.24.2.

Some back story… I’ve been recording from an rtsp stream using splitmuxsink. I was trying to concatenate those videos using the concat element. Seems to work fine for generating a new concatenated file, but if I’m trying to use it for viewing with autovideosink or streaming with webrtcbin it seems to get stuck after the first few files. I’ve filed a bug report here, but have no response.

I eventually found there is a splitmuxsrc. Unsure why that’s needed when there is a concat element. Nevertheless, testing splitmuxsrc from the command line resulted in success with autovideosink. So, since I wasn’t getting a response on my bug report, I figured I’d go in this direction.

Next step was to test it in my application with webrtcbin. Unfortunately I’m stuck on how to provide the input files. There is a format-location signal which is exactly what I want. Here is my signal handler:

        private void HandleFormatLocation(
            object o,
            SignalArgs args)
        {
            if (!RecordingFiles.Any())
            {
                return;
            }

            var files = RecordingFiles.OrderBy(p => p.StartDate)
                .ToList()
                .Select(p => p.Filename)
                .Select(p => p.Replace("\\", "\\\\"))
                .ToArray();

            Logger.LogWarning("Getting files: {files}", files.JoinString(", "));

            var value = new GLib.Value(files);
            args.RetVal = value;
        }

I’ve tried various things, but I think this is the correct code. Unfortunately, something seems to be messing it up. Here is the output:

[15:21:08 WRN] Getting files: D:/VideoRecording/Ubiquiti/video0000041.mp4, D:/VideoRecording/Ubiquiti/video0000042.mp4, D:/VideoRecording/Ubiquiti/video0000043.mp4, D:/VideoRecording/Ubiquiti/video0000044.mp4, D:/VideoRecording/Ubiquiti/video0000045.mp4, D:/VideoRecording/Ubiquiti/video0000046.mp4, D:/VideoRecording/Ubiquiti/video0000047.mp4, D:/VideoRecording/Ubiquiti/video0000048.mp4, D:/VideoRecording/Ubiquiti/video0000049.mp4, D:/VideoRecording/Ubiquiti/video0000050.mp4, D:/VideoRecording/Ubiquiti/video0000051.mp4, D:/VideoRecording/Ubiquiti/video0000052.mp4, D:/VideoRecording/Ubiquiti/video0000053.mp4, D:/VideoRecording/Ubiquiti/video0000054.mp4
[15:21:08 ERR] MESSAGE: Pipeline error for view recording files:
Could not open resource for reading.
../gst/multifile/gstsplitmuxsrc.c(506): gst_splitmux_part_bus_handler (): /GstPipeline:pipeline0/GstSplitMuxSrc:concat:
Failed to prepare first file part ☺ for playback

Looking at the code in gstsplitmuxsrc.c, it appears the smiley face in the last line of output is supposed to be a filename. Most likely it should say D:/VideoRecording/Ubiquiti/video0000041.mp4.

I’ve tried a few different ways of returning a string array and nothing works. I run into things like marshalling errors, or unknown type gstrv. All the GStreamer-Sharp examples I could find just set boolean values to RetVal.

I asked in the gstreamer general discussion of element.io, but also got no help. Hoping someone here has run into this problem and has a workaround?

Any chance anyone has tried to do this before?

I tried to emulate what DynamicSignal.cs does in the OnMarshal method…

var val = new[] { "D:/VideoRecording/Ubiquiti 1/video0000000.mp4" };

var glibVal = new GLib.Value(val);
foreach (var s in (string[])glibVal)
{
    Logger.LogError(s);
}


var temp = new GLib.Value(new GType(g_strv_get_type()));
temp.Val = glibVal;

foreach (var s in (string[])temp)
{
    Logger.LogError(s);
}

args.RetVal = glibVal;

With this, I was able to reproduce what GStreamer is seeing. Here is the output:

[15:36:32 ERR] D:/VideoRecording/Ubiquiti 1/video0000000.mp4
[15:36:32 ERR] ☺
[15:36:32 ERR] ?=?☻?☻

So, not only was there a smiley face there, but a bunch of other odd characters. Still not really sure whether the problem is GStreamer or the GLib library it uses. I would imagine a special case could be done for string in DynamicSignal.cs.

Found a workaround for now. Not super happy with it, but it works. Basically make a copy of DynamicSignal.cs and change the OnMarshal method. Then instead of using the Connect method on the element, use the Connect method on your version of DynamicSignal.cs

static void OnMarshal(
    IntPtr closure,
    ref Value retval,
    uint argc,
    IntPtr argsPtr,
    IntPtr ihint,
    IntPtr data)
{
    var args = new object[argc - 1];
    var o = ((Value)Marshal.PtrToStructure(argsPtr, typeof(Value))).Val;

    for (var i = 1; i < argc; i++)
    {
        var struct_ptr = (IntPtr)((long)argsPtr + (i * gvalue_struct_size));
        var argument = (Value)Marshal.PtrToStructure(struct_ptr, typeof(Value));
        args[i - 1] = argument.Val;
    }

    if (data == IntPtr.Zero)
    {
        Console.Error.WriteLine("No available data");
        return;
    }

    var k = (ObjectSignalKey)((GCHandle)data).Target;
    if (k == null)
    {
        return;
    }

    var si = (SignalInfo)SignalHandlers[k];
    var arg = (SignalArgs)Activator.CreateInstance(si.ArgsType);
    arg.Args = args;
    si.RegisteredHandler.DynamicInvoke(o, arg);
    switch (arg.RetVal)
    {
        case null:
            return;
        case string[] stringArray:
        {
            var nativeArray = Marshal.AllocHGlobal((stringArray.Length + 1) * IntPtr.Size);
            for (var i = 0; i < stringArray.Length; i++)
            {
                Marshal.WriteIntPtr(nativeArray, i * IntPtr.Size,
                    Marshaller.StringToPtrGStrdup(stringArray[i]));
            }

            Marshal.WriteIntPtr(nativeArray, stringArray.Length * IntPtr.Size, IntPtr.Zero);

            g_value_set_boxed(ref retval, nativeArray);

            for (var i = 0; i < stringArray.Length; i++)
            {
                Marshaller.Free(Marshal.ReadIntPtr(nativeArray, i * IntPtr.Size));
            }

            Marshal.FreeHGlobal(nativeArray);
            break;
        }
        default:
            retval.Val = arg.RetVal;
            break;
    }
}