diff --git a/src/QGst/Quick/videosurface.cpp b/src/QGst/Quick/videosurface.cpp index 6964516..e0fb322 100644 --- a/src/QGst/Quick/videosurface.cpp +++ b/src/QGst/Quick/videosurface.cpp @@ -1,75 +1,75 @@ /* Copyright (C) 2012-2013 Collabora Ltd. @author George Kiagiadakis This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #include "videosurface_p.h" #include #include namespace QGst { namespace Quick { +static void updateCallback(void*, VideoSurface* vs) { vs->onUpdate(); } + VideoSurface::VideoSurface(QObject *parent) : QObject(parent), d(new VideoSurfacePrivate) { } VideoSurface::~VideoSurface() { + if (d->updateHandler) + g_signal_handler_disconnect(d->videoSink, d->updateHandler); + if (d->videoSink) { gst_element_set_state(d->videoSink, GST_STATE_NULL); gst_object_unref(d->videoSink); } delete d; } -static void updateCallback(void*, VideoSurface* vs) -{ - vs->onUpdate(); -} - GstElement* VideoSurface::videoSink() const { if (!d->videoSink) { d->videoSink = gst_element_factory_make("qtquick2videosink", "qtquick2videosink"); if (d->videoSink) { gst_object_ref_sink(d->videoSink); } else { qCritical("Failed to create qtquick2videosink. Make sure it is installed correctly"); return nullptr; } Q_ASSERT(GST_IS_ELEMENT(d->videoSink)); Q_ASSERT(G_IS_OBJECT(d->videoSink)); - g_signal_connect(d->videoSink, "update", G_CALLBACK(updateCallback), (gpointer) this); + d->updateHandler = g_signal_connect(d->videoSink, "update", G_CALLBACK(updateCallback), (gpointer) this); } return d->videoSink; } void VideoSurface::onUpdate() { Q_FOREACH(QQuickItem *item, d->items) { item->update(); } } } // namespace Quick } // namespace QGst diff --git a/src/QGst/Quick/videosurface_p.h b/src/QGst/Quick/videosurface_p.h index 6062d70..14d75d5 100644 --- a/src/QGst/Quick/videosurface_p.h +++ b/src/QGst/Quick/videosurface_p.h @@ -1,37 +1,38 @@ /* Copyright (C) 2012-2013 Collabora Ltd. @author George Kiagiadakis This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #ifndef QGST_QUICK_VIDEOSURFACE_P_H #define QGST_QUICK_VIDEOSURFACE_P_H #include "videosurface.h" #include "videoitem.h" namespace QGst { namespace Quick { class VideoSurfacePrivate { public: QSet items; GstElement* videoSink = nullptr; + int updateHandler = 0; }; } // namespace Quick } // namespace QGst #endif // QGST_QUICK_VIDEOSURFACE_P_H diff --git a/src/qml/Config.qml b/src/qml/Config.qml index 48d0e2b..e059610 100644 --- a/src/qml/Config.qml +++ b/src/qml/Config.qml @@ -1,79 +1,79 @@ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.0 import KamosoQtGStreamer 1.0 import org.kde.kamoso 3.0 import org.kde.kirigami 2.0 as Kirigami GridView { id: view readonly property real delegateWidth: Kirigami.Units.gridUnit*4 readonly property int columnCount: Math.floor(width/delegateWidth) cellWidth: width/columnCount cellHeight: cellWidth model: ListModel { ListElement { filters: "identity" } ListElement { filters: "bulge" } ListElement { filters: "frei0r-filter-cartoon" } ListElement { filters: "frei0r-filter-twolay0r" } // ListElement { filters: "frei0r-filter-color-distance" } ListElement { filters: "dicetv" } ListElement { filters: "frei0r-filter-distort0r" } ListElement { filters: "edgetv" } ListElement { filters: "videoflip method=horizontal-flip" } ListElement { filters: "coloreffects preset=heat" } ListElement { filters: "videobalance saturation=0 ! agingtv" } ListElement { filters: "videobalance saturation=1.5 hue=-0.5" } // ListElement { filters: "frei0r-filter-invert0r" } ListElement { filters: "kaleidoscope" } ListElement { filters: "videobalance saturation=1.5 hue=+0.5" } ListElement { filters: "mirror" } ListElement { filters: "videobalance saturation=0" } ListElement { filters: "optv" } ListElement { filters: "pinch" } ListElement { filters: "quarktv" } ListElement { filters: "radioactv" } ListElement { filters: "revtv" } ListElement { filters: "rippletv" } ListElement { filters: "videobalance saturation=2" } ListElement { filters: "coloreffects preset=sepia" } ListElement { filters: "shagadelictv" } // ListElement { filters: "frei0r-filter-sobel" } ListElement { filters: "square" } ListElement { filters: "streaktv" } ListElement { filters: "stretch" } ListElement { filters: "frei0r-filter-delay0r delaytime=1" } ListElement { filters: "twirl" } ListElement { filters: "vertigotv" } ListElement { filters: "warptv" } ListElement { filters: "coloreffects preset=xray" } } delegate: MouseArea { id: delegateItem width: GridView.view.cellHeight height: GridView.view.cellWidth VideoItem { anchors.fill: parent PipelineItem { id: pipe playing: false onFailed: { delegateItem.visible = false -// view.model.remove(index) + view.model.remove(index) } description: "filesrc location=\"" + webcam.sampleImage + "\" ! decodebin ! imagefreeze ! videoconvert ! " + model.filters + " name=last" } surface: pipe.surface } onClicked: { devicesModel.playingDevice.filters = model.filters } } } diff --git a/src/video/webcamcontrol.cpp b/src/video/webcamcontrol.cpp index fdb7498..2fa4374 100644 --- a/src/video/webcamcontrol.cpp +++ b/src/video/webcamcontrol.cpp @@ -1,407 +1,408 @@ /************************************************************************************* * Copyright (C) 2012 by Alejandro Fiestas Olivares * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "webcamcontrol.h" #include "kamosoSettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "QGst/Quick/VideoItem" #include #include #include #include #include #include #include #include #include "QGst/Quick/VideoSurface" static QString debugMessage(GstMessage* msg) { gchar *debug = nullptr; GError *e = nullptr; if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_WARNING) { gst_message_parse_warning(msg, &e, &debug); } else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR) { gst_message_parse_error(msg, &e, &debug); } if (!debug) return {}; if (e) { qWarning() << "error debugMessage:" << e->message; g_error_free (e); } const auto ret = QString::fromUtf8(debug); g_free(debug); return ret; } template GstState pipelineCurrentState(const T &pipe) { GstState currentState, pendingState; GstStateChangeReturn result = gst_element_get_state(GST_ELEMENT(pipe.data()), ¤tState, &pendingState, GST_CLOCK_TIME_NONE ); Q_ASSERT(result != GST_STATE_CHANGE_FAILURE); return currentState; } class PipelineItem : public QObject, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) Q_PROPERTY(QObject* surface READ surface CONSTANT) Q_PROPERTY(bool playing READ playing WRITE setPlaying NOTIFY playingChanged) public: PipelineItem() : QObject() , m_surface(new QGst::Quick::VideoSurface(this)) { g_object_ref(m_surface->videoSink()); g_object_set(m_surface->videoSink(), "force-aspect-ratio", true, NULL); } ~PipelineItem() { if (m_pipeline) gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_NULL); } void onBusMessage(GstMessage* message) { switch (GST_MESSAGE_TYPE(message)) { case GST_MESSAGE_EOS: //End of stream. We reached the end of the file. setPlaying(false); break; case GST_MESSAGE_ERROR: {//Some error occurred. qCritical() << "error on:" << m_description << debugMessage(message); gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_NULL); m_pipeline.reset(nullptr); + Q_EMIT failed(); } break; default: break; } } void classBegin() override {} void componentComplete() override { m_complete = true; refresh(); } QGst::Quick::VideoSurface *surface() const { return m_surface; } QString description() const { return m_description; } void setDescription(const QString &desc) { if (m_description != desc) { m_description = desc; Q_EMIT descriptionChanged(); refresh(); } } Q_SCRIPTABLE void refresh() { if (!m_description.isEmpty() && m_complete) { if(m_pipeline) gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_NULL); GError* e = nullptr; m_pipeline.reset(GST_PIPELINE(gst_parse_launch(m_description.toUtf8().constData(), &e))); if (e) { qWarning() << "error:" << e->message; Q_EMIT failed(); return; } Q_ASSERT(m_pipeline); gst_bin_add(GST_BIN(m_pipeline.data()), m_surface->videoSink()); Q_ASSERT(m_pipeline); auto lastItem = gst_bin_get_by_name(GST_BIN(m_pipeline.data()), "last"); Q_ASSERT(lastItem); bool b = gst_element_link(lastItem, m_surface->videoSink()); Q_ASSERT(b); gst_bus_add_watch (gst_pipeline_get_bus(m_pipeline.data()), &pipelineWatch, this); } setPlaying(m_playing); } static gboolean pipelineWatch(GstBus */*bus*/, GstMessage *message, gpointer user_data) { PipelineItem* wc = static_cast(user_data); wc->onBusMessage(message); return G_SOURCE_CONTINUE; } void setPlaying(bool playing) { if (playing != m_playing) { m_playing = playing; Q_EMIT playingChanged(playing); } if (m_pipeline) gst_element_set_state(GST_ELEMENT(m_pipeline.data()), playing ? GST_STATE_PLAYING : GST_STATE_PAUSED); } bool playing() const { return m_pipeline && pipelineCurrentState(m_pipeline) == GST_STATE_PLAYING; } Q_SIGNALS: void playingChanged(bool playing); void surfaceChanged(); void descriptionChanged(); void failed(); private: bool m_playing = false; bool m_complete = false; QString m_description; QScopedPointer> m_pipeline; QGst::Quick::VideoSurface * const m_surface; }; WebcamControl::WebcamControl() { gst_init(NULL, NULL); QQmlApplicationEngine* engine = new QQmlApplicationEngine(this); engine->rootContext()->setContextObject(new KLocalizedContext(engine)); qmlRegisterUncreatableType("org.kde.kamoso", 3, 0, "Device", "You're not supposed to mess with this yo"); qmlRegisterType("org.kde.kamoso", 3, 0, "DirModel"); qmlRegisterType("org.kde.kamoso", 3, 0, "PreviewFetcher"); qmlRegisterType("org.kde.kamoso", 3, 0, "PipelineItem"); qmlRegisterType("KamosoQtGStreamer", 1, 0, "VideoItem"); qmlRegisterUncreatableType("org.kde.kamoso", 3, 0, "KJob", "you're not supposed to do that"); m_surface = new QGst::Quick::VideoSurface(this); engine->rootContext()->setContextProperty("config", Settings::self()); engine->rootContext()->setContextProperty("whites", new WhiteWidgetManager(this)); engine->rootContext()->setContextProperty("devicesModel", DeviceManager::self()); engine->rootContext()->setContextProperty("webcam", new Kamoso(this)); engine->rootContext()->setContextProperty("videoSurface1", m_surface); engine->load(QUrl("qrc:/qml/Main.qml")); g_object_set(m_surface->videoSink(), "force-aspect-ratio", true, NULL); connect(DeviceManager::self(), &DeviceManager::playingDeviceChanged, this, &WebcamControl::play); connect(DeviceManager::self(), &DeviceManager::noDevices, this, &WebcamControl::stop); } WebcamControl::~WebcamControl() { DeviceManager::self()->save(); Settings::self()->save(); } void WebcamControl::stop() { qDebug() << "Stop"; if(m_pipeline) { gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_NULL); m_pipeline.reset(nullptr); } } bool WebcamControl::play() { auto dev = DeviceManager::self()->playingDevice(); return !dev || playDevice(dev); } static gboolean webcamWatch(GstBus */*bus*/, GstMessage *message, gpointer user_data) { WebcamControl* wc = static_cast(user_data); wc->onBusMessage(message); return G_SOURCE_CONTINUE; } bool WebcamControl::playDevice(Device *device) { Q_ASSERT(device); //If we already have a pipeline for this device, just set it to picture mode if (m_pipeline && m_currentDevice == device->udi()) { g_object_set(m_pipeline.data(), "mode", 2, nullptr); g_object_set(m_pipeline.data(), "location", m_tmpVideoPath.toUtf8().constData(), nullptr); return true; } //If we are changing the device, cleanup and stop old pipeline if (m_pipeline && m_currentDevice != device->udi()) { //Should we maybe try to just change the device path instead of re-creating? qDebug() << "playing device" << device->path() << pipelineCurrentState(m_pipeline); gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_NULL); } m_cameraSource.reset(gst_element_factory_make("wrappercamerabinsrc", "video_balance")); // Another option here is to return true, therefore continuing with launching, but // in that case the application is mostly useless. if (m_cameraSource.isNull()) { qWarning() << "The webcam controller was unable to find or load wrappercamerabinsrc plugin;" << "please make sure all required gstreamer plugins are installed."; return false; } { GError* error = nullptr; auto source = gst_parse_bin_from_description(QByteArray("v4l2src device=") + device->path().toUtf8(), GST_PARSE_FLAG_FATAL_ERRORS, &error); Q_ASSERT(!error); g_object_set(m_cameraSource.data(), "video-source", source, nullptr); } m_pipeline.reset(GST_PIPELINE(gst_element_factory_make("camerabin", "camerabin"))); gst_bus_add_watch (gst_pipeline_get_bus(m_pipeline.data()), &webcamWatch, this); g_object_set(m_pipeline.data(), "camera-source", m_cameraSource.data(), nullptr); g_object_set(m_pipeline.data(), "viewfinder-sink", m_surface->videoSink(), nullptr); setVideoSettings(); gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_READY); auto caps = gst_caps_from_string("video/x-raw, framerate=(fraction){30/1, 15/1}, width=(int)640, height=(int)480, format=(string){YUY2}, pixel-aspect-ratio=(fraction)1/1, interlace-mode=(string)progressive"); g_object_set(m_pipeline.data(), "viewfinder-caps", caps, nullptr); gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_PLAYING); m_currentDevice = device->udi(); return true; } void WebcamControl::onBusMessage(GstMessage* message) { switch (GST_MESSAGE_TYPE (message)) { case GST_MESSAGE_EOS: //End of stream. We reached the end of the file. stop(); break; case GST_MESSAGE_ERROR: //Some error occurred. qCritical() << debugMessage(message); stop(); play(); break; case GST_MESSAGE_ELEMENT: if (strcmp (GST_MESSAGE_SRC_NAME (message), "camerabin") == 0) { auto structure = gst_message_get_structure (message); if (gst_structure_get_name (structure) == QByteArray("image-done")) { const gchar *filename = gst_structure_get_string (structure, "filename"); Q_EMIT photoTaken(QString::fromUtf8(filename)); } } else { qDebug() << "skipping message..." << GST_MESSAGE_SRC_NAME (message); } default: // qDebug() << msg->type(); // qDebug() << msg->typeName(); // qDebug() << msg->internalStructure()->name(); break; } } void WebcamControl::takePhoto(const QUrl &url) { if (!m_pipeline) { qWarning() << "couldn't take photo, no pipeline"; return; } g_object_set(m_pipeline.data(), "mode", 1, nullptr); const QString path = url.isLocalFile() ? url.toLocalFile() : QStandardPaths::writableLocation(QStandardPaths::TempLocation)+"/kamoso_photo.jpg"; g_object_set(m_pipeline.data(), "location", path.toUtf8().constData(), nullptr); g_signal_emit_by_name (m_pipeline.data(), "start-capture", 0); if (!url.isLocalFile()) { KIO::copy(QUrl::fromLocalFile(path), url); } } void WebcamControl::startRecording() { QString date = QDateTime::currentDateTime().toString("ddmmyyyy_hhmmss"); m_tmpVideoPath = QDir::tempPath() + QStringLiteral("/kamoso_%1.mkv").arg(date); g_object_set(m_pipeline.data(), "mode", 2, nullptr); g_object_set(m_pipeline.data(), "location", m_tmpVideoPath.toUtf8().constData(), nullptr); g_signal_emit_by_name (m_pipeline.data(), "start-capture", 0); } QString WebcamControl::stopRecording() { g_signal_emit_by_name (m_pipeline.data(), "stop-capture", 0); return m_tmpVideoPath; } void WebcamControl::setExtraFilters(const QString& extraFilters) { if (extraFilters != m_extraFilters) { m_extraFilters = extraFilters; updateSourceFilter(); } } void WebcamControl::updateSourceFilter() { const auto prevstate = pipelineCurrentState(m_pipeline); gst_element_set_state(GST_ELEMENT(m_pipeline.data()), GST_STATE_NULL); //videoflip: use video-direction=horiz, method is deprecated, not changing now because video-direction doesn't seem to be available on gstreamer 1.8 which is still widely used QString filters = QStringLiteral("videoflip method=4"); if (!m_extraFilters.isEmpty()) { filters.prepend(m_extraFilters + QStringLiteral(" ! ")); } qDebug() << "setting filter" << filters; GError* error = nullptr; g_object_set(m_cameraSource.data(), "video-source-filter", gst_parse_bin_from_description(filters.toUtf8().constData(), true, &error), nullptr); Q_ASSERT(!error); if (prevstate != GST_STATE_NULL) gst_element_set_state(GST_ELEMENT(m_pipeline.data()), prevstate); } void WebcamControl::setVideoSettings() { Device *device = DeviceManager::self()->playingDevice(); connect(device, &Device::filtersChanged, this, &WebcamControl::setExtraFilters); m_extraFilters = device->filters(); updateSourceFilter(); } #include "webcamcontrol.moc"