Add a GStreamer element that provides a video sink that draws directly on QWidget.
[qtgstreamer:qtgstreamer.git] / elements / gstqwidgetvideosink.cpp
1 /*
2     Copyright (C) 2010 George Kiagiadakis <kiagiadakis.george@gmail.com>
3
4     This library is free software; you can redistribute it and/or modify
5     it under the terms of the GNU Lesser General Public License as published
6     by the Free Software Foundation; either version 2.1 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU Lesser General Public License
15     along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 /**
19  * SECTION:element-qwidgetvideosink
20  *
21  * qwidgetvideosink is a video sink element that can draw directly on a QWidget
22  * using the QPainter API. To use it, you only have to set a QWidget pointer as
23  * the "widget" property and the video will then be rendered on that QWidget.
24  *
25  * This is useful for cases where you cannot or you do not want to use one of the
26  * sinks that implement the GstXOverlay interface, for example for rendering video
27  * inside a QGraphicsView or for rendering video on the QWS (Qt/Embedded) platform.
28  * This sink is guaranteed to work on all platforms supported by Qt, however it
29  * is not recommended to use it if you have another choice. For example, on X11 it
30  * is recommended to use "xvimagesink" instead, which uses hardware acceleration.
31  *
32  * There are certain rules for using qwidgetvideosink with threads. It must be
33  * created in the main thread, it must be destructed in the main thread and the
34  * "widget" property may only be read or written from the main thread as well.
35  * This is a (reasonable) limitation implied by Qt.
36  */
37
38 #include <gst/gst.h>
39 #include <gst/video/gstvideosink.h>
40 #include <gst/video/video.h>
41 #include <QtCore/QCoreApplication>
42 #include <QtCore/QWeakPointer>
43 #include <QtCore/QEvent>
44 #include <QtCore/QMutex>
45 #include <QtGui/QWidget>
46 #include <QtGui/QResizeEvent>
47 #include <QtGui/QPainter>
48
49 //BEGIN ******** declarations ********
50
51 enum {
52     BufferEventType = QEvent::User,
53     DeactivateEventType
54 };
55
56 class BufferEvent : public QEvent
57 {
58 public:
59     inline BufferEvent(GstBuffer *buf)
60         : QEvent(static_cast<QEvent::Type>(BufferEventType)),
61           buffer(gst_buffer_ref(buf))
62     {
63     }
64
65     GstBuffer *buffer;
66 };
67
68 class DeactivateEvent : public QEvent
69 {
70 public:
71     inline DeactivateEvent()
72         : QEvent(static_cast<QEvent::Type>(DeactivateEventType))
73     {
74     }
75 };
76
77 class WidgetProxy : public QObject
78 {
79     Q_OBJECT
80 public:
81     WidgetProxy(GObject *sink);
82     virtual ~WidgetProxy();
83
84     // "widget" property
85     QWidget *widget() const;
86     void setWidget(QWidget *widget);
87
88     // "force-aspect-ratio" property
89     bool forceAspectRatio() const;
90     void setForceAspectRatio(bool force);
91
92     bool isActive() const;
93     void setActive(bool active);
94
95     QSize widgetSize() const;
96     void setWidgetSize(const QSize & size);
97
98 private Q_SLOTS:
99     void widgetDestroyed();
100
101 protected:
102     virtual bool event(QEvent *event);
103     virtual bool eventFilter(QObject *filteredObject, QEvent *event);
104
105 private:
106 #ifndef GST_DISABLE_GST_DEBUG
107     //used for calling the Gst debug macros
108     GObject *m_sink;
109 #endif
110
111     // "widget" property
112     QWeakPointer<QWidget> m_widget;
113
114     // original value of the Qt::WA_OpaquePaintEvent attribute
115     bool m_opaquePaintEventAttribute : 1;
116
117     // "force-aspect-ratio" property
118     bool m_forceAspectRatio : 1;
119
120     // whether the sink is active (PAUSED or PLAYING)
121     bool m_isActive : 1;
122     mutable QMutex m_isActiveMutex;
123
124     // the current widget size, used for caps negotiation
125     mutable QMutex m_widgetSizeMutex;
126     QSize m_widgetSize;
127
128     // the buffer to be drawn next
129     GstBuffer *m_buffer;
130 };
131
132
133 GST_DEBUG_CATEGORY_STATIC(gst_qwidget_video_sink_debug);
134 #define GST_CAT_DEFAULT gst_qwidget_video_sink_debug
135
136 static const char *qwidgetvideosink_description = "A video sink that draws on a QWidget using QPainter";
137
138 #define GST_TYPE_QWIDGETVIDEOSINK \
139   (gst_qwidget_video_sink_get_type())
140 #define GST_QWIDGETVIDEOSINK(obj) \
141   (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_QWIDGETVIDEOSINK,GstQWidgetVideoSink))
142 #define GST_QWIDGETVIDEOSINK_CLASS(klass) \
143   (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_QWIDGETVIDEOSINK,GstQWidgetVideoSinkClass))
144
145 typedef struct _GstQWidgetVideoSink      GstQWidgetVideoSink;
146 typedef struct _GstQWidgetVideoSinkClass GstQWidgetVideoSinkClass;
147
148 struct _GstQWidgetVideoSink
149 {
150     GstVideoSink element;
151     WidgetProxy *proxy;
152 };
153
154 struct _GstQWidgetVideoSinkClass
155 {
156     GstVideoSinkClass parent_class;
157 };
158
159 enum {
160     PROP_0,
161     PROP_WIDGET,
162     PROP_FORCE_ASPECT_RATIO
163 };
164
165 static void
166 gst_qwidget_video_sink_finalize(GObject *object);
167
168 static void
169 gst_qwidget_video_sink_set_property(GObject *object, guint prop_id,
170                                     const GValue *value, GParamSpec *pspec);
171
172 static void
173 gst_qwidget_video_sink_get_property(GObject *object, guint prop_id,
174                                     GValue *value, GParamSpec *pspec);
175
176 static GstStateChangeReturn
177 gst_qwidget_video_sink_change_state(GstElement *element, GstStateChange transition);
178
179 static GstFlowReturn
180 gst_qwidget_video_sink_buffer_alloc(GstBaseSink *sink, guint64 offset,
181                                     guint size, GstCaps *caps, GstBuffer **buf);
182
183 static GstFlowReturn
184 gst_qwidget_video_sink_show_frame(GstVideoSink *video_sink, GstBuffer *buf);
185
186 //END ******** declarations ********
187 //BEGIN ******** WidgetProxy ********
188
189 WidgetProxy::WidgetProxy(GObject *sink)
190     : QObject(),
191 #ifndef GST_DISABLE_GST_DEBUG
192     m_sink(sink),
193 #endif
194     m_forceAspectRatio(false),
195     m_isActive(false),
196     m_buffer(NULL)
197 {
198 }
199
200 WidgetProxy::~WidgetProxy()
201 {
202     if (m_buffer) {
203         gst_buffer_unref(m_buffer);
204     }
205     setWidget(NULL);
206 }
207
208 QWidget *WidgetProxy::widget() const
209 {
210     return m_widget.data();
211 }
212
213 void WidgetProxy::setWidget(QWidget *widget)
214 {
215     GST_DEBUG_OBJECT(m_sink, "Setting \"widget\" property to %"GST_PTR_FORMAT, widget);
216
217     if (m_widget) {
218         m_widget.data()->removeEventFilter(this);
219         m_widget.data()->setAttribute(Qt::WA_OpaquePaintEvent, m_opaquePaintEventAttribute);
220         m_widget.data()->update();
221         disconnect(m_widget.data(), SIGNAL(destroyed(QObject*)), this, SLOT(widgetDestroyed()));
222
223         m_widget = QWeakPointer<QWidget>();
224     }
225
226     if (widget) {
227         widget->installEventFilter(this);
228         m_opaquePaintEventAttribute = widget->testAttribute(Qt::WA_OpaquePaintEvent);
229         widget->setAttribute(Qt::WA_OpaquePaintEvent, true);
230         widget->update();
231         connect(widget, SIGNAL(destroyed(QObject*)), this, SLOT(widgetDestroyed()));
232
233         m_widget = widget;
234         setWidgetSize(widget->size());
235     }
236 }
237
238 void WidgetProxy::widgetDestroyed()
239 {
240     m_widget = QWeakPointer<QWidget>();
241 }
242
243 bool WidgetProxy::forceAspectRatio() const
244 {
245     return m_forceAspectRatio;
246 }
247
248 void WidgetProxy::setForceAspectRatio(bool force)
249 {
250     GST_DEBUG_OBJECT(m_sink, "Setting \"force-aspect-ratio\" property to %s", force ? "true" : "false");
251
252     m_forceAspectRatio = force;
253 }
254
255 bool WidgetProxy::isActive() const
256 {
257     QMutexLocker lock(&m_isActiveMutex);
258     return m_isActive;
259 }
260
261 void WidgetProxy::setActive(bool active)
262 {
263     GST_INFO_OBJECT(m_sink, active ? "Activating" : "Deactivating");
264
265     QMutexLocker lock(&m_isActiveMutex);
266     m_isActive = active;
267     if (!active) {
268         QCoreApplication::postEvent(this, new DeactivateEvent());
269     }
270 }
271
272 QSize WidgetProxy::widgetSize() const
273 {
274     QMutexLocker lock(&m_widgetSizeMutex);
275     return m_widgetSize;
276 }
277
278 void WidgetProxy::setWidgetSize(const QSize & size)
279 {
280     GST_DEBUG_OBJECT(m_sink, "Widget size changed to (%d, %d)", size.width(), size.height());
281
282     QMutexLocker lock(&m_widgetSizeMutex);
283     m_widgetSize = size;
284 }
285
286 bool WidgetProxy::event(QEvent *event)
287 {
288     switch(event->type()) {
289     case BufferEventType:
290     {
291         BufferEvent *bufEvent = dynamic_cast<BufferEvent*>(event);
292         Q_ASSERT(bufEvent);
293
294         GST_LOG_OBJECT(m_sink, "Received buffer %"GST_PTR_FORMAT, bufEvent->buffer);
295
296         if (m_buffer) {
297             //free the previous buffer
298             gst_buffer_unref(m_buffer);
299             m_buffer = NULL;
300         }
301
302         if (m_widget && isActive()) {
303             //schedule this frame for rendering
304             m_buffer = bufEvent->buffer;
305             m_widget.data()->update();
306         } else {
307             //no widget, drop the frame
308             gst_buffer_unref(bufEvent->buffer);
309         }
310
311         return true;
312     }
313     case DeactivateEventType:
314     {
315         GST_LOG_OBJECT(m_sink, "Received deactivate event");
316
317         if (m_buffer) {
318             gst_buffer_unref(m_buffer);
319             m_buffer = NULL;
320         }
321
322         if (m_widget) {
323             m_widget.data()->update();
324         }
325
326         return true;
327     }
328     default:
329         return QObject::event(event);
330     }
331 }
332
333 bool WidgetProxy::eventFilter(QObject *filteredObject, QEvent *event)
334 {
335     if (filteredObject == m_widget.data()) {
336         switch(event->type()) {
337         case QEvent::Paint:
338         {
339             QPainter painter(m_widget.data());
340             if (m_buffer && isActive()) {
341                 GstCaps *caps = GST_BUFFER_CAPS(m_buffer);
342                 GstStructure *structure = gst_caps_get_structure(caps, 0);
343
344                 GstVideoRectangle srcRect;
345                 srcRect.x = srcRect.y = 0;
346                 gst_structure_get_int(structure, "width", &srcRect.w);
347                 gst_structure_get_int(structure, "height", &srcRect.h);
348
349                 QRect drawRect;
350                 if (m_forceAspectRatio) {
351                     GstVideoRectangle destRect;
352                     destRect.x = destRect.y = 0;
353                     destRect.w = m_widget.data()->width();
354                     destRect.h = m_widget.data()->height();
355
356                     GstVideoRectangle resultRect;
357                     gst_video_sink_center_rect(srcRect, destRect, &resultRect, TRUE);
358                     drawRect = QRect(resultRect.x, resultRect.y, resultRect.w, resultRect.h);
359
360                     //we are probably not going to paint all the widget,
361                     //so fill the remaining space with black
362                     painter.fillRect(m_widget.data()->rect(), Qt::black);
363                 } else {
364                     drawRect = m_widget.data()->rect();
365                 }
366
367                 GST_LOG_OBJECT(m_sink, "Rendering buffer %"GST_PTR_FORMAT". "
368                                        "Frame size is (%d, %d), "
369                                        "widget size is (%d, %d), "
370                                        "calculated draw area is (%d, %d)",
371                                        m_buffer,
372                                        srcRect.w, srcRect.h,
373                                        m_widget.data()->width(), m_widget.data()->height(),
374                                        drawRect.width(), drawRect.height());
375
376                 QImage image(m_buffer->data, srcRect.w, srcRect.h, QImage::Format_RGB32);
377                 painter.drawImage(drawRect, image);
378             } else {
379                 GST_LOG_OBJECT(m_sink, "Filling widget with black");
380                 painter.fillRect(m_widget.data()->rect(), Qt::black);
381             }
382             return true;
383         }
384         case QEvent::Resize:
385         {
386             QResizeEvent *resizeEvent = dynamic_cast<QResizeEvent*>(event);
387             setWidgetSize(resizeEvent->size());
388             return false;
389         }
390         default:
391             return false;
392         }
393     } else {
394         return QObject::eventFilter(filteredObject, event);
395     }
396 }
397
398 //END ******** WidgetProxy ********
399 //BEGIN ******** GstQWidgetVideoSink pad template & boilerplate ********
400
401 static GstStaticPadTemplate pad_template =
402     GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS,
403                             GST_STATIC_CAPS (GST_VIDEO_CAPS_xRGB_HOST_ENDIAN));
404
405 GST_BOILERPLATE(GstQWidgetVideoSink, gst_qwidget_video_sink, GstVideoSink, GST_TYPE_VIDEO_SINK);
406
407 //END ******** GstQWidgetVideoSink pad template & boilerplate ********
408 //BEGIN ******** GstQWidgetVideoSinkClass initialization methods ********
409
410 static void gst_qwidget_video_sink_base_init(gpointer gclass)
411 {
412     GstElementClass *element_class = GST_ELEMENT_CLASS(gclass);
413
414     gst_element_class_set_details_simple(element_class, "QWidgetVideoSink", "Sink/Video",
415                                          qwidgetvideosink_description,
416                                          "George Kiagiadakis <kiagiadakis.george@gmail.com>");
417
418     gst_element_class_add_pad_template(element_class, gst_static_pad_template_get(&pad_template));
419 }
420
421 /* initialize the class vtable */
422 static void gst_qwidget_video_sink_class_init(GstQWidgetVideoSinkClass *klass)
423 {
424     GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
425     gobject_class->finalize = gst_qwidget_video_sink_finalize;
426     gobject_class->set_property = gst_qwidget_video_sink_set_property;
427     gobject_class->get_property = gst_qwidget_video_sink_get_property;
428
429     GstElementClass *element_class = GST_ELEMENT_CLASS(klass);
430     element_class->change_state = gst_qwidget_video_sink_change_state;
431
432     GstBaseSinkClass *basesink_class = GST_BASE_SINK_CLASS(klass);
433     basesink_class->buffer_alloc = gst_qwidget_video_sink_buffer_alloc;
434
435     GstVideoSinkClass *videosink_class = GST_VIDEO_SINK_CLASS(klass);
436     videosink_class->show_frame = gst_qwidget_video_sink_show_frame;
437
438     /**
439      * GstQWidgetVideoSink::widget
440      *
441      * This property holds a pointer to the QWidget on which the sink will paint the video.
442      * You can set this property at any time, even if the element is in PLAYING
443      * state. You can also set this property to NULL at any time to release
444      * the widget. In this case, qwidgetvideosink will behave like a fakesink,
445      * i.e. it will silently drop all the frames that it receives. It is also safe
446      * to delete the widget that has been set as this property; the sink will be
447      * signaled and this property will automatically be set to NULL.
448      **/
449     g_object_class_install_property(gobject_class, PROP_WIDGET,
450         g_param_spec_pointer("widget", "Widget",
451                              "The widget on which this element will paint the video",
452                              static_cast<GParamFlags>(G_PARAM_READWRITE)));
453
454     /**
455      * GstQWidgetVideoSink::force-aspect-ratio
456      *
457      * If set to TRUE, the sink will scale the video respecting its original aspect ratio
458      * and any remaining space will be filled with black.
459      * If set to FALSE, the sink will scale the video to fit the whole widget.
460      **/
461     g_object_class_install_property(gobject_class, PROP_FORCE_ASPECT_RATIO,
462         g_param_spec_boolean("force-aspect-ratio", "Force aspect ratio",
463                              "When enabled, scaling will respect original aspect ratio",
464                              FALSE, static_cast<GParamFlags>(G_PARAM_READWRITE)));
465 }
466
467 //END ******** GstQWidgetVideoSinkClass initialization methods ********
468 //BEGIN ******** GstQWidgetVideoSink instance virtual methods ********
469
470 /* element constructor */
471 static void gst_qwidget_video_sink_init(GstQWidgetVideoSink *sink, GstQWidgetVideoSinkClass*)
472 {
473     GST_INFO_OBJECT(sink, "Initializing qwidgetvideosink");
474     sink->proxy = new WidgetProxy(G_OBJECT(sink));
475 }
476
477 /* element destructor */
478 static void gst_qwidget_video_sink_finalize(GObject *object)
479 {
480     GstQWidgetVideoSink *sink = GST_QWIDGETVIDEOSINK(object);
481
482     delete sink->proxy;
483     sink->proxy = NULL;
484     GST_INFO_OBJECT(sink, "Finalized qwidgetvideosink");
485
486     G_OBJECT_CLASS(parent_class)->finalize(object);
487 }
488
489 static void gst_qwidget_video_sink_set_property(GObject *object, guint prop_id,
490                                                 const GValue *value, GParamSpec *pspec)
491 {
492     GstQWidgetVideoSink *sink = GST_QWIDGETVIDEOSINK(object);
493
494     switch (prop_id) {
495     case PROP_WIDGET:
496         sink->proxy->setWidget(static_cast<QWidget*>(g_value_get_pointer(value)));
497         break;
498     case PROP_FORCE_ASPECT_RATIO:
499         sink->proxy->setForceAspectRatio(g_value_get_boolean(value));
500         break;
501     default:
502         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
503         break;
504     }
505 }
506
507 static void gst_qwidget_video_sink_get_property(GObject *object, guint prop_id,
508                                                 GValue *value, GParamSpec *pspec)
509 {
510     GstQWidgetVideoSink *sink = GST_QWIDGETVIDEOSINK(object);
511
512     switch (prop_id) {
513     case PROP_WIDGET:
514         g_value_set_pointer(value, sink->proxy->widget());
515         break;
516     case PROP_FORCE_ASPECT_RATIO:
517         g_value_set_boolean(value, sink->proxy->forceAspectRatio());
518         break;
519     default:
520         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
521         break;
522     }
523 }
524
525 static GstStateChangeReturn gst_qwidget_video_sink_change_state(GstElement *element,
526                                                                 GstStateChange transition)
527 {
528     GstQWidgetVideoSink *sink = GST_QWIDGETVIDEOSINK(element);
529
530     switch(transition) {
531     case GST_STATE_CHANGE_READY_TO_PAUSED:
532         sink->proxy->setActive(true);
533         break;
534     case GST_STATE_CHANGE_PAUSED_TO_READY:
535         sink->proxy->setActive(false);
536         break;
537     default:
538         break;
539     }
540
541     return GST_ELEMENT_CLASS(parent_class)->change_state(element, transition);
542 }
543
544 static GstFlowReturn gst_qwidget_video_sink_buffer_alloc(GstBaseSink *base_sink, guint64 offset,
545                                                          guint size, GstCaps *caps, GstBuffer **buf)
546 {
547     GstQWidgetVideoSink *sink = GST_QWIDGETVIDEOSINK(base_sink);
548
549     bool unrefCaps = false;
550     GstStructure *structure = gst_caps_get_structure(caps, 0);
551
552     GstVideoRectangle srcRect;
553     srcRect.x = srcRect.y = 0;
554     if (!gst_structure_get_int(structure, "width", &srcRect.w) ||
555         !gst_structure_get_int(structure, "height", &srcRect.h))
556     {
557         return GST_FLOW_NOT_NEGOTIATED;
558     }
559
560     GstVideoRectangle destRect;
561     destRect.x = destRect.y = 0;
562     QSize widgetSize = sink->proxy->widgetSize();
563     destRect.w = widgetSize.width();
564     destRect.h = widgetSize.height();
565
566     GstVideoRectangle resultRect;
567     if (sink->proxy->forceAspectRatio()) {
568         gst_video_sink_center_rect(srcRect, destRect, &resultRect, TRUE);
569     } else {
570         resultRect = destRect;
571     }
572
573     if (resultRect.w != srcRect.w || resultRect.h != srcRect.h) {
574         GstCaps *desiredCaps = gst_caps_copy(caps);
575         GstStructure *structure = gst_caps_get_structure(desiredCaps, 0);
576         gst_structure_set(structure, "width", G_TYPE_INT, resultRect.w,
577                                      "height", G_TYPE_INT, resultRect.h, NULL);
578
579         if (gst_pad_peer_accept_caps(GST_VIDEO_SINK_PAD(sink), desiredCaps)) {
580             caps = desiredCaps;
581             unrefCaps = true;
582             size = resultRect.w * resultRect.h * 4;
583         } else {
584             gst_caps_unref(desiredCaps);
585         }
586     }
587
588     *buf = gst_buffer_new_and_alloc(size);
589     gst_buffer_set_caps(*buf, caps);
590     GST_BUFFER_OFFSET(*buf) = offset;
591
592     if (unrefCaps) {
593         gst_caps_unref(caps);
594     }
595
596     GST_LOG_OBJECT(sink, "Requested buffer was (%d, %d), allocated was (%d, %d)",
597                           srcRect.w, srcRect.h, resultRect.w, resultRect.h);
598
599     return GST_FLOW_OK;
600 }
601
602 static GstFlowReturn gst_qwidget_video_sink_show_frame(GstVideoSink *video_sink, GstBuffer *buf)
603 {
604     GstQWidgetVideoSink *sink = GST_QWIDGETVIDEOSINK(video_sink);
605     GST_LOG_OBJECT(sink, "Posting new buffer (%"GST_PTR_FORMAT") for rendering", buf);
606     QCoreApplication::postEvent(sink->proxy, new BufferEvent(buf));
607     return GST_FLOW_OK;
608 }
609
610 //END ******** GstQWidgetVideoSink instance virtual methods ********
611 //BEGIN ******** plugin interface ********
612
613 /* entry point to initialize the plug-in */
614 static gboolean plugin_init(GstPlugin *plugin)
615 {
616     GST_DEBUG_CATEGORY_INIT(gst_qwidget_video_sink_debug, "qwidgetvideosink",
617                             0, qwidgetvideosink_description);
618
619     return gst_element_register(plugin, "qwidgetvideosink",
620                                 GST_RANK_NONE, GST_TYPE_QWIDGETVIDEOSINK);
621 }
622
623 /* PACKAGE: this is usually set by autotools depending on some _INIT macro
624  * in configure.ac and then written into and defined in config.h, but we can
625  * just set it ourselves here in case someone doesn't use autotools to
626  * compile this code. GST_PLUGIN_DEFINE needs PACKAGE to be defined.
627  */
628 #ifndef PACKAGE
629 # define PACKAGE "qtgstreamer"
630 #endif
631
632 GST_PLUGIN_DEFINE (
633     GST_VERSION_MAJOR,
634     GST_VERSION_MINOR,
635     "qwidgetvideosink",
636     qwidgetvideosink_description,
637     plugin_init,
638     "0.1", //plugin version
639     "LGPL",
640     "QtGstreamer",
641     "http://gitorious.org/qtgstreamer"
642 )
643
644 //END ******** plugin interface ********
645
646 #include "gstqwidgetvideosink.moc"