From 9fec222b5aa8bcc24660a482e21bce91beb79fe9 Mon Sep 17 00:00:00 2001 From: Andoni Morales Alastruey Date: Sat, 11 May 2013 03:41:36 +0200 Subject: [PATCH] applemedia: add new source/decoder element for iOS assets https://bugzilla.gnome.org/show_bug.cgi?id=728249 --- sys/applemedia/Makefile.am | 6 +- sys/applemedia/iosavassetsrc.h | 128 +++++ sys/applemedia/iosavassetsrc.m | 1046 ++++++++++++++++++++++++++++++++++++++++ sys/applemedia/plugin.m | 3 + 4 files changed, 1181 insertions(+), 2 deletions(-) create mode 100644 sys/applemedia/iosavassetsrc.h create mode 100644 sys/applemedia/iosavassetsrc.m diff --git a/sys/applemedia/Makefile.am b/sys/applemedia/Makefile.am index f1e2c8a..7c7f87f 100644 --- a/sys/applemedia/Makefile.am +++ b/sys/applemedia/Makefile.am @@ -73,12 +73,14 @@ noinst_HEADERS = \ atdec.h \ dynapi.h \ dynapi-internal.h \ - iosassetsrc.h + iosassetsrc.h \ + iosavassetsrc.h if HAVE_IOS libgstapplemedia_la_SOURCES += \ - iosassetsrc.m + iosassetsrc.m \ + iosavassetsrc.m libgstapplemedia_la_LDFLAGS += \ -Wl,-framework -Wl,Foundation \ diff --git a/sys/applemedia/iosavassetsrc.h b/sys/applemedia/iosavassetsrc.h new file mode 100644 index 0000000..9f374d0 --- /dev/null +++ b/sys/applemedia/iosavassetsrc.h @@ -0,0 +1,128 @@ +/* + * GStreamer + * Copyright (C) 2013 Fluendo S.L. + * Authors: Andoni Morales Alastruey + * + * 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; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GST_AVASSET_SRC_H__ +#define __GST_AVASSET_SRC_H__ + +#include +#import +#import + +G_BEGIN_DECLS + +#define GST_TYPE_AVASSET_SRC \ + (gst_avasset_src_get_type()) +#define GST_AVASSET_SRC(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AVASSET_SRC,GstAVAssetSrc)) +#define GST_AVASSET_SRC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AVASSET_SRC,GstAVAssetSrcClass)) +#define GST_IS_AVASSET_SRC(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AVASSET_SRC)) +#define GST_IS_AVASSET_SRC_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AVASSET_SRC)) +#define GST_AVASSET_SRC_ERROR gst_avasset_src_error_quark () + +typedef struct _GstAVAssetSrc GstAVAssetSrc; +typedef struct _GstAVAssetSrcClass GstAVAssetSrcClass; + +typedef enum +{ + GST_AVASSET_READER_MEDIA_TYPE_AUDIO, + GST_AVASSET_READER_MEDIA_TYPE_VIDEO, +} GstAVAssetReaderMediaType; + +typedef enum +{ + GST_AVASSET_ERROR_NOT_PLAYABLE, + GST_AVASSET_ERROR_INIT, + GST_AVASSET_ERROR_START, + GST_AVASSET_ERROR_READ, +} GstAVAssetError; + +typedef enum +{ + GST_AVASSET_SRC_STATE_STOPPED, + GST_AVASSET_SRC_STATE_STARTED, + GST_AVASSET_SRC_STATE_READING, +} GstAVAssetSrcState; + +@interface GstAVAssetReader: NSObject +{ + AVAsset *asset; + AVAssetReader *reader; + AVAssetReaderTrackOutput *video_track; + AVAssetReaderTrackOutput *audio_track; + NSArray *audio_tracks; + NSArray *video_tracks; + int selected_audio_track; + int selected_video_track; + GstCaps *audio_caps; + GstCaps *video_caps; + gboolean reading; + GstClockTime duration; + GstClockTime position; +} + +@property GstClockTime duration; +@property GstClockTime position; + +- (id) initWithURI:(gchar*) uri : (GError **) error; +- (void) start : (GError **) error; +- (void) stop; +- (void) seekTo: (guint64) start : (guint64) stop : (GError **) error; +- (bool) hasMediaType: (GstAVAssetReaderMediaType) type; +- (GstCaps *) getCaps: (GstAVAssetReaderMediaType) type; +- (bool) selectTrack: (GstAVAssetReaderMediaType) type : (gint) index; +- (GstBuffer *) nextBuffer: (GstAVAssetReaderMediaType) type : (GError **) error; +@end + +struct _GstAVAssetSrc +{ + GstElement element; + + GstPad *videopad; + GstPad *audiopad; + GstTask *video_task; + GstTask *audio_task; + GStaticRecMutex video_lock; + GStaticRecMutex audio_lock; + gint selected_video_track; + gint selected_audio_track; + + GstAVAssetReader *reader; + GstAVAssetSrcState state; + GMutex lock; + GstEvent *seek_event; + + /* Properties */ + gchar * uri; +}; + +struct _GstAVAssetSrcClass +{ + GstElementClass parent_class; +}; + +GType gst_avasset_src_get_type (void); + +G_END_DECLS + +#endif /* __GST_AVASSET_SRC_H__ */ diff --git a/sys/applemedia/iosavassetsrc.m b/sys/applemedia/iosavassetsrc.m new file mode 100644 index 0000000..4377f0e --- /dev/null +++ b/sys/applemedia/iosavassetsrc.m @@ -0,0 +1,1046 @@ +/* + * GStreamer + * Copyright (C) 2013 Fluendo S.L. + * Authors: Andoni Morales Alastruey + * + * 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; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/** + * SECTION:element-plugin + * + * Read and decode samples from iOS assets using the AVAssetReader API. + * + * + * Example launch line + * |[ + * gst-launch -v -m iosavassetsrc uri="file://movie.mp4" ! autovideosink + * ]| + * + */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include + +#include "iosavassetsrc.h" +#include "coremediabuffer.h" +#import + +GST_DEBUG_CATEGORY_STATIC (gst_avasset_src_debug); +#define GST_CAT_DEFAULT gst_avasset_src_debug + +#define CMTIME_TO_GST_TIME(x) \ + (x.value == 0 ? 0 : (guint64)(x.value * GST_SECOND / x.timescale)); +#define GST_AVASSET_SRC_LOCK(x) (g_mutex_lock (&x->lock)); +#define GST_AVASSET_SRC_UNLOCK(x) (g_mutex_unlock (&x->lock)); +#define MEDIA_TYPE_TO_STR(x) \ + (x == GST_AVASSET_READER_MEDIA_TYPE_AUDIO ? "audio" : "video") +#define AVASSET_READER_HAS_AUDIO(x) \ + ([self->reader hasMediaType:GST_AVASSET_READER_MEDIA_TYPE_AUDIO]) +#define AVASSET_READER_HAS_VIDEO(x) \ + ([self->reader hasMediaType:GST_AVASSET_READER_MEDIA_TYPE_VIDEO]) +#define OBJC_CALLOUT_BEGIN() \ + NSAutoreleasePool *pool; \ + \ + pool = [[NSAutoreleasePool alloc] init] +#define OBJC_CALLOUT_END() \ + [pool release] + +enum +{ + PROP_0, + PROP_URI +}; + +#define COMMON_CAPS "endianness = (int) {" G_STRINGIFY (G_BYTE_ORDER) " }, " \ + "signed = (boolean) { TRUE }, " \ + "rate = (int) [1, MAX], " \ + "channels = (int) [1, 2];" + +static GstStaticPadTemplate audio_factory = GST_STATIC_PAD_TEMPLATE ("audio", + GST_PAD_SRC, + GST_PAD_SOMETIMES, + GST_STATIC_CAPS ( + "audio/x-raw-float, width = (int) 32, depth = (int) 32, " COMMON_CAPS) + ); + +static GstStaticPadTemplate video_factory = GST_STATIC_PAD_TEMPLATE ("video", + GST_PAD_SRC, + GST_PAD_SOMETIMES, + GST_STATIC_CAPS ("video/x-raw-yuv, format=(fourcc)NV12, " + "framerate = (fraction) [ 0, MAX ], " + "width = (int) [1, MAX], " + "height = (int) [1, MAX]") + ); + +static void gst_avasset_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec); +static void gst_avasset_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec); +static void gst_avasset_src_dispose (GObject *object); + +static GstStateChangeReturn gst_avasset_src_change_state (GstElement * element, + GstStateChange transition); + +static gboolean gst_avasset_src_query (GstPad *pad, GstQuery *query); +static gboolean gst_avasset_src_event (GstPad *pad, GstEvent *event); +static gboolean gst_avasset_src_send_event (GstAVAssetSrc *self, + GstEvent *event); + +static void gst_avasset_src_read_audio (GstAVAssetSrc *self); +static void gst_avasset_src_read_video (GstAVAssetSrc *self); +static void gst_avasset_src_start (GstAVAssetSrc *self); +static void gst_avasset_src_stop (GstAVAssetSrc *self); +static void gst_avasset_src_start_reading (GstAVAssetSrc *self); +static void gst_avasset_src_stop_reading (GstAVAssetSrc *self); +static void gst_avasset_src_stop_all (GstAVAssetSrc *self); +static void gst_avasset_src_uri_handler_init (gpointer g_iface, + gpointer iface_data); + +static void +_do_init (GType avassetsrc_type) +{ + static const GInterfaceInfo urihandler_info = { + gst_avasset_src_uri_handler_init, + NULL, + NULL + }; + + g_type_add_interface_static (avassetsrc_type, GST_TYPE_URI_HANDLER, + &urihandler_info); + GST_DEBUG_CATEGORY_INIT (gst_avasset_src_debug, "iosavassetsrc", + 0, "iosavassetsrc element"); +} + +GST_BOILERPLATE_FULL (GstAVAssetSrc, gst_avasset_src, GstElement, + GST_TYPE_ELEMENT, _do_init); + +/* GObject vmethod implementations */ + +static void +gst_avasset_src_base_init (gpointer gclass) +{ + GstElementClass *element_class = GST_ELEMENT_CLASS (gclass); + + gst_element_class_set_details_simple(element_class, + "Source and decoder for iOS assets", + "Source/Codec", + "Read and decode samples from iOS assets using the AVAssetReader API", + "Andoni Morales Alastruey amorales@fluendo.com"); + + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&audio_factory)); + gst_element_class_add_pad_template (element_class, + gst_static_pad_template_get (&video_factory)); +} + +static void +gst_avasset_src_class_init (GstAVAssetSrcClass * klass) +{ + GObjectClass *gobject_class; + GstElementClass *gstelement_class; + + gobject_class = (GObjectClass *) klass; + gstelement_class = (GstElementClass *) klass; + + gobject_class->set_property = gst_avasset_src_set_property; + gobject_class->get_property = gst_avasset_src_get_property; + gobject_class->dispose = gst_avasset_src_dispose; + + /** + * GstAVAssetSrc:uri + * + * URI of the asset to read + * + **/ + g_object_class_install_property (gobject_class, PROP_URI, + g_param_spec_string ("uri", "Asset URI", + "URI of the asset to read", NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | + GST_PARAM_MUTABLE_READY)); + + gstelement_class->change_state = gst_avasset_src_change_state; + +} + +static void +gst_avasset_src_init (GstAVAssetSrc * self, GstAVAssetSrcClass * gclass) +{ + self->audio_task = gst_task_create ( + (GstTaskFunction) gst_avasset_src_read_audio, self); + self->video_task = gst_task_create ( + (GstTaskFunction) gst_avasset_src_read_video, self); + gst_task_set_lock (self->video_task, &self->video_lock); + gst_task_set_lock (self->audio_task, &self->audio_lock); + self->selected_audio_track = 0; + self->selected_video_track = 0; + g_mutex_init(&self->lock); +} + +static void +gst_avasset_src_dispose (GObject *object) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (object); + + if (self->video_task != NULL) { + gst_object_unref (self->video_task); + self->video_task = NULL; + } + + if (self->audio_task) { + gst_object_unref (self->audio_task); + self->audio_task = NULL; + } + + if (self->uri != NULL) { + g_free (self->uri); + self->uri = NULL; + } + + if (self->seek_event) { + gst_event_unref (self->seek_event); + self->seek_event = NULL; + } +} + +static void +gst_avasset_src_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (object); + + switch (prop_id) { + case PROP_URI: + if (self->uri) { + g_free (self->uri); + } + self->uri = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_avasset_src_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (object); + + switch (prop_id) { + case PROP_URI: + g_value_set_string (value, self->uri); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static GstStateChangeReturn +gst_avasset_src_change_state (GstElement * element, GstStateChange transition) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (element); + GstStateChangeReturn ret; + GError *error; + + OBJC_CALLOUT_BEGIN (); + switch (transition) { + case GST_STATE_CHANGE_NULL_TO_READY: { + self->state = GST_AVASSET_SRC_STATE_STOPPED; + self->reader = [[GstAVAssetReader alloc] initWithURI:self->uri:&error]; + if (error) { + GST_ELEMENT_ERROR (element, RESOURCE, FAILED, ("AVAssetReader error"), + ("%s", error->message)); + g_error_free (error); + gst_avasset_src_stop_all (self); + return GST_STATE_CHANGE_FAILURE; + } + break; + } + case GST_STATE_CHANGE_READY_TO_PAUSED: + gst_avasset_src_start (self); + self->state = GST_AVASSET_SRC_STATE_STARTED; + gst_avasset_src_start_reading (self); + self->state = GST_AVASSET_SRC_STATE_READING; + break; + case GST_STATE_CHANGE_PAUSED_TO_PLAYING: + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition); + + switch (transition) { + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + break; + case GST_STATE_CHANGE_PAUSED_TO_READY: + gst_avasset_src_stop_all (self); + break; + case GST_STATE_CHANGE_READY_TO_NULL: + [self->reader release]; + break; + default: + break; + } + OBJC_CALLOUT_END (); + return ret; +} + +static gboolean +gst_avasset_src_query (GstPad *pad, GstQuery *query) +{ + gboolean ret; + GstAVAssetSrc *self = GST_AVASSET_SRC (gst_pad_get_parent_element(pad)); + + switch (GST_QUERY_TYPE (query)) { + case GST_QUERY_URI: + gst_query_set_uri (query, self->uri); + ret = TRUE; + break; + case GST_QUERY_DURATION: + gst_query_set_duration (query, GST_FORMAT_TIME, self->reader.duration); + ret = TRUE; + break; + case GST_QUERY_POSITION: + gst_query_set_position (query, GST_FORMAT_TIME, self->reader.position); + ret = TRUE; + break; + default: + ret = FALSE; + break; + } + + g_object_unref(self); + return ret; +} + +static gboolean +gst_avasset_src_event (GstPad * pad, GstEvent * event) +{ + GstAVAssetSrc *self; + gboolean res = TRUE; + GError *error = NULL; + + OBJC_CALLOUT_BEGIN (); + self = GST_AVASSET_SRC (gst_pad_get_parent_element (pad)); + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_SEEK: { + GstFormat format; + GstSeekFlags flags; + gdouble rate; + GstSeekType start_type, stop_type; + gint64 start, stop; + GstEvent *newsegment; + + /* now do the seek */ + GST_AVASSET_SRC_LOCK (self); + if (self->seek_event && gst_event_get_seqnum (event) == + gst_event_get_seqnum (self->seek_event)) { + GST_AVASSET_SRC_UNLOCK (self); + break; + } + self->seek_event = gst_event_ref (event); + gst_event_parse_seek (event, &rate, &format, &flags, &start_type, + &start, &stop_type, &stop); + + if (rate < 0) { + GST_WARNING ("Negative rates are not supported yet"); + GST_AVASSET_SRC_UNLOCK (self); + res = FALSE; + break; + } + + gst_avasset_src_stop_reading (self); + if (format != GST_FORMAT_TIME || start_type == GST_SEEK_TYPE_NONE) { + GST_AVASSET_SRC_UNLOCK(self); + res = FALSE; + break; + } + if (stop_type == GST_SEEK_TYPE_NONE) { + stop = GST_CLOCK_TIME_NONE; + } + gst_avasset_src_send_event (self, gst_event_new_flush_start ()); + [self->reader seekTo: start: stop: &error]; + newsegment = gst_event_new_new_segment (FALSE, rate, GST_FORMAT_TIME, + start, stop, start); + gst_avasset_src_send_event (self, gst_event_new_flush_stop ()); + gst_avasset_src_send_event (self, newsegment); + + if (error != NULL) { + GST_ELEMENT_ERROR (self, RESOURCE, SEEK, + ("AVAssetReader seek failed"), ("%s", error->message)); + g_error_free(error); + res = FALSE; + } + gst_avasset_src_start_reading (self); + GST_AVASSET_SRC_UNLOCK (self); + gst_event_unref (event); + break; + } + default: + res = gst_pad_event_default (pad, event); + break; + } + + gst_object_unref (self); + OBJC_CALLOUT_END (); + return res; +} + + +static void +gst_avasset_src_read_data (GstAVAssetSrc *self, GstPad *pad, + GstAVAssetReaderMediaType type) +{ + GstBuffer *buf; + GstFlowReturn ret; + GError *error; + + OBJC_CALLOUT_BEGIN (); + + GST_AVASSET_SRC_LOCK (self); + if (self->state == GST_AVASSET_SRC_STATE_STOPPED) { + GST_AVASSET_SRC_UNLOCK (self); + goto exit; + } + + buf = [self->reader nextBuffer:type:&error]; + GST_AVASSET_SRC_UNLOCK (self); + + if (buf == NULL) { + if (error != NULL) { + GST_ELEMENT_ERROR (self, RESOURCE, READ, ("Error reading next buffer"), + ("%s", error->message)); + g_error_free (error); + gst_avasset_src_stop_all (self); + goto exit; + } + gst_pad_push_event (pad, gst_event_new_eos ()); + gst_avasset_src_stop_all (self); + goto exit; + } + + ret = gst_pad_push (pad, buf); + if (GST_FLOW_IS_FATAL (ret)) { + GST_ELEMENT_ERROR (self, STREAM, FAILED, NULL, + ("Error pushing %s buffer on pad", MEDIA_TYPE_TO_STR (type))); + gst_avasset_src_stop_all (self); + } + +exit: + OBJC_CALLOUT_END (); +} + +static void +gst_avasset_src_read_audio (GstAVAssetSrc *self) +{ + gst_avasset_src_read_data (self, self->audiopad, + GST_AVASSET_READER_MEDIA_TYPE_AUDIO); +} + +static void +gst_avasset_src_read_video (GstAVAssetSrc *self) +{ + gst_avasset_src_read_data (self, self->videopad, + GST_AVASSET_READER_MEDIA_TYPE_VIDEO); +} + +static gboolean +gst_avasset_src_start_task (GstAVAssetSrc *self, GstTask *task) +{ + GError *error = NULL; + gboolean ret = TRUE; + + OBJC_CALLOUT_BEGIN (); + + [self->reader start: &error]; + if (error != NULL) { + GST_ELEMENT_ERROR (self, RESOURCE, FAILED, + ("AVAssetReader could not start reading"), ("%s", error->message)); + g_error_free (error); + gst_avasset_src_stop_all (self); + ret = FALSE; + goto exit; + } + gst_task_start (task); + +exit: + OBJC_CALLOUT_END (); + return ret; +} + +static GstPadLinkReturn +gst_avasset_src_audio_pad_link (GstPad *pad, GstPad *peer) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (gst_pad_get_parent_element (pad)); + + if (!gst_avasset_src_start_task (self, self->audio_task)) { + return GST_PAD_LINK_REFUSED; + } + GST_DEBUG ("Started audio streaming task"); + g_object_unref (self); + + return GST_PAD_LINK_OK; +} + +static GstPadLinkReturn +gst_avasset_src_video_pad_link (GstPad *pad, GstPad *peer) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (gst_pad_get_parent_element (pad)); + + if (!gst_avasset_src_start_task (self, self->video_task)) { + return GST_PAD_LINK_REFUSED; + } + GST_DEBUG ("Started video streaming task"); + g_object_unref (self); + + return GST_PAD_LINK_OK; +} + +static gboolean +gst_avasset_src_send_event (GstAVAssetSrc *self, GstEvent *event) +{ + gboolean ret = TRUE; + + OBJC_CALLOUT_BEGIN (); + + if (AVASSET_READER_HAS_VIDEO (self)) { + ret |= gst_pad_push_event (self->videopad, gst_event_ref (event)); + } + if (AVASSET_READER_HAS_AUDIO (self)) { + ret |= gst_pad_push_event (self->audiopad, gst_event_ref (event)); + } + + gst_event_unref (event); + OBJC_CALLOUT_END (); + return ret; +} + +static void +gst_avasset_src_start (GstAVAssetSrc *self) +{ + OBJC_CALLOUT_BEGIN (); + if (self->state == GST_AVASSET_SRC_STATE_STARTED) { + goto exit; + } + + GST_DEBUG_OBJECT (self, "Creating pads and starting reader"); + + /* We call AVAssetReader's startReading when the pads are linked + * and no outputs can be added afterwards, so the tracks must be + * selected before adding any of the new pads */ + if (AVASSET_READER_HAS_AUDIO (self)) { + [self->reader selectTrack: GST_AVASSET_READER_MEDIA_TYPE_AUDIO: + self->selected_audio_track]; + } + if (AVASSET_READER_HAS_VIDEO (self)) { + [self->reader selectTrack: GST_AVASSET_READER_MEDIA_TYPE_VIDEO: + self->selected_video_track]; + } + + if (AVASSET_READER_HAS_AUDIO (self)) { + self->audiopad = gst_pad_new_from_static_template (&audio_factory, "audio"); + gst_pad_set_query_function (self->audiopad, + (GstPadQueryFunction) gst_avasset_src_query); + gst_pad_set_event_function(self->audiopad, + (GstPadEventFunction) gst_avasset_src_event); + gst_pad_set_link_function(self->audiopad, + (GstPadLinkFunction) gst_avasset_src_audio_pad_link); + gst_pad_set_active (self->audiopad, TRUE); + gst_pad_set_caps (self->audiopad, + [self->reader getCaps: GST_AVASSET_READER_MEDIA_TYPE_AUDIO]); + gst_element_add_pad (GST_ELEMENT (self), self->audiopad); + } + if (AVASSET_READER_HAS_VIDEO (self)) { + self->videopad = gst_pad_new_from_static_template (&video_factory, "video"); + gst_pad_set_query_function (self->videopad, + (GstPadQueryFunction) gst_avasset_src_query); + gst_pad_set_event_function(self->videopad, + (GstPadEventFunction) gst_avasset_src_event); + gst_pad_set_link_function(self->videopad, + (GstPadLinkFunction) gst_avasset_src_video_pad_link); + gst_pad_set_active (self->videopad, TRUE); + gst_pad_set_caps (self->videopad, + [self->reader getCaps: GST_AVASSET_READER_MEDIA_TYPE_VIDEO]); + gst_element_add_pad (GST_ELEMENT (self), self->videopad); + } + gst_element_no_more_pads (GST_ELEMENT (self)); + +exit: + OBJC_CALLOUT_END (); +} + +static void +gst_avasset_src_stop (GstAVAssetSrc *self) +{ + gboolean has_audio, has_video; + OBJC_CALLOUT_BEGIN(); + + if (self->state == GST_AVASSET_SRC_STATE_STOPPED) { + goto exit; + } + + GST_DEBUG ("Stop reading"); + + has_audio = AVASSET_READER_HAS_AUDIO (self); + has_video = AVASSET_READER_HAS_VIDEO (self); + [self->reader stop]; + + if (has_audio) { + gst_element_remove_pad (GST_ELEMENT (self), self->audiopad); + gst_task_stop (self->audio_task); + } + if (has_video) { + gst_element_remove_pad (GST_ELEMENT (self), self->videopad); + gst_task_stop (self->video_task); + } + +exit: + OBJC_CALLOUT_END (); +} + +static void +gst_avasset_src_start_reading (GstAVAssetSrc *self) +{ + /* Only start the task here if we were playing before, otherwise wait until + * the pad are linked to start the streaming task. + */ + + if (self->state < GST_AVASSET_SRC_STATE_STARTED) { + return; + } + + GST_DEBUG_OBJECT (self, "Start reading"); + + if (GST_TASK_STATE (self->audio_task) == GST_TASK_PAUSED) { + gst_task_start (self->audio_task); + } + if (GST_TASK_STATE (self->video_task) == GST_TASK_PAUSED) { + gst_task_start (self->video_task); + } +} + +static void +gst_avasset_src_stop_reading (GstAVAssetSrc *self) +{ + if (self->state != GST_AVASSET_SRC_STATE_READING) { + return; + } + + GST_DEBUG_OBJECT (self, "Stop reading"); + + if (GST_TASK_STATE (self->audio_task) == GST_TASK_STARTED) { + gst_task_pause (self->audio_task); + } + if (GST_TASK_STATE (self->video_task) == GST_TASK_STARTED) { + gst_task_pause (self->video_task); + } +} + +static void +gst_avasset_src_stop_all (GstAVAssetSrc *self) +{ + GST_AVASSET_SRC_LOCK (self); + gst_avasset_src_stop_reading (self); + gst_avasset_src_stop (self); + self->state = GST_AVASSET_SRC_STATE_STOPPED; + GST_AVASSET_SRC_UNLOCK (self); +} + +static GQuark +gst_avasset_src_error_quark (void) +{ + static GQuark q; /* 0 */ + + if (G_UNLIKELY (q == 0)) { + q = g_quark_from_static_string ("avasset-src-error-quark"); + } + return q; +} + +static GstURIType +gst_avasset_src_uri_get_type (void) +{ + return GST_URI_SRC; +} + +static gchar ** +gst_avasset_src_uri_get_protocols (void) +{ + static gchar *protocols[] = { (gchar *) "file", + (gchar *) "ipod-library", NULL }; + + return protocols; +} + +static const gchar * +gst_avasset_src_uri_get_uri (GstURIHandler * handler) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (handler); + + return self->uri; +} + +static gboolean +gst_avasset_src_uri_set_uri (GstURIHandler * handler, const gchar * uri) +{ + GstAVAssetSrc *self = GST_AVASSET_SRC (handler); + NSString *str; + NSURL *url; + AVAsset *asset; + gchar *escaped_uri; + gboolean ret = FALSE; + + OBJC_CALLOUT_BEGIN (); + escaped_uri = g_uri_escape_string (uri, ":/", TRUE); + str = [NSString stringWithUTF8String: escaped_uri]; + url = [[NSURL alloc] initWithString: str]; + asset = [AVAsset assetWithURL: url]; + g_free (escaped_uri); + + if (asset.playable) { + ret = TRUE; + if (self->uri) { + g_free (self->uri); + } + self->uri = g_strdup (uri); + } + OBJC_CALLOUT_END (); + return ret; +} + +static void +gst_avasset_src_uri_handler_init (gpointer g_iface, gpointer iface_data) +{ + GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface; + + iface->get_type = gst_avasset_src_uri_get_type; + iface->get_protocols = gst_avasset_src_uri_get_protocols; + iface->get_uri = gst_avasset_src_uri_get_uri; + iface->set_uri = gst_avasset_src_uri_set_uri; +} + +@implementation GstAVAssetReader + +@synthesize duration; +@synthesize position; + +- (NSDictionary *) capsToAudioSettings +{ + gint samplerate, channels, depth, width; + gboolean isFloat; + GstStructure *s; + + if (!gst_caps_is_fixed (audio_caps)) + return NULL; + + s = gst_caps_get_structure (audio_caps, 0); + isFloat = gst_structure_has_name (s, "audio/x-raw-float"); + gst_structure_get_int (s, "rate", &samplerate); + gst_structure_get_int (s, "depth", &depth); + gst_structure_get_int (s, "width", &width); + gst_structure_get_int (s, "channels", &channels); + + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:kAudioFormatLinearPCM], AVFormatIDKey, + [NSNumber numberWithFloat: samplerate], AVSampleRateKey, + [NSNumber numberWithInt:channels], AVNumberOfChannelsKey, + //[NSData dataWithBytes:&channelLayout length:sizeof(AudioChannelLayout)], + //AVChannelLayoutKey, + [NSNumber numberWithInt:depth], AVLinearPCMBitDepthKey, + [NSNumber numberWithBool:isFloat],AVLinearPCMIsFloatKey, + [NSNumber numberWithBool:NO], AVLinearPCMIsNonInterleaved, + [NSNumber numberWithBool:NO], AVLinearPCMIsBigEndianKey, + nil]; +} + +- (void) releaseReader +{ + [video_track release]; + [audio_track release]; + [video_tracks release]; + [audio_tracks release]; + [reader release]; +} + +- (void) initReader: (GError **) error +{ + NSError *nserror; + + reader = [[AVAssetReader alloc] initWithAsset:asset error:&nserror]; + if (nserror != NULL) { + GST_ERROR ("Error initializing reader: %s", + [nserror.description UTF8String]); + *error = g_error_new (GST_AVASSET_SRC_ERROR, GST_AVASSET_ERROR_INIT, "%s", + [nserror.description UTF8String]); + [asset release]; + [reader release]; + return; + } + + audio_tracks = [[asset tracksWithMediaType:AVMediaTypeAudio] retain]; + video_tracks = [[asset tracksWithMediaType:AVMediaTypeVideo] retain]; + reader.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration); + GST_INFO ("Found %d video tracks and %d audio tracks", + [video_tracks count], [audio_tracks count]); +} + +- (id) initWithURI:(gchar*)uri : (GError **)error; +{ + NSString *str; + NSURL *url; + gchar *escaped_uri; + + GST_INFO ("Initializing AVAssetReader with uri:%s", uri); + *error = NULL; + + escaped_uri = g_uri_escape_string (uri, ":/", TRUE); + str = [NSString stringWithUTF8String: escaped_uri]; + url = [[NSURL alloc] initWithString: str]; + asset = [[AVAsset assetWithURL: url] retain]; + g_free (escaped_uri); + + if (!asset.playable) { + *error = g_error_new (GST_AVASSET_SRC_ERROR, GST_AVASSET_ERROR_NOT_PLAYABLE, + "Media is not playable"); + [asset release]; + return nil; + } + + selected_audio_track = -1; + selected_video_track = -1; + reading = FALSE; + position = 0; + duration = CMTIME_TO_GST_TIME (asset.duration); + + /* FIXME: use fixed caps here until we found a way to determine + * the native audio format */ + audio_caps = gst_caps_from_string ("audio/x-raw-float, " + "endianness = (int) 1234, signed = (boolean) TRUE, " + "rate = (int) 44100, channels = (int) 2, " + "width= (int) 32, depth = (int) 32"); + + [self initReader: error]; + if (*error) { + return nil; + } + + self = [super init]; + return self; +} + +- (bool) selectTrack: (GstAVAssetReaderMediaType) type : (gint) index +{ + NSArray *tracks; + AVAssetTrack *track; + AVAssetReaderOutput **output; + NSDictionary *settings; + NSString *mediaType; + gint *selected_track; + + GST_INFO ("Selecting %s track %d", MEDIA_TYPE_TO_STR(type), index); + + if (type == GST_AVASSET_READER_MEDIA_TYPE_AUDIO) { + mediaType = AVMediaTypeAudio; + selected_track = &selected_audio_track; + output = &audio_track; + settings = [self capsToAudioSettings]; + } else if (type == GST_AVASSET_READER_MEDIA_TYPE_VIDEO) { + mediaType = AVMediaTypeVideo; + selected_track = &selected_video_track; + output = &video_track; + settings = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt: + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], + kCVPixelBufferPixelFormatTypeKey, nil]; + } else { + return FALSE; + } + + tracks = [asset tracksWithMediaType:mediaType]; + if ([tracks count] == 0 || [tracks count] < index + 1) { + return FALSE; + } + + track = [tracks objectAtIndex:index]; + *selected_track = index; + *output = [AVAssetReaderTrackOutput + assetReaderTrackOutputWithTrack:track + outputSettings:settings]; + [*output retain]; + [reader addOutput:*output]; + return TRUE; +} + +- (void) start: (GError **)error +{ + if (reading) + return; + + if (![reader startReading]) { + *error = g_error_new (GST_AVASSET_SRC_ERROR, GST_AVASSET_ERROR_START, + "%s", [reader.error.description UTF8String]); + reading = FALSE; + return; + } + reading = TRUE; +} + +- (void) stop +{ + [self->reader cancelReading]; + reading = FALSE; +} + +- (bool) hasMediaType: (GstAVAssetReaderMediaType) type +{ + if (type == GST_AVASSET_READER_MEDIA_TYPE_AUDIO) { + return [audio_tracks count] != 0; + } + if (type == GST_AVASSET_READER_MEDIA_TYPE_VIDEO) { + return [video_tracks count] != 0; + } + return FALSE; +} + +- (void) seekTo: (guint64) startTS : (guint64) stopTS : (GError **) error +{ + CMTime startTime = kCMTimeZero, stopTime = kCMTimePositiveInfinity; + + if (startTS != GST_CLOCK_TIME_NONE) { + startTime = CMTimeMake (startTS, GST_SECOND); + } + if (stopTS != GST_CLOCK_TIME_NONE) { + stopTime = CMTimeMake (stopTS, GST_SECOND); + } + + /* AVAssetReader needs to be recreated before changing the + * timerange property */ + [self stop]; + [self releaseReader]; + [self initReader: error]; + if (*error) { + return; + } + + GST_DEBUG ("Seeking to start:%" GST_TIME_FORMAT " stop:%" GST_TIME_FORMAT, + GST_TIME_ARGS(startTS), GST_TIME_ARGS(stopTS)); + + reader.timeRange = CMTimeRangeMake(startTime, stopTime); + [self selectTrack: GST_AVASSET_READER_MEDIA_TYPE_AUDIO:selected_audio_track]; + [self selectTrack: GST_AVASSET_READER_MEDIA_TYPE_VIDEO:selected_video_track]; + [self start: error]; +} + +- (GstBuffer *) nextBuffer: (GstAVAssetReaderMediaType) type : (GError **) error +{ + CMSampleBufferRef cmbuf; + AVAssetReaderTrackOutput *areader = NULL; + GstCaps *caps; + GstBuffer *buf; + CMTime dur, ts; + + GST_LOG ("Reading %s next buffer", MEDIA_TYPE_TO_STR (type)); + if (type == GST_AVASSET_READER_MEDIA_TYPE_AUDIO && audio_track != NULL) { + areader = audio_track; + caps = audio_caps; + } else if (type == GST_AVASSET_READER_MEDIA_TYPE_VIDEO && + video_track != NULL) { + areader = video_track; + caps = video_caps; + } + + if (areader == NULL) { + return NULL; + } + + *error = NULL; + cmbuf = [areader copyNextSampleBuffer]; + if (cmbuf == NULL) { + if (reader.error != NULL) { + *error = g_error_new (GST_AVASSET_SRC_ERROR, GST_AVASSET_ERROR_READ, + "%s", [reader.error.description UTF8String]); + } + /* EOS */ + return NULL; + } + + buf = gst_core_media_buffer_new (cmbuf); + dur = CMSampleBufferGetDuration (cmbuf); + ts = CMSampleBufferGetPresentationTimeStamp (cmbuf); + if (dur.value != 0) { + GST_BUFFER_DURATION (buf) = CMTIME_TO_GST_TIME (dur); + } + GST_BUFFER_TIMESTAMP (buf) = CMTIME_TO_GST_TIME (ts); + gst_buffer_set_caps (buf, caps); + GST_LOG ("Copying next %s buffer ts:%" GST_TIME_FORMAT " dur:%" + GST_TIME_FORMAT, MEDIA_TYPE_TO_STR (type), + GST_TIME_ARGS(GST_BUFFER_TIMESTAMP (buf)), + GST_TIME_ARGS(GST_BUFFER_DURATION (buf))); + if (GST_BUFFER_TIMESTAMP (buf) > position) { + position = GST_BUFFER_TIMESTAMP (buf); + } + return buf; +} + +- (GstCaps *) getCaps: (GstAVAssetReaderMediaType) type +{ + GstCaps *caps = NULL; + AVAssetTrack *track; + + if (type == GST_AVASSET_READER_MEDIA_TYPE_AUDIO) { + caps = gst_caps_ref (audio_caps); + GST_INFO ("Using audio caps: %" GST_PTR_FORMAT, caps); + } else if (type == GST_AVASSET_READER_MEDIA_TYPE_VIDEO) { + gint fr_n, fr_d; + + track = [video_tracks objectAtIndex: selected_video_track]; + gst_util_double_to_fraction(track.nominalFrameRate, &fr_n, &fr_d); + caps = gst_caps_new_simple ("video/x-raw-yuv", + "format", GST_TYPE_FOURCC, GST_MAKE_FOURCC ('N', 'V', '1', '2'), + "width", G_TYPE_INT, (int) track.naturalSize.width, + "height", G_TYPE_INT, (int) track.naturalSize.height, + "framerate", GST_TYPE_FRACTION, fr_n, fr_d, NULL); + GST_INFO ("Using video caps: %" GST_PTR_FORMAT, caps); + video_caps = gst_caps_ref (caps); + } + + return caps; +} + +- (oneway void) release +{ + [asset release]; + + [self releaseReader]; + + if (audio_caps != NULL) { + gst_caps_unref (audio_caps); + } + + if (video_caps != NULL) { + gst_caps_unref (audio_caps); + } +} + +@end diff --git a/sys/applemedia/plugin.m b/sys/applemedia/plugin.m index 098ec90..fdcba1c 100644 --- a/sys/applemedia/plugin.m +++ b/sys/applemedia/plugin.m @@ -24,6 +24,7 @@ #include #ifdef HAVE_IOS #include "iosassetsrc.h" +#include "iosavassetsrc.h" #else #include "qtkitvideosrc.h" #endif @@ -58,6 +59,8 @@ plugin_init (GstPlugin * plugin) #ifdef HAVE_IOS res &= gst_element_register (plugin, "iosassetsrc", GST_RANK_SECONDARY, GST_TYPE_IOS_ASSET_SRC); + res &= gst_element_register (plugin, "iosavassetsrc", GST_RANK_PRIMARY, + GST_TYPE_AVASSET_SRC); #else enable_mt_mode (); -- 2.7.4