/* -*- Mode: CPP; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Pix
*
* Copyright (C) 2008-2009 Free Software Foundation, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
/* This was based on a file from Tracker */
/*
* Tracker - audio/video metadata extraction based on GStreamer
* Copyright (C) 2006, Laurent Aguerreche (laurent.aguerreche@free.fr)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include
#include
#include
#include
#include "gstreamer-utils.h"
static gboolean gstreamer_initialized = FALSE;
typedef struct {
GstElement *playbin;
GstTagList *tagcache;
gboolean has_audio;
gboolean has_video;
gint video_height;
gint video_width;
gint video_fps_n;
gint video_fps_d;
gint video_bitrate;
char *video_codec;
gint audio_channels;
gint audio_samplerate;
gint audio_bitrate;
char *audio_codec;
} MetadataExtractor;
static void
reset_extractor_data (MetadataExtractor *extractor)
{
if (extractor->tagcache != NULL) {
gst_tag_list_unref (extractor->tagcache);
extractor->tagcache = NULL;
}
g_free (extractor->audio_codec);
extractor->audio_codec = NULL;
g_free (extractor->video_codec);
extractor->video_codec = NULL;
extractor->has_audio = FALSE;
extractor->has_video = FALSE;
extractor->video_fps_n = -1;
extractor->video_fps_d = -1;
extractor->video_height = -1;
extractor->video_width = -1;
extractor->video_bitrate = -1;
extractor->audio_channels = -1;
extractor->audio_samplerate = -1;
extractor->audio_bitrate = -1;
}
static void
metadata_extractor_free (MetadataExtractor *extractor)
{
reset_extractor_data (extractor);
gst_element_set_state (extractor->playbin, GST_STATE_NULL);
gst_object_unref (GST_OBJECT (extractor->playbin));
g_slice_free (MetadataExtractor, extractor);
}
gboolean
gstreamer_init (void)
{
if (! gstreamer_initialized) {
GError *error = NULL;
if (! gst_init_check (NULL, NULL, &error)) {
g_warning ("%s", error->message);
g_error_free (error);
return FALSE;
}
gstreamer_initialized = TRUE;
}
return TRUE;
}
static void
add_metadata (GFileInfo *info,
const char *key,
char *raw,
char *formatted)
{
GthMetadata *metadata;
if (raw == NULL)
return;
if (strcmp (key, "general::dimensions") == 0) {
g_file_info_set_attribute_string (info, key, raw);
return;
}
else if (strcmp (key, "general::duration") == 0) {
int secs;
g_free (formatted);
sscanf (raw, "%i", &secs);
formatted = _g_format_duration_for_display (secs * 1000);
}
else if (strcmp (key, "audio-video::general::bitrate") == 0) {
int bps;
g_free (formatted);
sscanf (raw, "%i", &bps);
formatted = g_strdup_printf ("%d kbps", bps / 1000);
}
metadata = gth_metadata_new ();
g_object_set (metadata,
"id", key,
"formatted", formatted != NULL ? formatted : raw,
"raw", raw,
NULL);
g_file_info_set_attribute_object (info, key, G_OBJECT (metadata));
g_object_unref (metadata);
g_free (raw);
g_free (formatted);
}
static void
add_metadata_from_tag (GFileInfo *info,
const GstTagList *list,
const char *tag,
const char *tag_key)
{
GType tag_type;
tag_type = gst_tag_get_type (tag);
if (tag_type == G_TYPE_BOOLEAN) {
gboolean ret;
if (gst_tag_list_get_boolean (list, tag, &ret)) {
if (ret)
add_metadata (info, tag_key, g_strdup ("TRUE"), NULL);
else
add_metadata (info, tag_key, g_strdup ("FALSE"), NULL);
}
}
if (tag_type == G_TYPE_STRING) {
char *ret = NULL;
if (gst_tag_list_get_string (list, tag, &ret))
add_metadata (info, tag_key, ret, NULL);
}
if (tag_type == G_TYPE_UCHAR) {
guint ret = 0;
if (gst_tag_list_get_uint (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%u", ret), NULL);
}
if (tag_type == G_TYPE_CHAR) {
int ret = 0;
if (gst_tag_list_get_int (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%d", ret), NULL);
}
if (tag_type == G_TYPE_UINT) {
guint ret = 0;
if (gst_tag_list_get_uint (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%u", ret), NULL);
}
if (tag_type == G_TYPE_INT) {
gint ret = 0;
if (gst_tag_list_get_int (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%d", ret), NULL);
}
if (tag_type == G_TYPE_ULONG) {
guint64 ret = 0;
if (gst_tag_list_get_uint64 (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%" G_GUINT64_FORMAT, ret), NULL);
}
if (tag_type == G_TYPE_LONG) {
gint64 ret = 0;
if (gst_tag_list_get_int64 (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%" G_GINT64_FORMAT, ret), NULL);
}
if (tag_type == G_TYPE_INT64) {
gint64 ret = 0;
if (gst_tag_list_get_int64 (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%" G_GINT64_FORMAT, ret), NULL);
}
if (tag_type == G_TYPE_UINT64) {
guint64 ret = 0;
if (gst_tag_list_get_uint64 (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%" G_GUINT64_FORMAT, ret), NULL);
}
if (tag_type == G_TYPE_DOUBLE) {
gdouble ret = 0;
if (gst_tag_list_get_double (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%f", ret), NULL);
}
if (tag_type == G_TYPE_FLOAT) {
gfloat ret = 0;
if (gst_tag_list_get_float (list, tag, &ret))
add_metadata (info, tag_key, g_strdup_printf ("%f", ret), NULL);
}
if (tag_type == G_TYPE_DATE) {
GDate *ret = NULL;
if (gst_tag_list_get_date (list, tag, &ret)) {
if (ret != NULL) {
char buf[128];
char *raw;
char *formatted;
g_date_strftime (buf, 10, "%F %T", ret);
raw = g_strdup (buf);
g_date_strftime (buf, 10, "%x %X", ret);
formatted = g_strdup (buf);
add_metadata (info, tag_key, raw, formatted);
}
g_free (ret);
}
}
}
static void
tag_iterate (const GstTagList *list,
const char *tag,
GFileInfo *info)
{
const char *tag_key;
char *attribute = NULL;
tag_key = NULL;
if (strcmp (tag, "container-format") == 0) {
tag_key = "general::format";
}
else if (strcmp (tag, "bitrate") == 0) {
tag_key = "audio-video::general::bitrate";
}
else if (strcmp (tag, "encoder") == 0) {
tag_key = "audio-video::general::encoder";
}
else if (strcmp (tag, "title") == 0) {
tag_key = "general::title";
}
else if (strcmp (tag, "artist") == 0) {
tag_key = "audio-video::general::artist";
}
else if (strcmp (tag, "album") == 0) {
tag_key = "audio-video::general::album";
}
else if (strcmp (tag, "audio-codec") == 0) {
tag_key = "audio-video::audio::codec";
}
else if (strcmp (tag, "video-codec") == 0) {
tag_key = "audio-video::video::codec";
}
if (tag_key == NULL) {
GthMetadataInfo *metadata_info;
attribute = g_strconcat ("audio-video::other::", tag, NULL);
metadata_info = gth_main_get_metadata_info (attribute);
if (metadata_info == NULL) {
GthMetadataInfo *info;
info = g_new0 (GthMetadataInfo, 1);
info->id = attribute;
info->display_name = gst_tag_get_nick (tag);
info->category = "audio-video::other";
info->sort_order = 500;
info->flags = GTH_METADATA_ALLOW_IN_PROPERTIES_VIEW;
metadata_info = gth_main_register_metadata_info (info);
g_free (info);
}
tag_key = attribute;
}
add_metadata_from_tag (info, list, tag, tag_key);
g_free (attribute);
}
static gint64
get_media_duration (MetadataExtractor *extractor)
{
GstFormat fmt;
gint64 duration;
g_return_val_if_fail (extractor, -1);
g_return_val_if_fail (extractor->playbin, -1);
fmt = GST_FORMAT_TIME;
duration = -1;
if (gst_element_query_duration (extractor->playbin, fmt, &duration) && (duration >= 0))
return duration / GST_SECOND;
else
return -1;
}
static void
extract_metadata (MetadataExtractor *extractor,
GFileInfo *info)
{
gint64 duration;
if (extractor->audio_channels >= 0)
add_metadata (info,
"audio-video::audio::channels",
g_strdup_printf ("%d", (guint) extractor->audio_channels),
g_strdup (extractor->audio_channels == 2 ? _("Stereo") : _("Mono")));
if (extractor->audio_samplerate >= 0)
add_metadata (info,
"audio-video::audio::samplerate",
g_strdup_printf ("%d", (guint) extractor->audio_samplerate),
g_strdup_printf ("%d Hz", (guint) extractor->audio_samplerate));
if (extractor->audio_bitrate >= 0)
add_metadata (info,
"audio-video::audio::bitrate",
g_strdup_printf ("%d", (guint) extractor->audio_bitrate),
g_strdup_printf ("%d bps", (guint) extractor->audio_bitrate));
if (extractor->video_height >= 0) {
add_metadata (info,
"audio-video::video::height",
g_strdup_printf ("%d", (guint) extractor->video_height),
NULL);
g_file_info_set_attribute_int32 (info, "frame::height", extractor->video_height);
}
if (extractor->video_width >= 0) {
add_metadata (info,
"audio-video::video::width",
g_strdup_printf ("%d", (guint) extractor->video_width),
NULL);
g_file_info_set_attribute_int32 (info, "frame::width", extractor->video_width);
}
if ((extractor->video_height >= 0) && (extractor->video_width >= 0))
add_metadata (info,
"general::dimensions",
g_strdup_printf (_("%d × %d"), (guint) extractor->video_width, (guint) extractor->video_height),
NULL);
if ((extractor->video_fps_n >= 0) && (extractor->video_fps_d >= 0))
add_metadata (info,
"audio-video::video::framerate",
g_strdup_printf ("%.7g", (gdouble) extractor->video_fps_n / (gdouble) extractor->video_fps_d),
g_strdup_printf ("%.7g fps", (gdouble) extractor->video_fps_n / (gdouble) extractor->video_fps_d));
if (extractor->video_bitrate >= 0)
add_metadata (info,
"audio-video::video::bitrate",
g_strdup_printf ("%d", (guint) extractor->video_bitrate),
g_strdup_printf ("%d bps", (guint) extractor->video_bitrate));
duration = get_media_duration (extractor);
if (duration >= 0)
add_metadata (info,
"general::duration",
g_strdup_printf ("%" G_GINT64_FORMAT, duration),
g_strdup_printf ("%" G_GINT64_FORMAT " sec", duration));
if (extractor->tagcache != NULL)
gst_tag_list_foreach (extractor->tagcache, (GstTagForeachFunc) tag_iterate, info);
}
static void
caps_set (GstPad *pad,
MetadataExtractor *extractor,
const char *type)
{
GstCaps *caps;
GstStructure *structure;
if ((caps = gst_pad_get_current_caps (pad)) == NULL)
return;
structure = gst_caps_get_structure (caps, 0);
if (structure == NULL) {
gst_caps_unref (caps);
return;
}
if (strcmp (type, "audio") == 0) {
gst_structure_get_int (structure, "channels", &extractor->audio_channels);
gst_structure_get_int (structure, "rate", &extractor->audio_samplerate);
gst_structure_get_int (structure, "bitrate", &extractor->audio_bitrate);
}
else if (strcmp (type, "video") == 0) {
gst_structure_get_fraction (structure, "framerate", &extractor->video_fps_n, &extractor->video_fps_d);
gst_structure_get_int (structure, "bitrate", &extractor->video_bitrate);
gst_structure_get_int (structure, "width", &extractor->video_width);
gst_structure_get_int (structure, "height", &extractor->video_height);
}
gst_caps_unref (caps);
}
static void
update_stream_info (MetadataExtractor *extractor)
{
GstElement *audio_sink;
GstElement *video_sink;
g_object_get (extractor->playbin,
"audio-sink", &audio_sink,
"video-sink", &video_sink,
NULL);
if (audio_sink != NULL) {
GstPad *audio_pad;
audio_pad = gst_element_get_static_pad (GST_ELEMENT (audio_sink), "sink");
if (audio_pad != NULL) {
GstCaps *caps;
if ((caps = gst_pad_get_current_caps (audio_pad)) != NULL) {
extractor->has_audio = TRUE;
caps_set (audio_pad, extractor, "audio");
gst_caps_unref (caps);
}
}
}
if (video_sink != NULL) {
GstPad *video_pad;
video_pad = gst_element_get_static_pad (GST_ELEMENT (video_sink), "sink");
if (video_pad != NULL) {
GstCaps *caps;
if ((caps = gst_pad_get_current_caps (video_pad)) != NULL) {
extractor->has_video = TRUE;
caps_set (video_pad, extractor, "video");
gst_caps_unref (caps);
}
}
}
}
static gboolean
message_loop_to_state_change (MetadataExtractor *extractor,
GstState state)
{
GstBus *bus;
GstMessageType events;
g_return_val_if_fail (extractor, FALSE);
g_return_val_if_fail (extractor->playbin, FALSE);
bus = gst_element_get_bus (extractor->playbin);
events = (GST_MESSAGE_TAG | GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
for (;;) {
GstMessage *message;
message = gst_bus_timed_pop_filtered (bus, GST_SECOND * 5, events);
if (message == NULL)
goto timed_out;
switch (GST_MESSAGE_TYPE (message)) {
case GST_MESSAGE_STATE_CHANGED: {
GstState old_state;
GstState new_state;
old_state = new_state = GST_STATE_NULL;
gst_message_parse_state_changed (message, &old_state, &new_state, NULL);
if (old_state == new_state)
break;
/* we only care about playbin (pipeline) state changes */
if (GST_MESSAGE_SRC (message) != GST_OBJECT (extractor->playbin))
break;
if ((old_state == GST_STATE_READY) && (new_state == GST_STATE_PAUSED))
update_stream_info (extractor);
else if ((old_state == GST_STATE_PAUSED) && (new_state == GST_STATE_READY))
reset_extractor_data (extractor);
if (new_state == state) {
gst_message_unref (message);
goto success;
}
break;
}
case GST_MESSAGE_TAG: {
GstTagList *tag_list;
GstTagList *result;
tag_list = NULL;
gst_message_parse_tag (message, &tag_list);
result = gst_tag_list_merge (extractor->tagcache, tag_list, GST_TAG_MERGE_KEEP);
if (extractor->tagcache != NULL)
gst_tag_list_unref (extractor->tagcache);
extractor->tagcache = result;
gst_tag_list_free (tag_list);
break;
}
case GST_MESSAGE_ERROR: {
gchar *debug = NULL;
GError *gsterror = NULL;
gst_message_parse_error (message, &gsterror, &debug);
/*g_warning ("Error: %s (%s)", gsterror->message, debug);*/
g_error_free (gsterror);
gst_message_unref (message);
g_free (debug);
goto error;
}
break;
case GST_MESSAGE_EOS: {
g_warning ("Media file could not be played.");
gst_message_unref (message);
goto error;
}
break;
default:
g_assert_not_reached ();
break;
}
gst_message_unref (message);
}
g_assert_not_reached ();
success:
/* state change succeeded */
GST_DEBUG ("state change to %s succeeded", gst_element_state_get_name (state));
return TRUE;
timed_out:
/* it's taking a long time to open */
GST_DEBUG ("state change to %s timed out, returning success", gst_element_state_get_name (state));
return TRUE;
error:
GST_DEBUG ("error while waiting for state change to %s", gst_element_state_get_name (state));
/* already set *error */
return FALSE;
}
gboolean
gstreamer_read_metadata_from_file (GFile *file,
GFileInfo *info,
GError **error)
{
char *uri;
MetadataExtractor *extractor;
if (! gstreamer_init ())
return FALSE;
uri = g_file_get_uri (file);
g_return_val_if_fail (uri != NULL, FALSE);
extractor = g_slice_new0 (MetadataExtractor);
reset_extractor_data (extractor);
extractor->playbin = gst_element_factory_make ("playbin", "playbin");
g_object_set (G_OBJECT (extractor->playbin),
"uri", uri,
"audio-sink", gst_element_factory_make ("fakesink", "fakesink-audio"),
"video-sink", gst_element_factory_make ("fakesink", "fakesink-video"),
NULL);
gst_element_set_state (extractor->playbin, GST_STATE_PAUSED);
message_loop_to_state_change (extractor, GST_STATE_PAUSED);
extract_metadata (extractor, info);
metadata_extractor_free (extractor);
g_free (uri);
return TRUE;
}
/* -- _gst_playbin_get_current_frame -- */
typedef struct {
GdkPixbuf *pixbuf;
FrameReadyCallback cb;
gpointer user_data;
} ScreenshotData;
static void
screenshot_data_finalize (ScreenshotData *data)
{
if (data->cb != NULL)
data->cb (data->pixbuf, data->user_data);
g_free (data);
}
static void
destroy_pixbuf (guchar *pix, gpointer data)
{
gst_sample_unref (GST_SAMPLE (data));
}
gboolean
_gst_playbin_get_current_frame (GstElement *playbin,
FrameReadyCallback cb,
gpointer user_data)
{
ScreenshotData *data;
GstElement *sink;
GstSample *sample;
GstCaps *sample_caps;
const char *format;
GstStructure *s;
int width;
int height;
data = g_new0 (ScreenshotData, 1);
data->cb = cb;
data->user_data = user_data;
sink = gst_bin_get_by_name (GST_BIN(playbin), "sink");
if (sink == NULL) {
g_warning ("Could not take screenshot: %s", "no sink on playbin");
screenshot_data_finalize (data);
return FALSE;
}
sample = NULL;
g_object_get (sink, "last-sample", &sample, NULL);
g_object_unref (sink);
if (sample == NULL) {
g_warning ("Could not take screenshot: %s", "failed to retrieve video frame");
screenshot_data_finalize (data);
return FALSE;
}
sample_caps = gst_sample_get_caps (sample);
if (sample_caps == NULL) {
g_warning ("Could not take screenshot: %s", "no caps on output buffer");
screenshot_data_finalize (data);
return FALSE;
}
s = gst_caps_get_structure (sample_caps, 0);
format = gst_structure_get_string (s, "format");
/*g_print ("cap: %s\n", gst_caps_to_string (sample_caps));
g_print ("format: %s\n", format);*/
if (! _g_str_equal (format, "RGB") && ! _g_str_equal (format, "RGBA")) {
GstCaps *to_caps;
GstSample *to_sample;
GError *error = NULL;
/* our desired output format (RGB24) */
to_caps = gst_caps_new_simple ("video/x-raw",
"format", G_TYPE_STRING, "RGB",
/* Note: we don't ask for a specific width/height here, so that
* videoscale can adjust dimensions from a non-1/1 pixel aspect
* ratio to a 1/1 pixel-aspect-ratio. We also don't ask for a
* specific framerate, because the input framerate won't
* necessarily match the output framerate if there's a deinterlacer
* in the pipeline. */
"pixel-aspect-ratio", GST_TYPE_FRACTION, 1, 1,
NULL);
to_sample = gst_video_convert_sample (sample, to_caps, GST_CLOCK_TIME_NONE, &error);
gst_caps_unref (to_caps);
gst_sample_unref (sample);
if (to_sample == NULL) {
g_warning ("Could not take screenshot: %s", (error != NULL) ? error->message : "failed to convert video frame");
g_clear_error (&error);
screenshot_data_finalize (data);
return FALSE;
}
sample = to_sample;
}
sample_caps = gst_sample_get_caps (sample);
if (sample_caps == NULL) {
g_warning ("Could not take screenshot: %s", "no caps on output buffer");
screenshot_data_finalize (data);
return FALSE;
}
/*g_print ("cap: %s\n", gst_caps_to_string (sample_caps));*/
s = gst_caps_get_structure (sample_caps, 0);
gst_structure_get_int (s, "width", &width);
gst_structure_get_int (s, "height", &height);
format = gst_structure_get_string (s, "format");
if (! _g_str_equal (format, "RGB") && ! _g_str_equal (format, "RGBA")) {
g_warning ("Could not take screenshot: %s", "wrong format");
screenshot_data_finalize (data);
return FALSE;
}
if ((width > 0) && (height > 0)) {
GstMemory *memory;
GstMapInfo info;
gboolean with_alpha = _g_str_equal (format, "RGBA");
memory = gst_buffer_get_memory (gst_sample_get_buffer (sample), 0);
if (gst_memory_map (memory, &info, GST_MAP_READ))
data->pixbuf = gdk_pixbuf_new_from_data (info.data,
GDK_COLORSPACE_RGB,
with_alpha,
8,
width,
height,
GST_ROUND_UP_4 (width * (with_alpha ? 4 : 3)),
destroy_pixbuf,
sample);
gst_memory_unmap (memory, &info);
gst_memory_unref (memory);
}
if (data->pixbuf == NULL) {
gst_sample_unref (sample);
g_warning ("Could not take screenshot: %s", "could not create pixbuf");
}
screenshot_data_finalize (data);
return TRUE;
}