diff options
Diffstat (limited to 'WebCore/platform/graphics/gstreamer')
12 files changed, 3351 insertions, 0 deletions
diff --git a/WebCore/platform/graphics/gstreamer/DataSourceGStreamer.cpp b/WebCore/platform/graphics/gstreamer/DataSourceGStreamer.cpp new file mode 100644 index 0000000..567da74 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/DataSourceGStreamer.cpp @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2009 Igalia S.L + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" +#include "DataSourceGStreamer.h" + +#include <gio/gio.h> +#include <glib.h> +#include <gst/gst.h> +#include <gst/pbutils/missing-plugins.h> + +static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS_ANY); + +GST_DEBUG_CATEGORY_STATIC(webkit_data_src_debug); +#define GST_CAT_DEFAULT webkit_data_src_debug + +static void webkit_data_src_uri_handler_init(gpointer g_iface, + gpointer iface_data); + +static void webkit_data_src_finalize(WebkitDataSrc* src); +static GstStateChangeReturn webkit_data_src_change_state(GstElement* element, + GstStateChange transition); + +static const GInterfaceInfo urihandler_info = { + webkit_data_src_uri_handler_init, + 0, 0 +}; + + +static void _do_init(GType datasrc_type) +{ + GST_DEBUG_CATEGORY_INIT(webkit_data_src_debug, "webkit_data_src", 0, "datasrc element"); + g_type_add_interface_static(datasrc_type, GST_TYPE_URI_HANDLER, + &urihandler_info); +} + +GST_BOILERPLATE_FULL(WebkitDataSrc, webkit_data_src, GstBin, GST_TYPE_BIN, _do_init); + +static void webkit_data_src_base_init(gpointer klass) +{ + GstElementClass* element_class = GST_ELEMENT_CLASS(klass); + + gst_element_class_add_pad_template(element_class, + gst_static_pad_template_get(&src_template)); + gst_element_class_set_details_simple(element_class, (gchar*) "WebKit data source element", + (gchar*) "Source", + (gchar*) "Handles data: uris", + (gchar*) "Philippe Normand <pnormand@igalia.com>"); + +} + +static void webkit_data_src_class_init(WebkitDataSrcClass* klass) +{ + GObjectClass* oklass = G_OBJECT_CLASS(klass); + GstElementClass* eklass = GST_ELEMENT_CLASS(klass); + + oklass->finalize = (GObjectFinalizeFunc) webkit_data_src_finalize; + eklass->change_state = webkit_data_src_change_state; +} + + +static gboolean webkit_data_src_reset(WebkitDataSrc* src) +{ + GstPad* targetpad; + + if (src->kid) { + gst_element_set_state(src->kid, GST_STATE_NULL); + gst_bin_remove(GST_BIN(src), src->kid); + } + + src->kid = gst_element_factory_make("giostreamsrc", "streamsrc"); + if (!src->kid) { + GST_ERROR_OBJECT(src, "Failed to create giostreamsrc"); + return FALSE; + } + + gst_bin_add(GST_BIN(src), src->kid); + + targetpad = gst_element_get_static_pad(src->kid, "src"); + gst_ghost_pad_set_target(GST_GHOST_PAD(src->pad), targetpad); + gst_object_unref(targetpad); + + return TRUE; +} + +static void webkit_data_src_init(WebkitDataSrc* src, + WebkitDataSrcClass* g_class) +{ + GstPadTemplate* pad_template = gst_static_pad_template_get(&src_template); + src->pad = gst_ghost_pad_new_no_target_from_template("src", + pad_template); + + gst_element_add_pad(GST_ELEMENT(src), src->pad); + + webkit_data_src_reset(src); +} + +static void webkit_data_src_finalize(WebkitDataSrc* src) +{ + g_free(src->uri); + + if (src->kid) { + GST_DEBUG_OBJECT(src, "Removing giostreamsrc element"); + gst_element_set_state(src->kid, GST_STATE_NULL); + gst_bin_remove(GST_BIN(src), src->kid); + src->kid = 0; + } + + GST_CALL_PARENT(G_OBJECT_CLASS, finalize, ((GObject* )(src))); +} + +static GstStateChangeReturn webkit_data_src_change_state(GstElement* element, GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + WebkitDataSrc* src = WEBKIT_DATA_SRC(element); + + switch (transition) { + case GST_STATE_CHANGE_NULL_TO_READY: + if (!src->kid) { + gst_element_post_message(element, + gst_missing_element_message_new(element, "giostreamsrc")); + GST_ELEMENT_ERROR(src, CORE, MISSING_PLUGIN, (0), ("no giostreamsrc")); + return GST_STATE_CHANGE_FAILURE; + } + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS(parent_class)->change_state(element, transition); + if (G_UNLIKELY(ret == GST_STATE_CHANGE_FAILURE)) + return ret; + + // Downwards state change code should be here, after chaining up + // to the parent class. + + return ret; +} + +/*** GSTURIHANDLER INTERFACE *************************************************/ + +static GstURIType webkit_data_src_uri_get_type(void) +{ + return GST_URI_SRC; +} + +static gchar** webkit_data_src_uri_get_protocols(void) +{ + static gchar* protocols[] = {(gchar*) "data", 0 }; + + return protocols; +} + +static const gchar* webkit_data_src_uri_get_uri(GstURIHandler* handler) +{ + WebkitDataSrc* src = WEBKIT_DATA_SRC(handler); + + return src->uri; +} + +static gboolean webkit_data_src_uri_set_uri(GstURIHandler* handler, const gchar* uri) +{ + WebkitDataSrc* src = WEBKIT_DATA_SRC(handler); + + // URI as defined in RFC2397: + // "data:" [ mediatype ] [ ";base64" ] "," data + // we parse URIs like this one: + // data:audio/3gpp;base64,AA... + + gchar** scheme_and_remains = g_strsplit(uri, ":", 2); + gchar** mime_type_and_options = g_strsplit(scheme_and_remains[1], ";", 0); + gint options_size = g_strv_length(mime_type_and_options); + gchar* data = 0; + gchar* mime_type = 0; + gint ret = FALSE; + + // we require uris with a specified mime-type and base64-encoded + // data. It doesn't make much sense anyway to play plain/text data + // with very few allowed characters (as per the RFC). + + if (GST_STATE(src) >= GST_STATE_PAUSED) { + GST_ERROR_OBJECT(src, "Element already configured. Reset it and retry"); + } else if (!options_size) + GST_ERROR_OBJECT(src, "A mime-type is needed in %s", uri); + else { + mime_type = mime_type_and_options[0]; + data = mime_type_and_options[options_size-1]; + + guchar* decoded_data = 0; + gsize decoded_size; + + if (!g_str_has_prefix(data, "base64")) + GST_ERROR_OBJECT(src, "Data has to be base64-encoded in %s", uri); + else { + decoded_data = g_base64_decode(data+7, &decoded_size); + GInputStream* stream = g_memory_input_stream_new_from_data(decoded_data, + decoded_size, + g_free); + g_object_set(src->kid, "stream", stream, NULL); + g_object_unref(stream); + + if (src->uri) { + g_free(src->uri); + src->uri = 0; + } + + src->uri = g_strdup(uri); + ret = TRUE; + } + } + + g_strfreev(scheme_and_remains); + g_strfreev(mime_type_and_options); + return ret; +} + +static void webkit_data_src_uri_handler_init(gpointer g_iface, gpointer iface_data) +{ + GstURIHandlerInterface* iface = (GstURIHandlerInterface *) g_iface; + + iface->get_type = webkit_data_src_uri_get_type; + iface->get_protocols = webkit_data_src_uri_get_protocols; + iface->get_uri = webkit_data_src_uri_get_uri; + iface->set_uri = webkit_data_src_uri_set_uri; +} diff --git a/WebCore/platform/graphics/gstreamer/DataSourceGStreamer.h b/WebCore/platform/graphics/gstreamer/DataSourceGStreamer.h new file mode 100644 index 0000000..3e88f63 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/DataSourceGStreamer.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009 Igalia S.L + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DATA_SOURCE_GSTREAMER_H +#define DATA_SOURCE_GSTREAMER_H + +#include <glib-object.h> +#include <gst/base/gstbasesrc.h> + +G_BEGIN_DECLS + +#define WEBKIT_TYPE_DATA_SRC (webkit_data_src_get_type ()) +#define WEBKIT_DATA_SRC(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), WEBKIT_TYPE_DATA_SRC, WebkitDataSrc)) +#define WEBKIT_DATA_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), WEBKIT_TYPE_DATA_SRC, WebkitDataSrcClass)) +#define WEBKIT_IS_DATA_SRC(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), WEBKIT_TYPE_DATA_SRC)) +#define WEBKIT_IS_DATA_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), WEBKIT_TYPE_DATA_SRC)) + +typedef struct _WebkitDataSrc WebkitDataSrc; +typedef struct _WebkitDataSrcClass WebkitDataSrcClass; + + +struct _WebkitDataSrc { + GstBin parent; + + /* explicit pointers to stuff used */ + GstElement* kid; + GstPad* pad; + gchar* uri; +}; + +struct _WebkitDataSrcClass { + GstBinClass parent_class; +}; + +GType webkit_data_src_get_type(void); + +G_END_DECLS + +#endif diff --git a/WebCore/platform/graphics/gstreamer/GOwnPtrGStreamer.cpp b/WebCore/platform/graphics/gstreamer/GOwnPtrGStreamer.cpp new file mode 100644 index 0000000..1d14b5a --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/GOwnPtrGStreamer.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 Igalia S.L + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#include "config.h" +#include "GOwnPtrGStreamer.h" + +#if ENABLE(VIDEO) +#include <gst/gstelement.h> + +namespace WTF { + +template <> void freeOwnedGPtr<GstElement>(GstElement* ptr) +{ + if (ptr) + gst_object_unref(ptr); +} +#endif + +} diff --git a/WebCore/platform/graphics/gstreamer/GOwnPtrGStreamer.h b/WebCore/platform/graphics/gstreamer/GOwnPtrGStreamer.h new file mode 100644 index 0000000..6655f38 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/GOwnPtrGStreamer.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 Igalia S.L + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef GOwnPtrGStreamer_h +#define GOwnPtrGStreamer_h + +#include "GOwnPtr.h" + +typedef struct _GstElement GstElement; + +namespace WTF { + +template<> void freeOwnedGPtr<GstElement>(GstElement* ptr); + +} + +#endif diff --git a/WebCore/platform/graphics/gstreamer/ImageGStreamer.h b/WebCore/platform/graphics/gstreamer/ImageGStreamer.h new file mode 100644 index 0000000..2e97b4d --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/ImageGStreamer.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 Igalia S.L + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef ImageGStreamer_h +#define ImageGStreamer_h + +#if ENABLE(VIDEO) + +#include "BitmapImage.h" + +#include <gst/gst.h> +#include <gst/video/video.h> +#include <wtf/PassRefPtr.h> + +#if PLATFORM(CAIRO) +#include <cairo.h> +#endif + +namespace WebCore { +class IntSize; + +class ImageGStreamer : public RefCounted<ImageGStreamer> { + public: + static PassRefPtr<ImageGStreamer> createImage(GstBuffer*); + ~ImageGStreamer(); + + PassRefPtr<BitmapImage> image() + { + ASSERT(m_image); + return m_image.get(); + } + + private: + RefPtr<BitmapImage> m_image; + +#if PLATFORM(CAIRO) + ImageGStreamer(GstBuffer*&, IntSize, cairo_format_t&); + cairo_surface_t* m_surface; +#endif + + }; +} + +#endif + +#endif diff --git a/WebCore/platform/graphics/gstreamer/ImageGStreamerCairo.cpp b/WebCore/platform/graphics/gstreamer/ImageGStreamerCairo.cpp new file mode 100644 index 0000000..6f975a4 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/ImageGStreamerCairo.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 Igalia S.L + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "config.h" + +#include "GOwnPtr.h" +#include "ImageGStreamer.h" + +using namespace std; + +using namespace WebCore; + +PassRefPtr<ImageGStreamer> ImageGStreamer::createImage(GstBuffer* buffer) +{ + int width = 0, height = 0; + GstCaps* caps = gst_buffer_get_caps(buffer); + GstVideoFormat format; + if (!gst_video_format_parse_caps(caps, &format, &width, &height)) { + gst_caps_unref(caps); + return 0; + } + + gst_caps_unref(caps); + + cairo_format_t cairoFormat; + if (format == GST_VIDEO_FORMAT_ARGB || format == GST_VIDEO_FORMAT_BGRA) + cairoFormat = CAIRO_FORMAT_ARGB32; + else + cairoFormat = CAIRO_FORMAT_RGB24; + + return adoptRef(new ImageGStreamer(buffer, IntSize(width, height), cairoFormat)); +} + +ImageGStreamer::ImageGStreamer(GstBuffer*& buffer, IntSize size, cairo_format_t& cairoFormat) + : m_image(0) + , m_surface(0) +{ + m_surface = cairo_image_surface_create_for_data(GST_BUFFER_DATA(buffer), cairoFormat, + size.width(), size.height(), + cairo_format_stride_for_width(cairoFormat, size.width())); + ASSERT(cairo_surface_status(m_surface) == CAIRO_STATUS_SUCCESS); + m_image = BitmapImage::create(m_surface); +} + +ImageGStreamer::~ImageGStreamer() +{ + if (m_image) + m_image.clear(); + + m_image = 0; + + if (m_surface && cairo_surface_get_reference_count(m_surface)) + cairo_surface_destroy(m_surface); + + m_surface = 0; +} diff --git a/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp b/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp new file mode 100644 index 0000000..0afb971 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.cpp @@ -0,0 +1,1407 @@ +/* + * Copyright (C) 2007, 2009 Apple Inc. All rights reserved. + * Copyright (C) 2007 Collabora Ltd. All rights reserved. + * Copyright (C) 2007 Alp Toker <alp@atoker.com> + * Copyright (C) 2009 Gustavo Noronha Silva <gns@gnome.org> + * Copyright (C) 2009, 2010 Igalia S.L + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * aint with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "config.h" + +#if ENABLE(VIDEO) + +#include "MediaPlayerPrivateGStreamer.h" + + +#include "ColorSpace.h" +#include "DataSourceGStreamer.h" +#include "Document.h" +#include "Frame.h" +#include "FrameView.h" +#include "GOwnPtrGStreamer.h" +#include "GraphicsContext.h" +#include "GraphicsTypes.h" +#include "ImageGStreamer.h" +#include "IntRect.h" +#include "KURL.h" +#include "MIMETypeRegistry.h" +#include "MediaPlayer.h" +#include "NotImplemented.h" +#include "ScrollView.h" +#include "SecurityOrigin.h" +#include "TimeRanges.h" +#include "VideoSinkGStreamer.h" +#include "WebKitWebSourceGStreamer.h" +#include "Widget.h" +#include <wtf/text/CString.h> + +#include <GOwnPtr.h> +#include <gst/gst.h> +#include <gst/interfaces/mixer.h> +#include <gst/interfaces/xoverlay.h> +#include <gst/video/video.h> +#include <limits> +#include <math.h> + +// GstPlayFlags flags from playbin2. It is the policy of GStreamer to +// not publicly expose element-specific enums. That's why this +// GstPlayFlags enum has been copied here. +typedef enum { + GST_PLAY_FLAG_VIDEO = 0x00000001, + GST_PLAY_FLAG_AUDIO = 0x00000002, + GST_PLAY_FLAG_TEXT = 0x00000004, + GST_PLAY_FLAG_VIS = 0x00000008, + GST_PLAY_FLAG_SOFT_VOLUME = 0x00000010, + GST_PLAY_FLAG_NATIVE_AUDIO = 0x00000020, + GST_PLAY_FLAG_NATIVE_VIDEO = 0x00000040, + GST_PLAY_FLAG_DOWNLOAD = 0x00000080, + GST_PLAY_FLAG_BUFFERING = 0x000000100 +} GstPlayFlags; + +using namespace std; + +namespace WebCore { + +static int greatestCommonDivisor(int a, int b) +{ + while (b) { + int temp = a; + a = b; + b = temp % b; + } + + return ABS(a); +} + +gboolean mediaPlayerPrivateMessageCallback(GstBus* bus, GstMessage* message, gpointer data) +{ + GOwnPtr<GError> err; + GOwnPtr<gchar> debug; + MediaPlayer::NetworkState error; + MediaPlayerPrivateGStreamer* mp = reinterpret_cast<MediaPlayerPrivateGStreamer*>(data); + bool issueError = true; + bool attemptNextLocation = false; + GstElement* pipeline = mp->pipeline(); + + if (message->structure) { + const gchar* messageTypeName = gst_structure_get_name(message->structure); + + // Redirect messages are sent from elements, like qtdemux, to + // notify of the new location(s) of the media. + if (!g_strcmp0(messageTypeName, "redirect")) { + mp->mediaLocationChanged(message); + return true; + } + } + + switch (GST_MESSAGE_TYPE(message)) { + case GST_MESSAGE_ERROR: + if (mp && mp->pipelineReset()) + break; + gst_message_parse_error(message, &err.outPtr(), &debug.outPtr()); + LOG_VERBOSE(Media, "Error: %d, %s", err->code, err->message); + + error = MediaPlayer::Empty; + if (err->code == GST_STREAM_ERROR_CODEC_NOT_FOUND + || err->code == GST_STREAM_ERROR_WRONG_TYPE + || err->code == GST_STREAM_ERROR_FAILED + || err->code == GST_CORE_ERROR_MISSING_PLUGIN + || err->code == GST_RESOURCE_ERROR_NOT_FOUND) + error = MediaPlayer::FormatError; + else if (err->domain == GST_STREAM_ERROR) { + error = MediaPlayer::DecodeError; + attemptNextLocation = true; + } else if (err->domain == GST_RESOURCE_ERROR) + error = MediaPlayer::NetworkError; + + if (mp) { + if (attemptNextLocation) + issueError = !mp->loadNextLocation(); + if (issueError) + mp->loadingFailed(error); + } + break; + case GST_MESSAGE_EOS: + LOG_VERBOSE(Media, "End of Stream"); + mp->didEnd(); + break; + case GST_MESSAGE_STATE_CHANGED: + // Ignore state changes from internal elements. They are + // forwarded to playbin2 anyway. + if (GST_MESSAGE_SRC(message) == reinterpret_cast<GstObject*>(pipeline)) + mp->updateStates(); + break; + case GST_MESSAGE_BUFFERING: + mp->processBufferingStats(message); + break; + case GST_MESSAGE_DURATION: + LOG_VERBOSE(Media, "Duration changed"); + mp->durationChanged(); + break; + default: + LOG_VERBOSE(Media, "Unhandled GStreamer message type: %s", + GST_MESSAGE_TYPE_NAME(message)); + break; + } + return true; +} + +void mediaPlayerPrivateSourceChangedCallback(GObject *object, GParamSpec *pspec, gpointer data) +{ + MediaPlayerPrivateGStreamer* mp = reinterpret_cast<MediaPlayerPrivateGStreamer*>(data); + GOwnPtr<GstElement> element; + + g_object_get(mp->m_playBin, "source", &element.outPtr(), NULL); + gst_object_replace((GstObject**) &mp->m_source, (GstObject*) element.get()); + + if (WEBKIT_IS_WEB_SRC(element.get())) { + Frame* frame = mp->m_player->frameView() ? mp->m_player->frameView()->frame() : 0; + + if (frame) + webKitWebSrcSetFrame(WEBKIT_WEB_SRC(element.get()), frame); + } +} + +void mediaPlayerPrivateVolumeChangedCallback(GObject *element, GParamSpec *pspec, gpointer data) +{ + // This is called when playbin receives the notify::volume signal. + MediaPlayerPrivateGStreamer* mp = reinterpret_cast<MediaPlayerPrivateGStreamer*>(data); + mp->volumeChanged(); +} + +void mediaPlayerPrivateMuteChangedCallback(GObject *element, GParamSpec *pspec, gpointer data) +{ + // This is called when playbin receives the notify::mute signal. + MediaPlayerPrivateGStreamer* mp = reinterpret_cast<MediaPlayerPrivateGStreamer*>(data); + mp->muteChanged(); +} + +static float playbackPosition(GstElement* playbin) +{ + + float ret = 0.0; + + GstQuery* query = gst_query_new_position(GST_FORMAT_TIME); + if (!gst_element_query(playbin, query)) { + LOG_VERBOSE(Media, "Position query failed..."); + gst_query_unref(query); + return ret; + } + + gint64 position; + gst_query_parse_position(query, 0, &position); + + // Position is available only if the pipeline is not in GST_STATE_NULL or + // GST_STATE_READY state. + if (position != static_cast<gint64>(GST_CLOCK_TIME_NONE)) + ret = static_cast<float>(position) / static_cast<float>(GST_SECOND); + + LOG_VERBOSE(Media, "Position %" GST_TIME_FORMAT, GST_TIME_ARGS(position)); + + gst_query_unref(query); + + return ret; +} + + +void mediaPlayerPrivateRepaintCallback(WebKitVideoSink*, GstBuffer *buffer, MediaPlayerPrivateGStreamer* playerPrivate) +{ + g_return_if_fail(GST_IS_BUFFER(buffer)); + gst_buffer_replace(&playerPrivate->m_buffer, buffer); + playerPrivate->repaint(); +} + +MediaPlayerPrivateInterface* MediaPlayerPrivateGStreamer::create(MediaPlayer* player) +{ + return new MediaPlayerPrivateGStreamer(player); +} + +void MediaPlayerPrivateGStreamer::registerMediaEngine(MediaEngineRegistrar registrar) +{ + if (isAvailable()) + registrar(create, getSupportedTypes, supportsType); +} + +static bool gstInitialized = false; + +static bool doGstInit() +{ + // FIXME: We should pass the arguments from the command line + if (!gstInitialized) { + GOwnPtr<GError> error; + gstInitialized = gst_init_check(0, 0, &error.outPtr()); + if (!gstInitialized) { + LOG_VERBOSE(Media, "Could not initialize GStreamer: %s", + error ? error->message : "unknown error occurred"); + } else { + gst_element_register(0, "webkitmediasrc", GST_RANK_PRIMARY, + WEBKIT_TYPE_DATA_SRC); + gst_element_register(0, "webkitwebsrc", GST_RANK_PRIMARY + 100, + WEBKIT_TYPE_WEB_SRC); + } + + } + return gstInitialized; +} + +bool MediaPlayerPrivateGStreamer::isAvailable() +{ + if (!doGstInit()) + return false; + + GstElementFactory* factory = gst_element_factory_find("playbin2"); + if (factory) { + gst_object_unref(GST_OBJECT(factory)); + return true; + } + return false; +} + +MediaPlayerPrivateGStreamer::MediaPlayerPrivateGStreamer(MediaPlayer* player) + : m_player(player) + , m_playBin(0) + , m_videoSink(0) + , m_fpsSink(0) + , m_source(0) + , m_seekTime(0) + , m_changingRate(false) + , m_endTime(numeric_limits<float>::infinity()) + , m_networkState(MediaPlayer::Empty) + , m_readyState(MediaPlayer::HaveNothing) + , m_isStreaming(false) + , m_size(IntSize()) + , m_buffer(0) + , m_mediaLocations(0) + , m_mediaLocationCurrentIndex(0) + , m_resetPipeline(false) + , m_paused(true) + , m_seeking(false) + , m_buffering(false) + , m_playbackRate(1) + , m_errorOccured(false) + , m_mediaDuration(0) + , m_startedBuffering(false) + , m_fillTimer(this, &MediaPlayerPrivateGStreamer::fillTimerFired) + , m_maxTimeLoaded(0) + , m_bufferingPercentage(0) + , m_preload(MediaPlayer::Auto) + , m_delayingLoad(false) + , m_mediaDurationKnown(true) +{ + if (doGstInit()) + createGSTPlayBin(); +} + +MediaPlayerPrivateGStreamer::~MediaPlayerPrivateGStreamer() +{ + if (m_fillTimer.isActive()) + m_fillTimer.stop(); + + if (m_buffer) + gst_buffer_unref(m_buffer); + m_buffer = 0; + + if (m_mediaLocations) { + gst_structure_free(m_mediaLocations); + m_mediaLocations = 0; + } + + if (m_source) { + gst_object_unref(m_source); + m_source = 0; + } + + if (m_playBin) { + gst_element_set_state(m_playBin, GST_STATE_NULL); + gst_object_unref(GST_OBJECT(m_playBin)); + } + + if (m_videoSink) { + g_object_unref(m_videoSink); + m_videoSink = 0; + } + + if (m_fpsSink) { + g_object_unref(m_fpsSink); + m_fpsSink = 0; + } +} + +void MediaPlayerPrivateGStreamer::load(const String& url) +{ + g_object_set(m_playBin, "uri", url.utf8().data(), NULL); + + LOG_VERBOSE(Media, "Load %s", url.utf8().data()); + + if (m_preload == MediaPlayer::None) { + LOG_VERBOSE(Media, "Delaying load."); + m_delayingLoad = true; + return; + } + + commitLoad(); +} + +void MediaPlayerPrivateGStreamer::commitLoad() +{ + // GStreamer needs to have the pipeline set to a paused state to + // start providing anything useful. + gst_element_set_state(m_playBin, GST_STATE_PAUSED); + + LOG_VERBOSE(Media, "Committing load."); + if (m_networkState != MediaPlayer::Loading) { + m_networkState = MediaPlayer::Loading; + m_player->networkStateChanged(); + } + if (m_readyState != MediaPlayer::HaveNothing) { + m_readyState = MediaPlayer::HaveNothing; + m_player->readyStateChanged(); + } +} + +bool MediaPlayerPrivateGStreamer::changePipelineState(GstState newState) +{ + ASSERT(newState == GST_STATE_PLAYING || newState == GST_STATE_PAUSED); + + GstState currentState; + GstState pending; + + gst_element_get_state(m_playBin, ¤tState, &pending, 0); + if (currentState != newState && pending != newState) { + GstStateChangeReturn ret = gst_element_set_state(m_playBin, newState); + GstState pausedOrPlaying = newState == GST_STATE_PLAYING ? GST_STATE_PAUSED : GST_STATE_PLAYING; + if (currentState != pausedOrPlaying && ret == GST_STATE_CHANGE_FAILURE) { + loadingFailed(MediaPlayer::Empty); + return false; + } + } + return true; +} + +void MediaPlayerPrivateGStreamer::prepareToPlay() +{ + if (m_delayingLoad) { + m_delayingLoad = false; + commitLoad(); + } +} + +void MediaPlayerPrivateGStreamer::play() +{ + if (changePipelineState(GST_STATE_PLAYING)) + LOG_VERBOSE(Media, "Play"); +} + +void MediaPlayerPrivateGStreamer::pause() +{ + if (changePipelineState(GST_STATE_PAUSED)) + LOG_VERBOSE(Media, "Pause"); +} + +float MediaPlayerPrivateGStreamer::duration() const +{ + if (!m_playBin) + return 0.0; + + if (m_errorOccured) + return 0.0; + + // Media duration query failed already, don't attempt new useless queries. + if (!m_mediaDurationKnown) + return numeric_limits<float>::infinity(); + + if (m_mediaDuration) + return m_mediaDuration; + + GstFormat timeFormat = GST_FORMAT_TIME; + gint64 timeLength = 0; + + if (!gst_element_query_duration(m_playBin, &timeFormat, &timeLength) || timeFormat != GST_FORMAT_TIME || static_cast<guint64>(timeLength) == GST_CLOCK_TIME_NONE) { + LOG_VERBOSE(Media, "Time duration query failed."); + return numeric_limits<float>::infinity(); + } + + LOG_VERBOSE(Media, "Duration: %" GST_TIME_FORMAT, GST_TIME_ARGS(timeLength)); + + return (float) ((guint64) timeLength / 1000000000.0); + // FIXME: handle 3.14.9.5 properly +} + +float MediaPlayerPrivateGStreamer::currentTime() const +{ + if (!m_playBin) + return 0; + + if (m_errorOccured) + return 0; + + if (m_seeking) + return m_seekTime; + + return playbackPosition(m_playBin); + +} + +void MediaPlayerPrivateGStreamer::seek(float time) +{ + // Avoid useless seeking. + if (time == playbackPosition(m_playBin)) + return; + + if (!m_playBin) + return; + + if (m_errorOccured) + return; + + GstClockTime sec = (GstClockTime)(time * GST_SECOND); + LOG_VERBOSE(Media, "Seek: %" GST_TIME_FORMAT, GST_TIME_ARGS(sec)); + if (!gst_element_seek(m_playBin, m_player->rate(), + GST_FORMAT_TIME, + (GstSeekFlags)(GST_SEEK_FLAG_FLUSH), + GST_SEEK_TYPE_SET, sec, + GST_SEEK_TYPE_NONE, GST_CLOCK_TIME_NONE)) + LOG_VERBOSE(Media, "Seek to %f failed", time); + else { + m_seeking = true; + m_seekTime = sec; + } +} + +void MediaPlayerPrivateGStreamer::startEndPointTimerIfNeeded() +{ + notImplemented(); +} + +void MediaPlayerPrivateGStreamer::cancelSeek() +{ + notImplemented(); +} + +void MediaPlayerPrivateGStreamer::endPointTimerFired(Timer<MediaPlayerPrivateGStreamer>*) +{ + notImplemented(); +} + +bool MediaPlayerPrivateGStreamer::paused() const +{ + return m_paused; +} + +bool MediaPlayerPrivateGStreamer::seeking() const +{ + return m_seeking; +} + +// Returns the size of the video +IntSize MediaPlayerPrivateGStreamer::naturalSize() const +{ + if (!hasVideo()) + return IntSize(); + + GstPad* pad = gst_element_get_static_pad(m_videoSink, "sink"); + if (!pad) + return IntSize(); + + int width = 0, height = 0; + GstCaps* caps = GST_PAD_CAPS(pad); + int pixelAspectRatioNumerator, pixelAspectRatioDenominator; + int displayWidth, displayHeight, displayAspectRatioGCD; + int originalWidth = 0, originalHeight = 0; + + // TODO: handle possible clean aperture data. See + // https://bugzilla.gnome.org/show_bug.cgi?id=596571 + // TODO: handle possible transformation matrix. See + // https://bugzilla.gnome.org/show_bug.cgi?id=596326 + + // Get the video PAR and original size. + if (!GST_IS_CAPS(caps) || !gst_caps_is_fixed(caps) + || !gst_video_format_parse_caps(caps, 0, &originalWidth, &originalHeight) + || !gst_video_parse_caps_pixel_aspect_ratio(caps, &pixelAspectRatioNumerator, + &pixelAspectRatioDenominator)) { + gst_object_unref(GST_OBJECT(pad)); + return IntSize(); + } + + gst_object_unref(GST_OBJECT(pad)); + + LOG_VERBOSE(Media, "Original video size: %dx%d", originalWidth, originalHeight); + LOG_VERBOSE(Media, "Pixel aspect ratio: %d/%d", pixelAspectRatioNumerator, pixelAspectRatioDenominator); + + // Calculate DAR based on PAR and video size. + displayWidth = originalWidth * pixelAspectRatioNumerator; + displayHeight = originalHeight * pixelAspectRatioDenominator; + + // Divide display width and height by their GCD to avoid possible overflows. + displayAspectRatioGCD = greatestCommonDivisor(displayWidth, displayHeight); + displayWidth /= displayAspectRatioGCD; + displayHeight /= displayAspectRatioGCD; + + // Apply DAR to original video size. This is the same behavior as in xvimagesink's setcaps function. + if (!(originalHeight % displayHeight)) { + LOG_VERBOSE(Media, "Keeping video original height"); + width = gst_util_uint64_scale_int(originalHeight, displayWidth, displayHeight); + height = originalHeight; + } else if (!(originalWidth % displayWidth)) { + LOG_VERBOSE(Media, "Keeping video original width"); + height = gst_util_uint64_scale_int(originalWidth, displayHeight, displayWidth); + width = originalWidth; + } else { + LOG_VERBOSE(Media, "Approximating while keeping original video height"); + width = gst_util_uint64_scale_int(originalHeight, displayWidth, displayHeight); + height = originalHeight; + } + + LOG_VERBOSE(Media, "Natural size: %dx%d", width, height); + return IntSize(width, height); +} + +bool MediaPlayerPrivateGStreamer::hasVideo() const +{ + gint currentVideo = -1; + if (m_playBin) + g_object_get(m_playBin, "current-video", ¤tVideo, NULL); + return currentVideo > -1; +} + +bool MediaPlayerPrivateGStreamer::hasAudio() const +{ + gint currentAudio = -1; + if (m_playBin) + g_object_get(m_playBin, "current-audio", ¤tAudio, NULL); + return currentAudio > -1; +} + +void MediaPlayerPrivateGStreamer::setVolume(float volume) +{ + if (!m_playBin) + return; + + g_object_set(m_playBin, "volume", static_cast<double>(volume), NULL); +} + +void MediaPlayerPrivateGStreamer::volumeChangedTimerFired(Timer<MediaPlayerPrivateGStreamer>*) +{ + double volume; + g_object_get(m_playBin, "volume", &volume, NULL); + m_player->volumeChanged(static_cast<float>(volume)); +} + +void MediaPlayerPrivateGStreamer::volumeChanged() +{ + Timer<MediaPlayerPrivateGStreamer> volumeChangedTimer(this, &MediaPlayerPrivateGStreamer::volumeChangedTimerFired); + volumeChangedTimer.startOneShot(0); +} + +void MediaPlayerPrivateGStreamer::setRate(float rate) +{ + // Avoid useless playback rate update. + if (m_playbackRate == rate) + return; + + GstState state; + GstState pending; + + gst_element_get_state(m_playBin, &state, &pending, 0); + if ((state != GST_STATE_PLAYING && state != GST_STATE_PAUSED) + || (pending == GST_STATE_PAUSED)) + return; + + if (m_isStreaming) + return; + + m_playbackRate = rate; + m_changingRate = true; + float currentPosition = playbackPosition(m_playBin) * GST_SECOND; + GstSeekFlags flags = (GstSeekFlags)(GST_SEEK_FLAG_FLUSH); + gint64 start, end; + bool mute = false; + + LOG_VERBOSE(Media, "Set Rate to %f", rate); + if (rate >= 0) { + // Mute the sound if the playback rate is too extreme. + // TODO: in other cases we should perform pitch adjustments. + mute = (bool) (rate < 0.8 || rate > 2); + start = currentPosition; + end = GST_CLOCK_TIME_NONE; + } else { + start = 0; + mute = true; + + // If we are at beginning of media, start from the end to + // avoid immediate EOS. + if (currentPosition <= 0) + end = duration() * GST_SECOND; + else + end = currentPosition; + } + + LOG_VERBOSE(Media, "Need to mute audio: %d", (int) mute); + + if (!gst_element_seek(m_playBin, rate, GST_FORMAT_TIME, flags, + GST_SEEK_TYPE_SET, start, + GST_SEEK_TYPE_SET, end)) + LOG_VERBOSE(Media, "Set rate to %f failed", rate); + else + g_object_set(m_playBin, "mute", mute, NULL); +} + +MediaPlayer::NetworkState MediaPlayerPrivateGStreamer::networkState() const +{ + return m_networkState; +} + +MediaPlayer::ReadyState MediaPlayerPrivateGStreamer::readyState() const +{ + return m_readyState; +} + +PassRefPtr<TimeRanges> MediaPlayerPrivateGStreamer::buffered() const +{ + RefPtr<TimeRanges> timeRanges = TimeRanges::create(); + float loaded = maxTimeLoaded(); + if (!m_errorOccured && !m_isStreaming && loaded > 0) + timeRanges->add(0, loaded); + return timeRanges.release(); +} + +void MediaPlayerPrivateGStreamer::processBufferingStats(GstMessage* message) +{ + // This is the immediate buffering that needs to happen so we have + // enough to play right now. + m_buffering = true; + const GstStructure *structure = gst_message_get_structure(message); + gst_structure_get_int(structure, "buffer-percent", &m_bufferingPercentage); + + LOG_VERBOSE(Media, "[Buffering] Buffering: %d%%.", m_bufferingPercentage); + + GstBufferingMode mode; + gst_message_parse_buffering_stats(message, &mode, 0, 0, 0); + if (mode != GST_BUFFERING_DOWNLOAD) { + updateStates(); + return; + } + + // This is on-disk buffering, that allows us to download much more + // than needed for right now. + if (!m_startedBuffering) { + LOG_VERBOSE(Media, "[Buffering] Starting on-disk buffering."); + + m_startedBuffering = true; + + if (m_fillTimer.isActive()) + m_fillTimer.stop(); + + m_fillTimer.startRepeating(0.2); + } +} + +void MediaPlayerPrivateGStreamer::fillTimerFired(Timer<MediaPlayerPrivateGStreamer>*) +{ + GstQuery* query = gst_query_new_buffering(GST_FORMAT_PERCENT); + + if (!gst_element_query(m_playBin, query)) { + gst_query_unref(query); + return; + } + + gint64 start, stop; + gdouble fillStatus = 100.0; + + gst_query_parse_buffering_range(query, 0, &start, &stop, 0); + gst_query_unref(query); + + if (stop != -1) + fillStatus = 100.0 * stop / GST_FORMAT_PERCENT_MAX; + + LOG_VERBOSE(Media, "[Buffering] Download buffer filled up to %f%%", fillStatus); + + if (!m_mediaDuration) + durationChanged(); + + // Update maxTimeLoaded only if the media duration is + // available. Otherwise we can't compute it. + if (m_mediaDuration) { + if (fillStatus == 100.0) + m_maxTimeLoaded = m_mediaDuration; + else + m_maxTimeLoaded = static_cast<float>((fillStatus * m_mediaDuration) / 100.0); + LOG_VERBOSE(Media, "[Buffering] Updated maxTimeLoaded: %f", m_maxTimeLoaded); + } + + if (fillStatus != 100.0) { + updateStates(); + return; + } + + // Media is now fully loaded. It will play even if network + // connection is cut. Buffering is done, remove the fill source + // from the main loop. + m_fillTimer.stop(); + m_startedBuffering = false; + updateStates(); +} + +float MediaPlayerPrivateGStreamer::maxTimeSeekable() const +{ + if (m_errorOccured) + return 0.0; + + LOG_VERBOSE(Media, "maxTimeSeekable"); + // infinite duration means live stream + if (isinf(duration())) + return 0.0; + + return maxTimeLoaded(); +} + +float MediaPlayerPrivateGStreamer::maxTimeLoaded() const +{ + if (m_errorOccured) + return 0.0; + + float loaded = m_maxTimeLoaded; + if (!loaded && !m_fillTimer.isActive()) + loaded = duration(); + LOG_VERBOSE(Media, "maxTimeLoaded: %f", loaded); + return loaded; +} + +unsigned MediaPlayerPrivateGStreamer::bytesLoaded() const +{ + if (!m_playBin) + return 0; + + if (!m_mediaDuration) + return 0; + + unsigned loaded = totalBytes() * maxTimeLoaded() / m_mediaDuration; + LOG_VERBOSE(Media, "bytesLoaded: %d", loaded); + return loaded; +} + +unsigned MediaPlayerPrivateGStreamer::totalBytes() const +{ + if (!m_source) + return 0; + + if (m_errorOccured) + return 0; + + GstFormat fmt = GST_FORMAT_BYTES; + gint64 length = 0; + gst_element_query_duration(m_source, &fmt, &length); + LOG_VERBOSE(Media, "totalBytes %" G_GINT64_FORMAT, length); + + return length; +} + +void MediaPlayerPrivateGStreamer::cancelLoad() +{ + if (m_networkState < MediaPlayer::Loading || m_networkState == MediaPlayer::Loaded) + return; + + if (m_playBin) + gst_element_set_state(m_playBin, GST_STATE_NULL); +} + +void MediaPlayerPrivateGStreamer::updateStates() +{ + if (!m_playBin) + return; + + if (m_errorOccured) + return; + + MediaPlayer::NetworkState oldNetworkState = m_networkState; + MediaPlayer::ReadyState oldReadyState = m_readyState; + GstState state; + GstState pending; + + GstStateChangeReturn ret = gst_element_get_state(m_playBin, + &state, &pending, 250 * GST_NSECOND); + + bool shouldUpdateAfterSeek = false; + switch (ret) { + case GST_STATE_CHANGE_SUCCESS: + LOG_VERBOSE(Media, "State: %s, pending: %s", + gst_element_state_get_name(state), + gst_element_state_get_name(pending)); + + m_resetPipeline = state <= GST_STATE_READY; + + // Try to figure out ready and network states. + if (state == GST_STATE_READY) { + m_readyState = MediaPlayer::HaveNothing; + m_networkState = MediaPlayer::Empty; + } else if (maxTimeLoaded() == duration()) { + m_networkState = MediaPlayer::Loaded; + m_readyState = MediaPlayer::HaveEnoughData; + } else { + m_readyState = currentTime() < maxTimeLoaded() ? MediaPlayer::HaveFutureData : MediaPlayer::HaveCurrentData; + m_networkState = MediaPlayer::Loading; + } + + if (m_buffering && state != GST_STATE_READY) { + m_readyState = MediaPlayer::HaveCurrentData; + m_networkState = MediaPlayer::Loading; + } + + // Now let's try to get the states in more detail using + // information from GStreamer, while we sync states where + // needed. + if (state == GST_STATE_PAUSED) { + if (m_buffering && m_bufferingPercentage == 100) { + m_buffering = false; + m_bufferingPercentage = 0; + m_readyState = MediaPlayer::HaveEnoughData; + + LOG_VERBOSE(Media, "[Buffering] Complete."); + + if (!m_paused) { + LOG_VERBOSE(Media, "[Buffering] Restarting playback."); + gst_element_set_state(m_playBin, GST_STATE_PLAYING); + } + } else if (!m_buffering && (currentTime() < duration())) { + m_paused = true; + } + } else if (state == GST_STATE_PLAYING) { + m_readyState = MediaPlayer::HaveEnoughData; + m_paused = false; + + if (!m_mediaDuration) { + float newDuration = duration(); + m_mediaDurationKnown = !isinf(newDuration); + if (m_mediaDurationKnown) + m_mediaDuration = newDuration; + } + + if (m_buffering) { + m_readyState = MediaPlayer::HaveCurrentData; + m_networkState = MediaPlayer::Loading; + + LOG_VERBOSE(Media, "[Buffering] Pausing stream for buffering."); + + gst_element_set_state(m_playBin, GST_STATE_PAUSED); + } + } else + m_paused = true; + + // Is on-disk buffering in progress? + if (m_fillTimer.isActive()) + m_networkState = MediaPlayer::Loading; + + if (m_changingRate) { + m_player->rateChanged(); + m_changingRate = false; + } + + if (m_seeking) { + shouldUpdateAfterSeek = true; + m_seeking = false; + } + + break; + case GST_STATE_CHANGE_ASYNC: + LOG_VERBOSE(Media, "Async: State: %s, pending: %s", + gst_element_state_get_name(state), + gst_element_state_get_name(pending)); + // Change in progress + + if (!m_isStreaming) + return; + + // Resume playback if a seek was performed in a live pipeline. + if (m_seeking) { + shouldUpdateAfterSeek = true; + m_seeking = false; + if (m_paused) + gst_element_set_state(m_playBin, GST_STATE_PLAYING); + } + break; + case GST_STATE_CHANGE_FAILURE: + LOG_VERBOSE(Media, "Failure: State: %s, pending: %s", + gst_element_state_get_name(state), + gst_element_state_get_name(pending)); + // Change failed + return; + case GST_STATE_CHANGE_NO_PREROLL: + LOG_VERBOSE(Media, "No preroll: State: %s, pending: %s", + gst_element_state_get_name(state), + gst_element_state_get_name(pending)); + + if (state == GST_STATE_READY) + m_readyState = MediaPlayer::HaveNothing; + else if (state == GST_STATE_PAUSED) { + m_readyState = MediaPlayer::HaveEnoughData; + m_paused = true; + // Live pipelines go in PAUSED without prerolling. + m_isStreaming = true; + } else if (state == GST_STATE_PLAYING) + m_paused = false; + + if (m_seeking) { + shouldUpdateAfterSeek = true; + m_seeking = false; + if (!m_paused) + gst_element_set_state(m_playBin, GST_STATE_PLAYING); + } else if (!m_paused) + gst_element_set_state(m_playBin, GST_STATE_PLAYING); + + m_networkState = MediaPlayer::Loading; + break; + default: + LOG_VERBOSE(Media, "Else : %d", ret); + break; + } + + if (seeking()) + m_readyState = MediaPlayer::HaveNothing; + + if (shouldUpdateAfterSeek) + timeChanged(); + + if (m_networkState != oldNetworkState) { + LOG_VERBOSE(Media, "Network State Changed from %u to %u", + oldNetworkState, m_networkState); + m_player->networkStateChanged(); + } + if (m_readyState != oldReadyState) { + LOG_VERBOSE(Media, "Ready State Changed from %u to %u", + oldReadyState, m_readyState); + m_player->readyStateChanged(); + } +} + +void MediaPlayerPrivateGStreamer::mediaLocationChanged(GstMessage* message) +{ + if (m_mediaLocations) + gst_structure_free(m_mediaLocations); + + if (message->structure) { + // This structure can contain: + // - both a new-location string and embedded locations structure + // - or only a new-location string. + m_mediaLocations = gst_structure_copy(message->structure); + const GValue* locations = gst_structure_get_value(m_mediaLocations, "locations"); + + if (locations) + m_mediaLocationCurrentIndex = static_cast<int>(gst_value_list_get_size(locations)) -1; + + loadNextLocation(); + } +} + +bool MediaPlayerPrivateGStreamer::loadNextLocation() +{ + if (!m_mediaLocations) + return false; + + const GValue* locations = gst_structure_get_value(m_mediaLocations, "locations"); + const gchar* newLocation = 0; + + if (!locations) { + // Fallback on new-location string. + newLocation = gst_structure_get_string(m_mediaLocations, "new-location"); + if (!newLocation) + return false; + } + + if (!newLocation) { + if (m_mediaLocationCurrentIndex < 0) { + m_mediaLocations = 0; + return false; + } + + const GValue* location = gst_value_list_get_value(locations, + m_mediaLocationCurrentIndex); + const GstStructure* structure = gst_value_get_structure(location); + + if (!structure) { + m_mediaLocationCurrentIndex--; + return false; + } + + newLocation = gst_structure_get_string(structure, "new-location"); + } + + if (newLocation) { + // Found a candidate. new-location is not always an absolute url + // though. We need to take the base of the current url and + // append the value of new-location to it. + + gchar* currentLocation = 0; + g_object_get(m_playBin, "uri", ¤tLocation, NULL); + + KURL currentUrl(KURL(), currentLocation); + g_free(currentLocation); + + KURL newUrl; + + if (gst_uri_is_valid(newLocation)) + newUrl = KURL(KURL(), newLocation); + else + newUrl = KURL(KURL(), currentUrl.baseAsString() + newLocation); + + RefPtr<SecurityOrigin> securityOrigin = SecurityOrigin::create(currentUrl); + if (securityOrigin->canRequest(newUrl)) { + LOG_VERBOSE(Media, "New media url: %s", newUrl.string().utf8().data()); + + // Reset player states. + m_networkState = MediaPlayer::Loading; + m_player->networkStateChanged(); + m_readyState = MediaPlayer::HaveNothing; + m_player->readyStateChanged(); + + // Reset pipeline state. + m_resetPipeline = true; + gst_element_set_state(m_playBin, GST_STATE_READY); + + GstState state; + gst_element_get_state(m_playBin, &state, 0, 0); + if (state <= GST_STATE_READY) { + // Set the new uri and start playing. + g_object_set(m_playBin, "uri", newUrl.string().utf8().data(), NULL); + gst_element_set_state(m_playBin, GST_STATE_PLAYING); + return true; + } + } + } + m_mediaLocationCurrentIndex--; + return false; + +} + +void MediaPlayerPrivateGStreamer::loadStateChanged() +{ + updateStates(); +} + +void MediaPlayerPrivateGStreamer::sizeChanged() +{ + notImplemented(); +} + +void MediaPlayerPrivateGStreamer::timeChanged() +{ + updateStates(); + m_player->timeChanged(); +} + +void MediaPlayerPrivateGStreamer::didEnd() +{ + // EOS was reached but in case of reverse playback the position is + // not always 0. So to not confuse the HTMLMediaElement we + // synchronize position and duration values. + float now = currentTime(); + if (now > 0) { + m_mediaDuration = now; + m_player->durationChanged(); + } + + gst_element_set_state(m_playBin, GST_STATE_PAUSED); + + timeChanged(); +} + +void MediaPlayerPrivateGStreamer::durationChanged() +{ + // Reset cached media duration + m_mediaDuration = 0; + + // And re-cache it if possible. + GstState state; + gst_element_get_state(m_playBin, &state, 0, 0); + float newDuration = duration(); + + if (state <= GST_STATE_READY) { + // Don't set m_mediaDurationKnown yet if the pipeline is not + // paused. This allows duration() query to fail at least once + // before playback starts and duration becomes known. + if (!isinf(newDuration)) + m_mediaDuration = newDuration; + } else { + m_mediaDurationKnown = !isinf(newDuration); + if (m_mediaDurationKnown) + m_mediaDuration = newDuration; + } + + if (!isinf(newDuration)) + m_mediaDuration = newDuration; + + m_player->durationChanged(); +} + +bool MediaPlayerPrivateGStreamer::supportsMuting() const +{ + return true; +} + +void MediaPlayerPrivateGStreamer::setMuted(bool muted) +{ + if (!m_playBin) + return; + + g_object_set(m_playBin, "mute", muted, NULL); +} + +void MediaPlayerPrivateGStreamer::muteChangedTimerFired(Timer<MediaPlayerPrivateGStreamer>*) +{ + gboolean muted; + g_object_get(m_playBin, "mute", &muted, NULL); + m_player->muteChanged(static_cast<bool>(muted)); +} + +void MediaPlayerPrivateGStreamer::muteChanged() +{ + Timer<MediaPlayerPrivateGStreamer> muteChangedTimer(this, &MediaPlayerPrivateGStreamer::muteChangedTimerFired); + muteChangedTimer.startOneShot(0); +} + +void MediaPlayerPrivateGStreamer::loadingFailed(MediaPlayer::NetworkState error) +{ + m_errorOccured = true; + if (m_networkState != error) { + m_networkState = error; + m_player->networkStateChanged(); + } + if (m_readyState != MediaPlayer::HaveNothing) { + m_readyState = MediaPlayer::HaveNothing; + m_player->readyStateChanged(); + } +} + +void MediaPlayerPrivateGStreamer::setSize(const IntSize& size) +{ + m_size = size; +} + +void MediaPlayerPrivateGStreamer::setVisible(bool visible) +{ +} + +void MediaPlayerPrivateGStreamer::repaint() +{ + m_player->repaint(); +} + +void MediaPlayerPrivateGStreamer::paint(GraphicsContext* context, const IntRect& rect) +{ + if (context->paintingDisabled()) + return; + + if (!m_player->visible()) + return; + if (!m_buffer) + return; + + RefPtr<ImageGStreamer> gstImage = ImageGStreamer::createImage(m_buffer); + if (!gstImage) + return; + + context->drawImage(reinterpret_cast<Image*>(gstImage->image().get()), sRGBColorSpace, + rect, CompositeCopy, false); +} + +static HashSet<String> mimeTypeCache() +{ + + doGstInit(); + + DEFINE_STATIC_LOCAL(HashSet<String>, cache, ()); + static bool typeListInitialized = false; + + if (!typeListInitialized) { + // Build a whitelist of mime-types known to be supported by + // GStreamer. + HashSet<String> handledApplicationSubtypes; + handledApplicationSubtypes.add(String("ogg")); + handledApplicationSubtypes.add(String("x-3gp")); + handledApplicationSubtypes.add(String("vnd.rn-realmedia")); + handledApplicationSubtypes.add(String("x-pn-realaudio")); + + GList* factories = gst_type_find_factory_get_list(); + for (GList* iterator = factories; iterator; iterator = iterator->next) { + GstTypeFindFactory* factory = GST_TYPE_FIND_FACTORY(iterator->data); + GstCaps* caps = gst_type_find_factory_get_caps(factory); + + if (!caps) + continue; + + for (guint structureIndex = 0; structureIndex < gst_caps_get_size(caps); structureIndex++) { + GstStructure* structure = gst_caps_get_structure(caps, structureIndex); + const gchar* name = gst_structure_get_name(structure); + bool cached = false; + + // These formats are supported by GStreamer, but not + // correctly advertised. + if (g_str_equal(name, "video/x-h264") + || g_str_equal(name, "audio/x-m4a")) { + cache.add(String("video/mp4")); + cache.add(String("audio/aac")); + cached = true; + } + + if (g_str_equal(name, "video/x-theora")) { + cache.add(String("video/ogg")); + cached = true; + } + + if (g_str_equal(name, "audio/x-vorbis")) { + cache.add(String("audio/ogg")); + cached = true; + } + + if (g_str_equal(name, "audio/x-wav")) { + cache.add(String("audio/wav")); + cached = true; + } + + if (g_str_equal(name, "audio/mpeg")) { + cache.add(String(name)); + cached = true; + + // This is what we are handling: + // mpegversion=(int)1, layer=(int)[ 1, 3 ] + gint mpegVersion = 0; + if (gst_structure_get_int(structure, "mpegversion", &mpegVersion) && (mpegVersion == 1)) { + const GValue* layer = gst_structure_get_value(structure, "layer"); + if (G_VALUE_TYPE(layer) == GST_TYPE_INT_RANGE) { + gint minLayer = gst_value_get_int_range_min(layer); + gint maxLayer = gst_value_get_int_range_max(layer); + if (minLayer <= 1 && 1 <= maxLayer) + cache.add(String("audio/mp1")); + if (minLayer <= 2 && 2 <= maxLayer) + cache.add(String("audio/mp2")); + if (minLayer <= 3 && 3 <= maxLayer) + cache.add(String("audio/mp3")); + } + } + } + + if (!cached) { + // GStreamer plugins can be capable of supporting + // types which WebKit supports by default. In that + // case, we should not consider these types + // supportable by GStreamer. Examples of what + // GStreamer can support but should not be added: + // text/plain, text/html, image/jpeg, + // application/xml + gchar** mimetype = g_strsplit(name, "/", 2); + if (g_str_equal(mimetype[0], "audio") + || g_str_equal(mimetype[0], "video") + || (g_str_equal(mimetype[0], "application") + && handledApplicationSubtypes.contains(String(mimetype[1])))) + cache.add(String(name)); + + g_strfreev(mimetype); + } + } + } + + gst_plugin_feature_list_free(factories); + typeListInitialized = true; + } + + return cache; +} + +void MediaPlayerPrivateGStreamer::getSupportedTypes(HashSet<String>& types) +{ + types = mimeTypeCache(); +} + +MediaPlayer::SupportsType MediaPlayerPrivateGStreamer::supportsType(const String& type, const String& codecs) +{ + if (type.isNull() || type.isEmpty()) + return MediaPlayer::IsNotSupported; + + // spec says we should not return "probably" if the codecs string is empty + if (mimeTypeCache().contains(type)) + return codecs.isEmpty() ? MediaPlayer::MayBeSupported : MediaPlayer::IsSupported; + return MediaPlayer::IsNotSupported; +} + +bool MediaPlayerPrivateGStreamer::hasSingleSecurityOrigin() const +{ + return true; +} + +bool MediaPlayerPrivateGStreamer::supportsFullscreen() const +{ + return true; +} + +void MediaPlayerPrivateGStreamer::setPreload(MediaPlayer::Preload preload) +{ + ASSERT(m_playBin); + + m_preload = preload; + + GstPlayFlags flags; + g_object_get(m_playBin, "flags", &flags, NULL); + if (preload == MediaPlayer::None) + g_object_set(m_playBin, "flags", flags & ~GST_PLAY_FLAG_DOWNLOAD, NULL); + else + g_object_set(m_playBin, "flags", flags | GST_PLAY_FLAG_DOWNLOAD, NULL); + + if (m_delayingLoad && m_preload != MediaPlayer::None) { + m_delayingLoad = false; + commitLoad(); + } +} + +void MediaPlayerPrivateGStreamer::createGSTPlayBin() +{ + ASSERT(!m_playBin); + m_playBin = gst_element_factory_make("playbin2", "play"); + + GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(m_playBin)); + gst_bus_add_signal_watch(bus); + g_signal_connect(bus, "message", G_CALLBACK(mediaPlayerPrivateMessageCallback), this); + gst_object_unref(bus); + + g_signal_connect(m_playBin, "notify::volume", G_CALLBACK(mediaPlayerPrivateVolumeChangedCallback), this); + g_signal_connect(m_playBin, "notify::source", G_CALLBACK(mediaPlayerPrivateSourceChangedCallback), this); + g_signal_connect(m_playBin, "notify::mute", G_CALLBACK(mediaPlayerPrivateMuteChangedCallback), this); + + m_videoSink = webkit_video_sink_new(); + + g_object_ref_sink(m_videoSink); + + WTFLogChannel* channel = getChannelFromName("Media"); + if (channel->state == WTFLogChannelOn) { + m_fpsSink = gst_element_factory_make("fpsdisplaysink", "sink"); + if (g_object_class_find_property(G_OBJECT_GET_CLASS(m_fpsSink), "video-sink")) { + g_object_set(m_fpsSink, "video-sink", m_videoSink, NULL); + g_object_ref_sink(m_fpsSink); + g_object_set(m_playBin, "video-sink", m_fpsSink, NULL); + } else { + m_fpsSink = 0; + g_object_set(m_playBin, "video-sink", m_videoSink, NULL); + LOG_VERBOSE(Media, "Can't display FPS statistics, you need gst-plugins-bad >= 0.10.18"); + } + } else + g_object_set(m_playBin, "video-sink", m_videoSink, NULL); + + g_signal_connect(m_videoSink, "repaint-requested", G_CALLBACK(mediaPlayerPrivateRepaintCallback), this); +} + +} + +#endif diff --git a/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.h b/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.h new file mode 100644 index 0000000..06519fa --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/MediaPlayerPrivateGStreamer.h @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2007, 2009 Apple Inc. All rights reserved. + * Copyright (C) 2007 Collabora Ltd. All rights reserved. + * Copyright (C) 2007 Alp Toker <alp@atoker.com> + * Copyright (C) 2009, 2010 Igalia S.L + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * aint with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef MediaPlayerPrivateGStreamer_h +#define MediaPlayerPrivateGStreamer_h + +#if ENABLE(VIDEO) + +#include "MediaPlayerPrivate.h" +#include "Timer.h" + +#include <glib.h> +#include <gst/gst.h> + +typedef struct _WebKitVideoSink WebKitVideoSink; +typedef struct _GstBuffer GstBuffer; +typedef struct _GstMessage GstMessage; +typedef struct _GstElement GstElement; +typedef struct _GstBus GstBus; + +namespace WebCore { + +class GraphicsContext; +class IntSize; +class IntRect; +class String; + +gboolean mediaPlayerPrivateMessageCallback(GstBus* bus, GstMessage* message, gpointer data); +void mediaPlayerPrivateVolumeChangedCallback(GObject* element, GParamSpec* pspec, gpointer data); +void mediaPlayerPrivateMuteChangedCallback(GObject* element, GParamSpec* pspec, gpointer data); +void mediaPlayerPrivateSourceChangedCallback(GObject* element, GParamSpec* pspec, gpointer data); + +class MediaPlayerPrivateGStreamer : public MediaPlayerPrivateInterface { + friend gboolean mediaPlayerPrivateMessageCallback(GstBus* bus, GstMessage* message, gpointer data); + friend void mediaPlayerPrivateRepaintCallback(WebKitVideoSink*, GstBuffer* buffer, MediaPlayerPrivateGStreamer* playerPrivate); + friend void mediaPlayerPrivateSourceChangedCallback(GObject* element, GParamSpec* pspec, gpointer data); + + public: + static void registerMediaEngine(MediaEngineRegistrar); + + IntSize naturalSize() const; + bool hasVideo() const; + bool hasAudio() const; + + void load(const String &url); + void commitLoad(); + void cancelLoad(); + bool loadNextLocation(); + + void prepareToPlay(); + void play(); + void pause(); + + bool paused() const; + bool seeking() const; + + float duration() const; + float currentTime() const; + void seek(float); + + void setRate(float); + + void setVolume(float); + void volumeChanged(); + void volumeChangedTimerFired(Timer<MediaPlayerPrivateGStreamer>*); + + bool supportsMuting() const; + void setMuted(bool); + void muteChanged(); + void muteChangedTimerFired(Timer<MediaPlayerPrivateGStreamer>*); + + void setPreload(MediaPlayer::Preload); + void fillTimerFired(Timer<MediaPlayerPrivateGStreamer>*); + + MediaPlayer::NetworkState networkState() const; + MediaPlayer::ReadyState readyState() const; + + PassRefPtr<TimeRanges> buffered() const; + float maxTimeSeekable() const; + unsigned bytesLoaded() const; + unsigned totalBytes() const; + + void setVisible(bool); + void setSize(const IntSize&); + + void mediaLocationChanged(GstMessage*); + void loadStateChanged(); + void sizeChanged(); + void timeChanged(); + void didEnd(); + void durationChanged(); + void loadingFailed(MediaPlayer::NetworkState); + + void repaint(); + void paint(GraphicsContext*, const IntRect&); + + bool hasSingleSecurityOrigin() const; + + bool supportsFullscreen() const; + + GstElement* pipeline() const { return m_playBin; } + bool pipelineReset() const { return m_resetPipeline; } + + private: + MediaPlayerPrivateGStreamer(MediaPlayer*); + ~MediaPlayerPrivateGStreamer(); + + static MediaPlayerPrivateInterface* create(MediaPlayer* player); + + static void getSupportedTypes(HashSet<String>&); + static MediaPlayer::SupportsType supportsType(const String& type, const String& codecs); + static bool isAvailable(); + + void updateStates(); + void cancelSeek(); + void endPointTimerFired(Timer<MediaPlayerPrivateGStreamer>*); + float maxTimeLoaded() const; + void startEndPointTimerIfNeeded(); + + void createGSTPlayBin(); + bool changePipelineState(GstState state); + + void processBufferingStats(GstMessage* message); + + private: + MediaPlayer* m_player; + GstElement* m_playBin; + GstElement* m_videoSink; + GstElement* m_fpsSink; + GstElement* m_source; + GstClockTime m_seekTime; + bool m_changingRate; + float m_endTime; + bool m_isEndReached; + MediaPlayer::NetworkState m_networkState; + MediaPlayer::ReadyState m_readyState; + mutable bool m_isStreaming; + IntSize m_size; + GstBuffer* m_buffer; + GstStructure* m_mediaLocations; + int m_mediaLocationCurrentIndex; + bool m_resetPipeline; + bool m_paused; + bool m_seeking; + bool m_buffering; + float m_playbackRate; + bool m_errorOccured; + gfloat m_mediaDuration; + bool m_startedBuffering; + Timer<MediaPlayerPrivateGStreamer> m_fillTimer; + float m_maxTimeLoaded; + int m_bufferingPercentage; + MediaPlayer::Preload m_preload; + bool m_delayingLoad; + bool m_mediaDurationKnown; + }; +} + +#endif +#endif diff --git a/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.cpp b/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.cpp new file mode 100644 index 0000000..dd8c3ee --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.cpp @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2007 OpenedHand + * Copyright (C) 2007 Alp Toker <alp@atoker.com> + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * SECTION:webkit-video-sink + * @short_description: GStreamer video sink + * + * #WebKitVideoSink is a GStreamer sink element that triggers + * repaints in the WebKit GStreamer media player for the + * current video buffer. + */ + +#include "config.h" +#include "VideoSinkGStreamer.h" + +#include <glib.h> +#include <gst/gst.h> +#include <gst/video/video.h> + +static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE("sink", + GST_PAD_SINK, GST_PAD_ALWAYS, +// CAIRO_FORMAT_RGB24 used to render the video buffers is little/big endian dependant. +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + GST_STATIC_CAPS(GST_VIDEO_CAPS_BGRx ";" GST_VIDEO_CAPS_BGRA) +#else + GST_STATIC_CAPS(GST_VIDEO_CAPS_xRGB ";" GST_VIDEO_CAPS_ARGB) +#endif +); + +GST_DEBUG_CATEGORY_STATIC(webkit_video_sink_debug); +#define GST_CAT_DEFAULT webkit_video_sink_debug + +enum { + REPAINT_REQUESTED, + LAST_SIGNAL +}; + +enum { + PROP_0 +}; + +static guint webkit_video_sink_signals[LAST_SIGNAL] = { 0, }; + +struct _WebKitVideoSinkPrivate { + GstBuffer* buffer; + guint timeout_id; + GMutex* buffer_mutex; + GCond* data_cond; + + // If this is TRUE all processing should finish ASAP + // This is necessary because there could be a race between + // unlock() and render(), where unlock() wins, signals the + // GCond, then render() tries to render a frame although + // everything else isn't running anymore. This will lead + // to deadlocks because render() holds the stream lock. + // + // Protected by the buffer mutex + gboolean unlocked; +}; + +#define _do_init(bla) \ + GST_DEBUG_CATEGORY_INIT(webkit_video_sink_debug, \ + "webkitsink", \ + 0, \ + "webkit video sink") + +GST_BOILERPLATE_FULL(WebKitVideoSink, + webkit_video_sink, + GstVideoSink, + GST_TYPE_VIDEO_SINK, + _do_init); + +static void +webkit_video_sink_base_init(gpointer g_class) +{ + GstElementClass* element_class = GST_ELEMENT_CLASS(g_class); + + gst_element_class_add_pad_template(element_class, gst_static_pad_template_get(&sinktemplate)); + gst_element_class_set_details_simple(element_class, "WebKit video sink", + "Sink/Video", "Sends video data from a GStreamer pipeline to a Cairo surface", + "Alp Toker <alp@atoker.com>"); +} + +static void +webkit_video_sink_init(WebKitVideoSink* sink, WebKitVideoSinkClass* klass) +{ + WebKitVideoSinkPrivate* priv; + + sink->priv = priv = G_TYPE_INSTANCE_GET_PRIVATE(sink, WEBKIT_TYPE_VIDEO_SINK, WebKitVideoSinkPrivate); + priv->data_cond = g_cond_new(); + priv->buffer_mutex = g_mutex_new(); +} + +static gboolean +webkit_video_sink_timeout_func(gpointer data) +{ + WebKitVideoSink* sink = reinterpret_cast<WebKitVideoSink*>(data); + WebKitVideoSinkPrivate* priv = sink->priv; + GstBuffer* buffer; + + g_mutex_lock(priv->buffer_mutex); + buffer = priv->buffer; + priv->buffer = 0; + priv->timeout_id = 0; + + if (!buffer || priv->unlocked || G_UNLIKELY(!GST_IS_BUFFER(buffer))) { + g_cond_signal(priv->data_cond); + g_mutex_unlock(priv->buffer_mutex); + return FALSE; + } + + g_signal_emit(sink, webkit_video_sink_signals[REPAINT_REQUESTED], 0, buffer); + gst_buffer_unref(buffer); + g_cond_signal(priv->data_cond); + g_mutex_unlock(priv->buffer_mutex); + + return FALSE; +} + +static GstFlowReturn +webkit_video_sink_render(GstBaseSink* bsink, GstBuffer* buffer) +{ + WebKitVideoSink* sink = WEBKIT_VIDEO_SINK(bsink); + WebKitVideoSinkPrivate* priv = sink->priv; + + g_mutex_lock(priv->buffer_mutex); + + if (priv->unlocked) { + g_mutex_unlock(priv->buffer_mutex); + return GST_FLOW_OK; + } + + priv->buffer = gst_buffer_ref(buffer); + + // For the unlikely case where the buffer has no caps, the caps + // are implicitely the caps of the pad. This shouldn't happen. + if (G_UNLIKELY(!GST_BUFFER_CAPS(buffer))) { + buffer = priv->buffer = gst_buffer_make_metadata_writable(priv->buffer); + gst_buffer_set_caps(priv->buffer, GST_PAD_CAPS(GST_BASE_SINK_PAD(bsink))); + } + + GstCaps *caps = GST_BUFFER_CAPS(buffer); + GstVideoFormat format; + int width, height; + if (G_UNLIKELY(!gst_video_format_parse_caps(caps, &format, &width, &height))) { + gst_buffer_unref(buffer); + g_mutex_unlock(priv->buffer_mutex); + return GST_FLOW_ERROR; + } + + // Cairo's ARGB has pre-multiplied alpha while GStreamer's doesn't. + // Here we convert to Cairo's ARGB. + if (format == GST_VIDEO_FORMAT_ARGB || format == GST_VIDEO_FORMAT_BGRA) { + // Because GstBaseSink::render() only owns the buffer reference in the + // method scope we can't use gst_buffer_make_writable() here. Also + // The buffer content should not be changed here because the same buffer + // could be passed multiple times to this method (in theory) + GstBuffer *newBuffer = gst_buffer_try_new_and_alloc(GST_BUFFER_SIZE(buffer)); + + // Check if allocation failed + if (G_UNLIKELY(!newBuffer)) { + gst_buffer_unref(buffer); + g_mutex_unlock(priv->buffer_mutex); + return GST_FLOW_ERROR; + } + + gst_buffer_copy_metadata(newBuffer, buffer, (GstBufferCopyFlags) GST_BUFFER_COPY_ALL); + + // We don't use Color::premultipliedARGBFromColor() here because + // one function call per video pixel is just too expensive: + // For 720p/PAL for example this means 1280*720*25=23040000 + // function calls per second! + unsigned short alpha; + const guint8 *source = GST_BUFFER_DATA(buffer); + guint8 *destination = GST_BUFFER_DATA(newBuffer); + + for (int x = 0; x < height; x++) { + for (int y = 0; y < width; y++) { +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + alpha = source[3]; + destination[0] = (source[0] * alpha + 128) / 255; + destination[1] = (source[1] * alpha + 128) / 255; + destination[2] = (source[2] * alpha + 128) / 255; + destination[3] = alpha; +#else + alpha = source[0]; + destination[0] = alpha; + destination[1] = (source[1] * alpha + 128) / 255; + destination[2] = (source[2] * alpha + 128) / 255; + destination[3] = (source[3] * alpha + 128) / 255; +#endif + source += 4; + destination += 4; + } + } + gst_buffer_unref(buffer); + buffer = priv->buffer = newBuffer; + } + + // This should likely use a lower priority, but glib currently starves + // lower priority sources. + // See: https://bugzilla.gnome.org/show_bug.cgi?id=610830. + priv->timeout_id = g_timeout_add_full(G_PRIORITY_DEFAULT, 0, + webkit_video_sink_timeout_func, + gst_object_ref(sink), + (GDestroyNotify)gst_object_unref); + + g_cond_wait(priv->data_cond, priv->buffer_mutex); + g_mutex_unlock(priv->buffer_mutex); + return GST_FLOW_OK; +} + +static void +webkit_video_sink_dispose(GObject* object) +{ + WebKitVideoSink* sink = WEBKIT_VIDEO_SINK(object); + WebKitVideoSinkPrivate* priv = sink->priv; + + if (priv->data_cond) { + g_cond_free(priv->data_cond); + priv->data_cond = 0; + } + + if (priv->buffer_mutex) { + g_mutex_free(priv->buffer_mutex); + priv->buffer_mutex = 0; + } + + G_OBJECT_CLASS(parent_class)->dispose(object); +} + +static void +unlock_buffer_mutex(WebKitVideoSinkPrivate* priv) +{ + g_mutex_lock(priv->buffer_mutex); + + if (priv->buffer) { + gst_buffer_unref(priv->buffer); + priv->buffer = 0; + } + + priv->unlocked = TRUE; + + g_cond_signal(priv->data_cond); + g_mutex_unlock(priv->buffer_mutex); +} + +static gboolean +webkit_video_sink_unlock(GstBaseSink* object) +{ + WebKitVideoSink* sink = WEBKIT_VIDEO_SINK(object); + + unlock_buffer_mutex(sink->priv); + + return GST_CALL_PARENT_WITH_DEFAULT(GST_BASE_SINK_CLASS, unlock, + (object), TRUE); +} + +static gboolean +webkit_video_sink_unlock_stop(GstBaseSink* object) +{ + WebKitVideoSink* sink = WEBKIT_VIDEO_SINK(object); + WebKitVideoSinkPrivate* priv = sink->priv; + + g_mutex_lock(priv->buffer_mutex); + priv->unlocked = FALSE; + g_mutex_unlock(priv->buffer_mutex); + + return GST_CALL_PARENT_WITH_DEFAULT(GST_BASE_SINK_CLASS, unlock_stop, + (object), TRUE); +} + +static gboolean +webkit_video_sink_stop(GstBaseSink* base_sink) +{ + WebKitVideoSinkPrivate* priv = WEBKIT_VIDEO_SINK(base_sink)->priv; + + unlock_buffer_mutex(priv); + return TRUE; +} + +static gboolean +webkit_video_sink_start(GstBaseSink* base_sink) +{ + WebKitVideoSinkPrivate* priv = WEBKIT_VIDEO_SINK(base_sink)->priv; + + g_mutex_lock(priv->buffer_mutex); + priv->unlocked = FALSE; + g_mutex_unlock(priv->buffer_mutex); + return TRUE; +} + +static void +marshal_VOID__MINIOBJECT(GClosure * closure, GValue * return_value, + guint n_param_values, const GValue * param_values, + gpointer invocation_hint, gpointer marshal_data) +{ + typedef void (*marshalfunc_VOID__MINIOBJECT) (gpointer obj, gpointer arg1, gpointer data2); + marshalfunc_VOID__MINIOBJECT callback; + GCClosure *cc = (GCClosure *) closure; + gpointer data1, data2; + + g_return_if_fail(n_param_values == 2); + + if (G_CCLOSURE_SWAP_DATA(closure)) { + data1 = closure->data; + data2 = g_value_peek_pointer(param_values + 0); + } else { + data1 = g_value_peek_pointer(param_values + 0); + data2 = closure->data; + } + callback = (marshalfunc_VOID__MINIOBJECT) (marshal_data ? marshal_data : cc->callback); + + callback(data1, gst_value_get_mini_object(param_values + 1), data2); +} + +static void +webkit_video_sink_class_init(WebKitVideoSinkClass* klass) +{ + GObjectClass* gobject_class = G_OBJECT_CLASS(klass); + GstBaseSinkClass* gstbase_sink_class = GST_BASE_SINK_CLASS(klass); + + g_type_class_add_private(klass, sizeof(WebKitVideoSinkPrivate)); + + gobject_class->dispose = webkit_video_sink_dispose; + + gstbase_sink_class->unlock = webkit_video_sink_unlock; + gstbase_sink_class->unlock_stop = webkit_video_sink_unlock_stop; + gstbase_sink_class->render = webkit_video_sink_render; + gstbase_sink_class->preroll = webkit_video_sink_render; + gstbase_sink_class->stop = webkit_video_sink_stop; + gstbase_sink_class->start = webkit_video_sink_start; + + webkit_video_sink_signals[REPAINT_REQUESTED] = g_signal_new("repaint-requested", + G_TYPE_FROM_CLASS(klass), + (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION), + 0, + 0, + 0, + marshal_VOID__MINIOBJECT, + G_TYPE_NONE, 1, GST_TYPE_BUFFER); +} + +/** + * webkit_video_sink_new: + * + * Creates a new GStreamer video sink. + * + * Return value: a #GstElement for the newly created video sink + */ +GstElement* +webkit_video_sink_new(void) +{ + return (GstElement*)g_object_new(WEBKIT_TYPE_VIDEO_SINK, 0); +} + diff --git a/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.h b/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.h new file mode 100644 index 0000000..7ea7d91 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/VideoSinkGStreamer.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2007 OpenedHand + * Copyright (C) 2007 Alp Toker <alp@atoker.com> + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _HAVE_WEBKIT_VIDEO_SINK_H +#define _HAVE_WEBKIT_VIDEO_SINK_H + +#include <cairo.h> +#include <glib-object.h> +#include <gst/video/gstvideosink.h> + +G_BEGIN_DECLS + +#define WEBKIT_TYPE_VIDEO_SINK webkit_video_sink_get_type() + +#define WEBKIT_VIDEO_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), \ + WEBKIT_TYPE_VIDEO_SINK, WebKitVideoSink)) + +#define WEBKIT_VIDEO_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), \ + WEBKIT_TYPE_VIDEO_SINK, WebKitVideoSinkClass)) + +#define WEBKIT_IS_VIDEO_SINK(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), \ + WEBKIT_TYPE_VIDEO_SINK)) + +#define WEBKIT_IS_VIDEO_SINK_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), \ + WEBKIT_TYPE_VIDEO_SINK)) + +#define WEBKIT_VIDEO_SINK_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), \ + WEBKIT_TYPE_VIDEO_SINK, WebKitVideoSinkClass)) + +typedef struct _WebKitVideoSink WebKitVideoSink; +typedef struct _WebKitVideoSinkClass WebKitVideoSinkClass; +typedef struct _WebKitVideoSinkPrivate WebKitVideoSinkPrivate; + +struct _WebKitVideoSink { + /*< private >*/ + GstVideoSink parent; + WebKitVideoSinkPrivate *priv; +}; + +struct _WebKitVideoSinkClass { + /*< private >*/ + GstVideoSinkClass parent_class; + + /* Future padding */ + void (* _webkit_reserved1)(void); + void (* _webkit_reserved2)(void); + void (* _webkit_reserved3)(void); + void (* _webkit_reserved4)(void); + void (* _webkit_reserved5)(void); + void (* _webkit_reserved6)(void); +}; + +GType webkit_video_sink_get_type(void) G_GNUC_CONST; +GstElement *webkit_video_sink_new(void); + +G_END_DECLS + +#endif diff --git a/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.cpp b/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.cpp new file mode 100644 index 0000000..daee506 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.cpp @@ -0,0 +1,763 @@ +/* + * Copyright (C) 2009, 2010 Sebastian Dröge <sebastian.droege@collabora.co.uk> + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" +#include "WebKitWebSourceGStreamer.h" + +#include "Document.h" +#include "GOwnPtr.h" +#include "GRefPtr.h" +#include "Noncopyable.h" +#include "NotImplemented.h" +#include "ResourceHandleClient.h" +#include "ResourceHandleInternal.h" +#include "ResourceRequest.h" +#include "ResourceResponse.h" +#include <wtf/text/CString.h> +#include <gst/app/gstappsrc.h> +#include <gst/pbutils/missing-plugins.h> + +using namespace WebCore; + +class StreamingClient : public Noncopyable, public ResourceHandleClient { + public: + StreamingClient(WebKitWebSrc*); + virtual ~StreamingClient(); + + virtual void willSendRequest(ResourceHandle*, ResourceRequest&, const ResourceResponse&); + virtual void didReceiveResponse(ResourceHandle*, const ResourceResponse&); + virtual void didReceiveData(ResourceHandle*, const char*, int, int); + virtual void didFinishLoading(ResourceHandle*); + virtual void didFail(ResourceHandle*, const ResourceError&); + virtual void wasBlocked(ResourceHandle*); + virtual void cannotShowURL(ResourceHandle*); + + private: + WebKitWebSrc* m_src; +}; + +#define WEBKIT_WEB_SRC_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), WEBKIT_TYPE_WEB_SRC, WebKitWebSrcPrivate)) +struct _WebKitWebSrcPrivate { + GstAppSrc* appsrc; + GstPad* srcpad; + gchar* uri; + + RefPtr<WebCore::Frame> frame; + + StreamingClient* client; + RefPtr<ResourceHandle> resourceHandle; + + guint64 offset; + guint64 size; + gboolean seekable; + gboolean paused; + + guint64 requestedOffset; + + guint needDataID; + guint enoughDataID; + guint seekID; + + // icecast stuff + gboolean iradioMode; + gchar* iradioName; + gchar* iradioGenre; + gchar* iradioUrl; + gchar* iradioTitle; + + // TRUE if appsrc's version is >= 0.10.27, see + // https://bugzilla.gnome.org/show_bug.cgi?id=609423 + gboolean haveAppSrc27; +}; + +enum { + PROP_IRADIO_MODE = 1, + PROP_IRADIO_NAME, + PROP_IRADIO_GENRE, + PROP_IRADIO_URL, + PROP_IRADIO_TITLE +}; + +static GstStaticPadTemplate srcTemplate = GST_STATIC_PAD_TEMPLATE("src", + GST_PAD_SRC, + GST_PAD_ALWAYS, + GST_STATIC_CAPS_ANY); + +GST_DEBUG_CATEGORY_STATIC(webkit_web_src_debug); +#define GST_CAT_DEFAULT webkit_web_src_debug + +static void webKitWebSrcUriHandlerInit(gpointer gIface, + gpointer ifaceData); + +static void webKitWebSrcFinalize(GObject* object); +static void webKitWebSrcSetProperty(GObject* object, guint propID, const GValue* value, GParamSpec* pspec); +static void webKitWebSrcGetProperty(GObject* object, guint propID, GValue* value, GParamSpec* pspec); +static GstStateChangeReturn webKitWebSrcChangeState(GstElement* element, GstStateChange transition); + +static void webKitWebSrcNeedDataCb(GstAppSrc* appsrc, guint length, gpointer userData); +static void webKitWebSrcEnoughDataCb(GstAppSrc* appsrc, gpointer userData); +static gboolean webKitWebSrcSeekDataCb(GstAppSrc* appsrc, guint64 offset, gpointer userData); + +static void webKitWebSrcStop(WebKitWebSrc* src, bool seeking); + +static GstAppSrcCallbacks appsrcCallbacks = { + webKitWebSrcNeedDataCb, + webKitWebSrcEnoughDataCb, + webKitWebSrcSeekDataCb, + { 0 } +}; + +static void doInit(GType gtype) +{ + static const GInterfaceInfo uriHandlerInfo = { + webKitWebSrcUriHandlerInit, + 0, 0 + }; + + GST_DEBUG_CATEGORY_INIT(webkit_web_src_debug, "webkitwebsrc", 0, "websrc element"); + g_type_add_interface_static(gtype, GST_TYPE_URI_HANDLER, + &uriHandlerInfo); +} + +GST_BOILERPLATE_FULL(WebKitWebSrc, webkit_web_src, GstBin, GST_TYPE_BIN, doInit); + +static void webkit_web_src_base_init(gpointer klass) +{ + GstElementClass* eklass = GST_ELEMENT_CLASS(klass); + + gst_element_class_add_pad_template(eklass, + gst_static_pad_template_get(&srcTemplate)); + gst_element_class_set_details_simple(eklass, + (gchar*) "WebKit Web source element", + (gchar*) "Source", + (gchar*) "Handles HTTP/HTTPS uris", + (gchar*) "Sebastian Dröge <sebastian.droege@collabora.co.uk>"); +} + +static void webkit_web_src_class_init(WebKitWebSrcClass* klass) +{ + GObjectClass* oklass = G_OBJECT_CLASS(klass); + GstElementClass* eklass = GST_ELEMENT_CLASS(klass); + + oklass->finalize = webKitWebSrcFinalize; + oklass->set_property = webKitWebSrcSetProperty; + oklass->get_property = webKitWebSrcGetProperty; + + // icecast stuff + g_object_class_install_property(oklass, + PROP_IRADIO_MODE, + g_param_spec_boolean("iradio-mode", + "iradio-mode", + "Enable internet radio mode (extraction of shoutcast/icecast metadata)", + FALSE, + (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property(oklass, + PROP_IRADIO_NAME, + g_param_spec_string("iradio-name", + "iradio-name", + "Name of the stream", + 0, + (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property(oklass, + PROP_IRADIO_GENRE, + g_param_spec_string("iradio-genre", + "iradio-genre", + "Genre of the stream", + 0, + (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property(oklass, + PROP_IRADIO_URL, + g_param_spec_string("iradio-url", + "iradio-url", + "Homepage URL for radio stream", + 0, + (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); + + g_object_class_install_property(oklass, + PROP_IRADIO_TITLE, + g_param_spec_string("iradio-title", + "iradio-title", + "Name of currently playing song", + 0, + (GParamFlags) (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS))); + + eklass->change_state = webKitWebSrcChangeState; + + g_type_class_add_private(klass, sizeof(WebKitWebSrcPrivate)); +} + +static void webkit_web_src_init(WebKitWebSrc* src, + WebKitWebSrcClass* gKlass) +{ + GstPadTemplate* padTemplate = gst_static_pad_template_get(&srcTemplate); + GstPad* targetpad; + WebKitWebSrcPrivate* priv = WEBKIT_WEB_SRC_GET_PRIVATE(src); + + src->priv = priv; + + priv->client = new StreamingClient(src); + + priv->srcpad = gst_ghost_pad_new_no_target_from_template("src", + padTemplate); + + gst_element_add_pad(GST_ELEMENT(src), priv->srcpad); + + priv->appsrc = GST_APP_SRC(gst_element_factory_make("appsrc", 0)); + if (!priv->appsrc) { + GST_ERROR_OBJECT(src, "Failed to create appsrc"); + return; + } + + GstElementFactory* factory = GST_ELEMENT_FACTORY(GST_ELEMENT_GET_CLASS(priv->appsrc)->elementfactory); + priv->haveAppSrc27 = gst_plugin_feature_check_version(GST_PLUGIN_FEATURE(factory), 0, 10, 27); + + gst_bin_add(GST_BIN(src), GST_ELEMENT(priv->appsrc)); + + targetpad = gst_element_get_static_pad(GST_ELEMENT(priv->appsrc), "src"); + gst_ghost_pad_set_target(GST_GHOST_PAD(priv->srcpad), targetpad); + gst_object_unref(targetpad); + + gst_app_src_set_callbacks(priv->appsrc, &appsrcCallbacks, src, 0); + gst_app_src_set_emit_signals(priv->appsrc, FALSE); + gst_app_src_set_stream_type(priv->appsrc, GST_APP_STREAM_TYPE_SEEKABLE); + + // 512k is a abitrary number but we should choose a value + // here to not pause/unpause the SoupMessage too often and + // to make sure there's always some data available for + // GStreamer to handle. + gst_app_src_set_max_bytes(priv->appsrc, 512 * 1024); + + // Emit the need-data signal if the queue contains less + // than 20% of data. Without this the need-data signal + // is emitted when the queue is empty, we then dispatch + // the soup message unpausing to the main loop and from + // there unpause the soup message. This already takes + // quite some time and libsoup even needs some more time + // to actually provide data again. If we do all this + // already if the queue is 20% empty, it's much more + // likely that libsoup already provides new data before + // the queue is really empty. + // This might need tweaking for ports not using libsoup. + if (priv->haveAppSrc27) + g_object_set(priv->appsrc, "min-percent", 20, NULL); + + webKitWebSrcStop(src, false); +} + +static void webKitWebSrcFinalize(GObject* object) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(object); + WebKitWebSrcPrivate* priv = src->priv; + + delete priv->client; + + g_free(priv->uri); + + GST_CALL_PARENT(G_OBJECT_CLASS, finalize, ((GObject* )(src))); +} + +static void webKitWebSrcSetProperty(GObject* object, guint propID, const GValue* value, GParamSpec* pspec) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(object); + WebKitWebSrcPrivate* priv = src->priv; + + switch (propID) { + case PROP_IRADIO_MODE: + priv->iradioMode = g_value_get_boolean(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, pspec); + break; + } +} + +static void webKitWebSrcGetProperty(GObject* object, guint propID, GValue* value, GParamSpec* pspec) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(object); + WebKitWebSrcPrivate* priv = src->priv; + + switch (propID) { + case PROP_IRADIO_MODE: + g_value_set_boolean(value, priv->iradioMode); + break; + case PROP_IRADIO_NAME: + g_value_set_string(value, priv->iradioName); + break; + case PROP_IRADIO_GENRE: + g_value_set_string(value, priv->iradioGenre); + break; + case PROP_IRADIO_URL: + g_value_set_string(value, priv->iradioUrl); + break; + case PROP_IRADIO_TITLE: + g_value_set_string(value, priv->iradioTitle); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propID, pspec); + break; + } +} + + +static void webKitWebSrcStop(WebKitWebSrc* src, bool seeking) +{ + WebKitWebSrcPrivate* priv = src->priv; + + if (priv->resourceHandle) { + priv->resourceHandle->cancel(); + priv->resourceHandle.release(); + } + priv->resourceHandle = 0; + + if (priv->frame) + priv->frame.release(); + + if (priv->needDataID) + g_source_remove(priv->needDataID); + priv->needDataID = 0; + + if (priv->enoughDataID) + g_source_remove(priv->enoughDataID); + priv->enoughDataID = 0; + + if (priv->seekID) + g_source_remove(priv->seekID); + priv->seekID = 0; + + priv->paused = FALSE; + + g_free(priv->iradioName); + priv->iradioName = 0; + + g_free(priv->iradioGenre); + priv->iradioGenre = 0; + + g_free(priv->iradioUrl); + priv->iradioUrl = 0; + + g_free(priv->iradioTitle); + priv->iradioTitle = 0; + + if (priv->appsrc) { + gst_app_src_set_caps(priv->appsrc, 0); + if (!seeking) + gst_app_src_set_size(priv->appsrc, -1); + } + + priv->offset = 0; + priv->seekable = FALSE; + + if (!seeking) { + priv->size = 0; + priv->requestedOffset = 0; + } + + GST_DEBUG_OBJECT(src, "Stopped request"); +} + +static bool webKitWebSrcStart(WebKitWebSrc* src) +{ + WebKitWebSrcPrivate* priv = src->priv; + + if (!priv->uri) { + GST_ERROR_OBJECT(src, "No URI provided"); + return false; + } + + KURL url = KURL(KURL(), priv->uri); + + ResourceRequest request(url); + request.setTargetType(ResourceRequestBase::TargetIsMedia); + request.setAllowCookies(true); + + // Let Apple web servers know we want to access their nice movie trailers. + if (!g_ascii_strcasecmp("movies.apple.com", url.host().utf8().data())) + request.setHTTPUserAgent("Quicktime/7.2.0"); + + if (priv->frame) { + Document* document = priv->frame->document(); + if (document) + request.setHTTPReferrer(document->documentURI()); + + FrameLoader* loader = priv->frame->loader(); + if (loader) + loader->addExtraFieldsToSubresourceRequest(request); + } + + if (priv->requestedOffset) { + GOwnPtr<gchar> val; + + val.set(g_strdup_printf("bytes=%" G_GUINT64_FORMAT "-", priv->requestedOffset)); + request.setHTTPHeaderField("Range", val.get()); + } + + if (priv->iradioMode) + request.setHTTPHeaderField("icy-metadata", "1"); + + // Needed to use DLNA streaming servers + request.setHTTPHeaderField("transferMode.dlna", "Streaming"); + + priv->resourceHandle = ResourceHandle::create(request, priv->client, 0, false, false); + if (!priv->resourceHandle) { + GST_ERROR_OBJECT(src, "Failed to create ResourceHandle"); + return false; + } + + GST_DEBUG_OBJECT(src, "Started request"); + + return true; +} + +static GstStateChangeReturn webKitWebSrcChangeState(GstElement* element, GstStateChange transition) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + WebKitWebSrc* src = WEBKIT_WEB_SRC(element); + WebKitWebSrcPrivate* priv = src->priv; + + switch (transition) { + case GST_STATE_CHANGE_NULL_TO_READY: + if (!priv->appsrc) { + gst_element_post_message(element, + gst_missing_element_message_new(element, "appsrc")); + GST_ELEMENT_ERROR(src, CORE, MISSING_PLUGIN, (0), ("no appsrc")); + return GST_STATE_CHANGE_FAILURE; + } + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS(parent_class)->change_state(element, transition); + if (G_UNLIKELY(ret == GST_STATE_CHANGE_FAILURE)) { + GST_DEBUG_OBJECT(src, "State change failed"); + return ret; + } + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + GST_DEBUG_OBJECT(src, "READY->PAUSED"); + if (!webKitWebSrcStart(src)) + ret = GST_STATE_CHANGE_FAILURE; + break; + case GST_STATE_CHANGE_PAUSED_TO_READY: + GST_DEBUG_OBJECT(src, "PAUSED->READY"); + webKitWebSrcStop(src, false); + break; + default: + break; + } + + return ret; +} + +// uri handler interface + +static GstURIType webKitWebSrcUriGetType(void) +{ + return GST_URI_SRC; +} + +static gchar** webKitWebSrcGetProtocols(void) +{ + static gchar* protocols[] = {(gchar*) "http", (gchar*) "https", 0 }; + + return protocols; +} + +static const gchar* webKitWebSrcGetUri(GstURIHandler* handler) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(handler); + WebKitWebSrcPrivate* priv = src->priv; + + return priv->uri; +} + +static gboolean webKitWebSrcSetUri(GstURIHandler* handler, const gchar* uri) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(handler); + WebKitWebSrcPrivate* priv = src->priv; + + if (GST_STATE(src) >= GST_STATE_PAUSED) { + GST_ERROR_OBJECT(src, "URI can only be set in states < PAUSED"); + return FALSE; + } + + g_free(priv->uri); + priv->uri = 0; + + if (!uri) + return TRUE; + + KURL url(KURL(), uri); + + if (!url.isValid() || !url.protocolInHTTPFamily()) { + GST_ERROR_OBJECT(src, "Invalid URI '%s'", uri); + return FALSE; + } + + priv->uri = g_strdup(url.string().utf8().data()); + return TRUE; +} + +static void webKitWebSrcUriHandlerInit(gpointer gIface, gpointer ifaceData) +{ + GstURIHandlerInterface* iface = (GstURIHandlerInterface *) gIface; + + iface->get_type = webKitWebSrcUriGetType; + iface->get_protocols = webKitWebSrcGetProtocols; + iface->get_uri = webKitWebSrcGetUri; + iface->set_uri = webKitWebSrcSetUri; +} + +// appsrc callbacks + +static gboolean webKitWebSrcNeedDataMainCb(WebKitWebSrc* src) +{ + WebKitWebSrcPrivate* priv = src->priv; + +#if USE(NETWORK_SOUP) + ResourceHandleInternal* d = priv->resourceHandle->getInternal(); + if (d->m_msg) + soup_session_unpause_message(ResourceHandle::defaultSession(), d->m_msg); +#endif + // Ports not using libsoup need to call the unpause/schedule API of their + // underlying network implementation here. + + priv->paused = FALSE; + priv->needDataID = 0; + return FALSE; +} + +static void webKitWebSrcNeedDataCb(GstAppSrc* appsrc, guint length, gpointer userData) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(userData); + WebKitWebSrcPrivate* priv = src->priv; + + GST_DEBUG_OBJECT(src, "Need more data: %u", length); + if (priv->needDataID || !priv->paused) + return; + + priv->needDataID = g_timeout_add_full(G_PRIORITY_DEFAULT, 0, (GSourceFunc) webKitWebSrcNeedDataMainCb, gst_object_ref(src), (GDestroyNotify) gst_object_unref); +} + +static gboolean webKitWebSrcEnoughDataMainCb(WebKitWebSrc* src) +{ + WebKitWebSrcPrivate* priv = src->priv; + +#if USE(NETWORK_SOUP) + ResourceHandleInternal* d = priv->resourceHandle->getInternal(); + soup_session_pause_message(ResourceHandle::defaultSession(), d->m_msg); +#endif + // Ports not using libsoup need to call the pause/unschedule API of their + // underlying network implementation here. + + priv->paused = TRUE; + priv->enoughDataID = 0; + return FALSE; +} + +static void webKitWebSrcEnoughDataCb(GstAppSrc* appsrc, gpointer userData) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(userData); + WebKitWebSrcPrivate* priv = src->priv; + + GST_DEBUG_OBJECT(src, "Have enough data"); + if (priv->enoughDataID || priv->paused) + return; + + priv->enoughDataID = g_timeout_add_full(G_PRIORITY_DEFAULT, 0, (GSourceFunc) webKitWebSrcEnoughDataMainCb, gst_object_ref(src), (GDestroyNotify) gst_object_unref); +} + +static gboolean webKitWebSrcSeekMainCb(WebKitWebSrc* src) +{ + webKitWebSrcStop(src, true); + webKitWebSrcStart(src); + + return FALSE; +} + +static gboolean webKitWebSrcSeekDataCb(GstAppSrc* appsrc, guint64 offset, gpointer userData) +{ + WebKitWebSrc* src = WEBKIT_WEB_SRC(userData); + WebKitWebSrcPrivate* priv = src->priv; + + GST_DEBUG_OBJECT(src, "Seeking to offset: %" G_GUINT64_FORMAT, offset); + if (offset == priv->offset) + return TRUE; + + if (!priv->seekable) + return FALSE; + if (offset > priv->size) + return FALSE; + + GST_DEBUG_OBJECT(src, "Doing range-request seek"); + priv->requestedOffset = offset; + if (priv->seekID) + g_source_remove(priv->seekID); + priv->seekID = g_timeout_add_full(G_PRIORITY_DEFAULT, 0, (GSourceFunc) webKitWebSrcSeekMainCb, gst_object_ref(src), (GDestroyNotify) gst_object_unref); + + return TRUE; +} + +void webKitWebSrcSetFrame(WebKitWebSrc* src, WebCore::Frame* frame) +{ + WebKitWebSrcPrivate* priv = src->priv; + + priv->frame = frame; +} + +StreamingClient::StreamingClient(WebKitWebSrc* src) : m_src(src) +{ + +} + +StreamingClient::~StreamingClient() +{ + +} + +void StreamingClient::willSendRequest(ResourceHandle*, ResourceRequest&, const ResourceResponse&) +{ +} + +void StreamingClient::didReceiveResponse(ResourceHandle*, const ResourceResponse& response) +{ + WebKitWebSrcPrivate* priv = m_src->priv; + + GST_DEBUG_OBJECT(m_src, "Received response: %d", response.httpStatusCode()); + + // If we seeked we need 206 == PARTIAL_CONTENT + if (priv->requestedOffset && response.httpStatusCode() != 206) { + GST_ELEMENT_ERROR(m_src, RESOURCE, READ, (0), (0)); + gst_app_src_end_of_stream(priv->appsrc); + webKitWebSrcStop(m_src, false); + return; + } + + long long length = response.expectedContentLength(); + if (length > 0) { + length += priv->requestedOffset; + gst_app_src_set_size(priv->appsrc, length); + if (!priv->haveAppSrc27) { + gst_segment_set_duration(&GST_BASE_SRC(priv->appsrc)->segment, GST_FORMAT_BYTES, length); + gst_element_post_message(GST_ELEMENT(priv->appsrc), + gst_message_new_duration(GST_OBJECT(priv->appsrc), + GST_FORMAT_BYTES, length)); + } + } + + priv->size = length >= 0 ? length : 0; + priv->seekable = length > 0 && g_ascii_strcasecmp("none", response.httpHeaderField("Accept-Ranges").utf8().data()); + + // icecast stuff + String value = response.httpHeaderField("icy-metaint"); + if (!value.isEmpty()) { + gchar* endptr = 0; + gint64 icyMetaInt = g_ascii_strtoll(value.utf8().data(), &endptr, 10); + + if (endptr && *endptr == '\0' && icyMetaInt > 0) { + GstCaps* caps = gst_caps_new_simple("application/x-icy", "metadata-interval", G_TYPE_INT, (gint) icyMetaInt, NULL); + + gst_app_src_set_caps(priv->appsrc, caps); + gst_caps_unref(caps); + } + } + + GstTagList* tags = gst_tag_list_new(); + value = response.httpHeaderField("icy-name"); + if (!value.isEmpty()) { + g_free(priv->iradioName); + priv->iradioName = g_strdup(value.utf8().data()); + g_object_notify(G_OBJECT(m_src), "iradio-name"); + gst_tag_list_add(tags, GST_TAG_MERGE_REPLACE, GST_TAG_ORGANIZATION, priv->iradioName, NULL); + } + value = response.httpHeaderField("icy-genre"); + if (!value.isEmpty()) { + g_free(priv->iradioGenre); + priv->iradioGenre = g_strdup(value.utf8().data()); + g_object_notify(G_OBJECT(m_src), "iradio-genre"); + gst_tag_list_add(tags, GST_TAG_MERGE_REPLACE, GST_TAG_GENRE, priv->iradioGenre, NULL); + } + value = response.httpHeaderField("icy-url"); + if (!value.isEmpty()) { + g_free(priv->iradioUrl); + priv->iradioUrl = g_strdup(value.utf8().data()); + g_object_notify(G_OBJECT(m_src), "iradio-url"); + gst_tag_list_add(tags, GST_TAG_MERGE_REPLACE, GST_TAG_LOCATION, priv->iradioUrl, NULL); + } + value = response.httpHeaderField("icy-title"); + if (!value.isEmpty()) { + g_free(priv->iradioTitle); + priv->iradioTitle = g_strdup(value.utf8().data()); + g_object_notify(G_OBJECT(m_src), "iradio-title"); + gst_tag_list_add(tags, GST_TAG_MERGE_REPLACE, GST_TAG_TITLE, priv->iradioTitle, NULL); + } + + if (gst_tag_list_is_empty(tags)) + gst_tag_list_free(tags); + else + gst_element_found_tags_for_pad(GST_ELEMENT(m_src), m_src->priv->srcpad, tags); +} + +void StreamingClient::didReceiveData(ResourceHandle* handle, const char* data, int length, int lengthReceived) +{ + WebKitWebSrcPrivate* priv = m_src->priv; + + GST_LOG_OBJECT(m_src, "Have %d bytes of data", length); + + if (priv->seekID || handle != priv->resourceHandle) { + GST_DEBUG_OBJECT(m_src, "Seek in progress, ignoring data"); + return; + } + + GstBuffer* buffer = gst_buffer_new_and_alloc(length); + + memcpy(GST_BUFFER_DATA(buffer), data, length); + GST_BUFFER_OFFSET(buffer) = priv->offset; + priv->offset += length; + GST_BUFFER_OFFSET_END(buffer) = priv->offset; + + GstFlowReturn ret = gst_app_src_push_buffer(priv->appsrc, buffer); + if (ret != GST_FLOW_OK && ret != GST_FLOW_UNEXPECTED) + GST_ELEMENT_ERROR(m_src, CORE, FAILED, (0), (0)); +} + +void StreamingClient::didFinishLoading(ResourceHandle*) +{ + GST_DEBUG_OBJECT(m_src, "Have EOS"); + gst_app_src_end_of_stream(m_src->priv->appsrc); +} + +void StreamingClient::didFail(ResourceHandle*, const ResourceError& error) +{ + GST_ERROR_OBJECT(m_src, "Have failure: %s", error.localizedDescription().utf8().data()); + GST_ELEMENT_ERROR(m_src, RESOURCE, FAILED, ("%s", error.localizedDescription().utf8().data()), (0)); + gst_app_src_end_of_stream(m_src->priv->appsrc); +} + +void StreamingClient::wasBlocked(ResourceHandle*) +{ +} + +void StreamingClient::cannotShowURL(ResourceHandle*) +{ +} + diff --git a/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.h b/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.h new file mode 100644 index 0000000..ae19640 --- /dev/null +++ b/WebCore/platform/graphics/gstreamer/WebKitWebSourceGStreamer.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2009,2010 Sebastian Dröge <sebastian.droege@collabora.co.uk> + * + * 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 of the License, or (at your option) any later version. + * + * This library 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 library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef WebKitWebSourceGStreamer_h +#define WebKitWebSourceGStreamer_h + +#include "Frame.h" +#include <gst/gst.h> + +G_BEGIN_DECLS + +#define WEBKIT_TYPE_WEB_SRC (webkit_web_src_get_type ()) +#define WEBKIT_WEB_SRC(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), WEBKIT_TYPE_WEB_SRC, WebKitWebSrc)) +#define WEBKIT_WEB_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), WEBKIT_TYPE_WEB_SRC, WebKitWebSrcClass)) +#define WEBKIT_IS_WEB_SRC(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), WEBKIT_TYPE_WEB_SRC)) +#define WEBKIT_IS_WEB_SRC_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), WEBKIT_TYPE_WEB_SRC)) + +typedef struct _WebKitWebSrc WebKitWebSrc; +typedef struct _WebKitWebSrcClass WebKitWebSrcClass; +typedef struct _WebKitWebSrcPrivate WebKitWebSrcPrivate; + +struct _WebKitWebSrc { + GstBin parent; + + WebKitWebSrcPrivate *priv; +}; + +struct _WebKitWebSrcClass { + GstBinClass parentClass; +}; + +GType webkit_web_src_get_type(void); +void webKitWebSrcSetFrame(WebKitWebSrc* src, WebCore::Frame* frame); + +G_END_DECLS + +#endif |