2022-05-14 11:59:54 +00:00
|
|
|
private static extern unowned Gst.Video.Info gst_video_frame_get_video_info(Gst.Video.Frame frame);
|
2023-03-20 21:01:23 +00:00
|
|
|
[CCode (array_length_type = "size_t", type = "void*")]
|
2022-05-14 11:59:54 +00:00
|
|
|
private static extern unowned uint8[] gst_video_frame_get_data(Gst.Video.Frame frame);
|
|
|
|
|
|
|
|
public class Dino.Plugins.Rtp.Paintable : Gdk.Paintable, Object {
|
|
|
|
private Gdk.Paintable image;
|
|
|
|
private double pixel_aspect_ratio;
|
|
|
|
|
|
|
|
public override Gdk.PaintableFlags get_flags() {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void snapshot(Gdk.Snapshot snapshot, double width, double height) {
|
|
|
|
if (image != null) image.snapshot(snapshot, width, height);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override Gdk.Paintable get_current_image() {
|
|
|
|
if (image != null) return image;
|
2023-10-07 12:29:26 +00:00
|
|
|
return Gdk.Paintable.empty(0, 0);
|
2022-05-14 11:59:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public override int get_intrinsic_width() {
|
|
|
|
if (image != null) return (int) (pixel_aspect_ratio * image.get_intrinsic_width());
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override int get_intrinsic_height() {
|
|
|
|
if (image != null) return (int) (pixel_aspect_ratio * image.get_intrinsic_height());
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override double get_intrinsic_aspect_ratio() {
|
|
|
|
if (image != null) return pixel_aspect_ratio * image.get_intrinsic_aspect_ratio();
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void dispose() {
|
|
|
|
image = null;
|
|
|
|
base.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void set_paintable(Gdk.Paintable paintable, double pixel_aspect_ratio) {
|
|
|
|
if (paintable == image) return;
|
|
|
|
bool size_changed = image == null ||
|
|
|
|
this.pixel_aspect_ratio * image.get_intrinsic_width() != pixel_aspect_ratio * paintable.get_intrinsic_width() ||
|
|
|
|
image.get_intrinsic_height() != paintable.get_intrinsic_height() ||
|
|
|
|
image.get_intrinsic_aspect_ratio() != paintable.get_intrinsic_aspect_ratio();
|
|
|
|
|
2022-05-14 12:45:59 +00:00
|
|
|
if (image != null) this.image.dispose();
|
2022-05-14 11:59:54 +00:00
|
|
|
this.image = paintable;
|
|
|
|
this.pixel_aspect_ratio = pixel_aspect_ratio;
|
|
|
|
|
|
|
|
if (size_changed) invalidate_size();
|
|
|
|
invalidate_contents();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void queue_set_texture(Gdk.Texture texture, double pixel_aspect_ratio) {
|
|
|
|
Idle.add(() => {
|
|
|
|
set_paintable(texture, pixel_aspect_ratio);
|
|
|
|
return Source.REMOVE;
|
|
|
|
}, Priority.DEFAULT);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class Dino.Plugins.Rtp.Sink : Gst.Video.Sink {
|
|
|
|
internal Paintable paintable = new Paintable();
|
|
|
|
private Gst.Video.Info info = new Gst.Video.Info();
|
|
|
|
|
|
|
|
class construct {
|
|
|
|
set_metadata("Dino Gtk Video Sink", "Sink/Video", "The video sink used by Dino", "Dino Team <team@dino.im>");
|
|
|
|
add_pad_template(new Gst.PadTemplate("sink", Gst.PadDirection.SINK, Gst.PadPresence.ALWAYS, Gst.Caps.from_string(@"video/x-raw, format={ BGRA, ARGB, RGBA, ABGR, RGB, BGR }")));
|
|
|
|
}
|
|
|
|
|
|
|
|
construct {
|
|
|
|
set_drop_out_of_segment(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
#if GST_1_20
|
|
|
|
public override bool set_info(Gst.Caps caps, Gst.Video.Info info) {
|
|
|
|
this.info = info;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
public override bool set_caps(Gst.Caps caps) {
|
|
|
|
base.set_caps(caps);
|
|
|
|
return info.from_caps(caps);
|
|
|
|
}
|
2021-12-23 05:39:18 +00:00
|
|
|
#endif
|
|
|
|
|
2022-05-14 11:59:54 +00:00
|
|
|
public override void get_times(Gst.Buffer buffer, out Gst.ClockTime start, out Gst.ClockTime end) {
|
|
|
|
if (buffer.pts != -1) {
|
|
|
|
start = buffer.pts;
|
|
|
|
if (buffer.duration != -1) {
|
|
|
|
end = start + buffer.duration;
|
|
|
|
} else if (info.fps_n > 0) {
|
|
|
|
end = start + Gst.Util.uint64_scale_int(Gst.SECOND, info.fps_d, info.fps_n);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override Gst.Caps get_caps(Gst.Caps? filter) {
|
|
|
|
Gst.Caps caps = Gst.Caps.from_string("video/x-raw, format={ BGRA, ARGB, RGBA, ABGR, RGB, BGR }");
|
|
|
|
|
|
|
|
if (filter != null) {
|
|
|
|
return filter.intersect(caps, Gst.CapsIntersectMode.FIRST);
|
|
|
|
} else {
|
|
|
|
return caps;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private Gdk.MemoryFormat memory_format_from_video(Gst.Video.Format format) {
|
|
|
|
switch (format) {
|
|
|
|
case Gst.Video.Format.BGRA: return Gdk.MemoryFormat.B8G8R8A8;
|
|
|
|
case Gst.Video.Format.ARGB: return Gdk.MemoryFormat.A8R8G8B8;
|
|
|
|
case Gst.Video.Format.RGBA: return Gdk.MemoryFormat.R8G8B8A8;
|
|
|
|
case Gst.Video.Format.ABGR: return Gdk.MemoryFormat.A8B8G8R8;
|
|
|
|
case Gst.Video.Format.RGB: return Gdk.MemoryFormat.R8G8B8;
|
|
|
|
case Gst.Video.Format.BGR: return Gdk.MemoryFormat.B8G8R8;
|
|
|
|
default:
|
|
|
|
warning("Unsupported video format: %s", format.to_string());
|
|
|
|
return Gdk.MemoryFormat.A8R8G8B8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private Gdk.Texture texture_from_buffer(Gst.Buffer buffer, out double pixel_aspect_ratio) {
|
2022-08-21 18:02:48 +00:00
|
|
|
Gst.Video.Frame frame = Gst.Video.Frame();
|
2022-05-14 11:59:54 +00:00
|
|
|
Gdk.Texture texture;
|
|
|
|
|
2022-08-21 18:02:48 +00:00
|
|
|
if (frame.map(info, buffer, Gst.MapFlags.READ)) {
|
2022-05-14 11:59:54 +00:00
|
|
|
unowned Gst.Video.Info info = gst_video_frame_get_video_info(frame);
|
|
|
|
Bytes bytes = new Bytes.take(gst_video_frame_get_data(frame));
|
|
|
|
texture = new Gdk.MemoryTexture(info.width, info.height, memory_format_from_video(info.finfo.format), bytes, info.stride[0]);
|
|
|
|
pixel_aspect_ratio = ((double) info.par_n) / ((double) info.par_d);
|
|
|
|
frame.unmap();
|
|
|
|
} else {
|
|
|
|
texture = null;
|
|
|
|
}
|
|
|
|
return texture;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void queue_buffer(Gst.Buffer buf) {
|
|
|
|
double pixel_aspect_ratio;
|
|
|
|
Gdk.Texture texture = texture_from_buffer(buf, out pixel_aspect_ratio);
|
|
|
|
if (texture != null) {
|
|
|
|
paintable.queue_set_texture(texture, pixel_aspect_ratio);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override Gst.FlowReturn show_frame(Gst.Buffer buf) {
|
|
|
|
@lock.lock();
|
|
|
|
queue_buffer(buf);
|
|
|
|
@lock.unlock();
|
|
|
|
|
|
|
|
return Gst.FlowReturn.OK;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-14 13:55:59 +00:00
|
|
|
public class Dino.Plugins.Rtp.VideoWidget : Gtk.Widget, Dino.Plugins.VideoCallWidget {
|
2022-01-28 10:01:07 +00:00
|
|
|
private const int RECAPS_AFTER_CHANGE = 5;
|
2021-03-21 11:41:38 +00:00
|
|
|
private static uint last_id = 0;
|
|
|
|
|
|
|
|
public uint id { get; private set; }
|
|
|
|
public Plugin plugin { get; private set; }
|
|
|
|
public Gst.Pipeline pipe { get {
|
|
|
|
return plugin.pipe;
|
|
|
|
}}
|
|
|
|
|
|
|
|
private bool attached;
|
|
|
|
private Device? connected_device;
|
2021-11-09 21:06:45 +00:00
|
|
|
private Gst.Element? connected_device_element;
|
2021-03-21 11:41:38 +00:00
|
|
|
private Stream? connected_stream;
|
2021-11-09 21:06:45 +00:00
|
|
|
private Gst.Element prepare;
|
2022-01-27 21:08:35 +00:00
|
|
|
private Gst.Caps last_input_caps;
|
|
|
|
private Gst.Caps last_caps;
|
2022-01-28 10:01:07 +00:00
|
|
|
private int recaps_since_change;
|
2022-05-14 11:59:54 +00:00
|
|
|
private Sink sink;
|
|
|
|
private Gtk.Picture widget;
|
2021-03-21 11:41:38 +00:00
|
|
|
|
|
|
|
public VideoWidget(Plugin plugin) {
|
|
|
|
this.plugin = plugin;
|
2022-05-14 11:59:54 +00:00
|
|
|
this.layout_manager = new Gtk.BinLayout();
|
2021-03-21 11:41:38 +00:00
|
|
|
|
|
|
|
id = last_id++;
|
2022-05-14 11:59:54 +00:00
|
|
|
sink = new Sink() { async = false, sync = true };
|
|
|
|
widget = new Gtk.Picture.for_paintable(sink.paintable);
|
|
|
|
widget.insert_after(this, null);
|
2021-11-09 21:06:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void input_caps_changed(GLib.Object pad, ParamSpec spec) {
|
2022-01-23 18:00:05 +00:00
|
|
|
Gst.Caps? caps = ((Gst.Pad)pad).caps;
|
2022-01-27 21:08:35 +00:00
|
|
|
if (caps == null) {
|
2022-01-28 10:01:07 +00:00
|
|
|
debug("Input: No caps");
|
2022-01-27 21:08:35 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-11-09 21:06:45 +00:00
|
|
|
|
|
|
|
int width, height;
|
|
|
|
caps.get_structure(0).get_int("width", out width);
|
|
|
|
caps.get_structure(0).get_int("height", out height);
|
2022-01-27 21:08:35 +00:00
|
|
|
debug("Input resolution changed: %ix%i", width, height);
|
2023-07-09 13:32:53 +00:00
|
|
|
// Invoke signal on GTK main loop as recipients are likely to use it for doing GTK operations
|
|
|
|
Idle.add(() => {
|
|
|
|
resolution_changed(width, height);
|
|
|
|
return Source.REMOVE;
|
|
|
|
});
|
2022-01-27 21:08:35 +00:00
|
|
|
last_input_caps = caps;
|
|
|
|
}
|
|
|
|
|
2022-02-12 13:35:44 +00:00
|
|
|
public void display_stream(Xmpp.Xep.JingleRtp.Stream? stream, Xmpp.Jid jid) {
|
2022-01-27 21:08:35 +00:00
|
|
|
if (sink == null) return;
|
2021-03-21 11:41:38 +00:00
|
|
|
detach();
|
|
|
|
if (stream.media != "video") return;
|
2022-02-12 13:35:44 +00:00
|
|
|
connected_stream = stream as Stream?;
|
2021-03-21 11:41:38 +00:00
|
|
|
if (connected_stream == null) return;
|
|
|
|
plugin.pause();
|
2022-01-27 21:08:35 +00:00
|
|
|
pipe.add(sink);
|
2022-05-14 11:59:54 +00:00
|
|
|
prepare = Gst.parse_bin_from_description(@"videoconvert name=video_widget_$(id)_convert", true);
|
2021-11-09 21:06:45 +00:00
|
|
|
prepare.name = @"video_widget_$(id)_prepare";
|
|
|
|
prepare.get_static_pad("sink").notify["caps"].connect(input_caps_changed);
|
|
|
|
pipe.add(prepare);
|
|
|
|
connected_stream.add_output(prepare);
|
2022-01-27 21:08:35 +00:00
|
|
|
prepare.link(sink);
|
|
|
|
sink.set_locked_state(false);
|
2021-03-21 11:41:38 +00:00
|
|
|
plugin.unpause();
|
|
|
|
attached = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void display_device(MediaDevice media_device) {
|
2022-01-27 21:08:35 +00:00
|
|
|
if (sink == null) return;
|
2021-03-21 11:41:38 +00:00
|
|
|
detach();
|
|
|
|
connected_device = media_device as Device;
|
|
|
|
if (connected_device == null) return;
|
|
|
|
plugin.pause();
|
2022-01-27 21:08:35 +00:00
|
|
|
pipe.add(sink);
|
2023-04-21 15:38:15 +00:00
|
|
|
#if GST_1_20
|
|
|
|
prepare = Gst.parse_bin_from_description(@"videoflip video-direction=auto name=video_widget_$(id)_orientation ! videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true);
|
|
|
|
#else
|
2022-05-14 11:59:54 +00:00
|
|
|
prepare = Gst.parse_bin_from_description(@"videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true);
|
2023-04-21 15:38:15 +00:00
|
|
|
#endif
|
2021-11-09 21:06:45 +00:00
|
|
|
prepare.name = @"video_widget_$(id)_prepare";
|
2023-04-21 15:38:15 +00:00
|
|
|
#if GST_1_20
|
|
|
|
if (prepare is Gst.Bin) {
|
|
|
|
((Gst.Bin) prepare).get_by_name(@"video_widget_$(id)_flip").get_static_pad("sink").notify["caps"].connect(input_caps_changed);
|
|
|
|
} else {
|
|
|
|
#endif
|
|
|
|
prepare.get_static_pad("sink").notify["caps"].connect(input_caps_changed);
|
|
|
|
#if GST_1_20
|
|
|
|
}
|
|
|
|
#endif
|
2021-11-09 21:06:45 +00:00
|
|
|
pipe.add(prepare);
|
|
|
|
connected_device_element = connected_device.link_source();
|
|
|
|
connected_device_element.link(prepare);
|
2022-01-27 21:08:35 +00:00
|
|
|
prepare.link(sink);
|
|
|
|
sink.set_locked_state(false);
|
2021-03-21 11:41:38 +00:00
|
|
|
plugin.unpause();
|
|
|
|
attached = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void detach() {
|
2022-01-27 21:08:35 +00:00
|
|
|
if (sink == null) return;
|
2021-03-21 11:41:38 +00:00
|
|
|
if (attached) {
|
2022-05-14 11:59:54 +00:00
|
|
|
debug("Detaching");
|
2021-03-21 11:41:38 +00:00
|
|
|
if (connected_stream != null) {
|
2021-11-09 21:06:45 +00:00
|
|
|
connected_stream.remove_output(prepare);
|
2021-03-21 11:41:38 +00:00
|
|
|
connected_stream = null;
|
|
|
|
}
|
|
|
|
if (connected_device != null) {
|
2022-01-27 21:08:35 +00:00
|
|
|
connected_device_element.unlink(sink);
|
2021-11-09 21:06:45 +00:00
|
|
|
connected_device_element = null;
|
2021-03-21 11:41:38 +00:00
|
|
|
connected_device.unlink();
|
|
|
|
connected_device = null;
|
|
|
|
}
|
2021-11-09 21:06:45 +00:00
|
|
|
prepare.set_locked_state(true);
|
|
|
|
prepare.set_state(Gst.State.NULL);
|
|
|
|
pipe.remove(prepare);
|
|
|
|
prepare = null;
|
2022-01-27 21:08:35 +00:00
|
|
|
sink.set_locked_state(true);
|
|
|
|
sink.set_state(Gst.State.NULL);
|
|
|
|
pipe.remove(sink);
|
2021-03-21 11:41:38 +00:00
|
|
|
attached = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void dispose() {
|
|
|
|
detach();
|
2022-05-14 11:59:54 +00:00
|
|
|
if (widget != null) widget.unparent();
|
2021-03-21 11:41:38 +00:00
|
|
|
widget = null;
|
2022-01-27 21:08:35 +00:00
|
|
|
sink = null;
|
2021-03-21 11:41:38 +00:00
|
|
|
}
|
2022-01-27 21:08:35 +00:00
|
|
|
}
|