Update theme submodule
[platform/upstream/gstreamer.git] / tutorials / xcode iOS / Tutorial 4 / GStreamerBackend.m
1 #import "GStreamerBackend.h"\r
2 \r
3 #include <gst/gst.h>\r
4 #include <gst/video/video.h>\r
5 \r
6 GST_DEBUG_CATEGORY_STATIC (debug_category);\r
7 #define GST_CAT_DEFAULT debug_category\r
8 \r
9 /* Do not allow seeks to be performed closer than this distance. It is visually useless, and will probably\r
10  * confuse some demuxers. */\r
11 #define SEEK_MIN_DELAY (500 * GST_MSECOND)\r
12 \r
13 @interface GStreamerBackend()\r
14 -(void)setUIMessage:(gchar*) message;\r
15 -(void)app_function;\r
16 -(void)check_initialization_complete;\r
17 @end\r
18 \r
19 @implementation GStreamerBackend {\r
20     id ui_delegate;              /* Class that we use to interact with the user interface */\r
21     GstElement *pipeline;        /* The running pipeline */\r
22     GstElement *video_sink;      /* The video sink element which receives XOverlay commands */\r
23     GMainContext *context;       /* GLib context used to run the main loop */\r
24     GMainLoop *main_loop;        /* GLib main loop */\r
25     gboolean initialized;        /* To avoid informing the UI multiple times about the initialization */\r
26     UIView *ui_video_view;       /* UIView that holds the video */\r
27     GstState state;              /* Current pipeline state */\r
28     GstState target_state;       /* Desired pipeline state, to be set once buffering is complete */\r
29     gint64 duration;             /* Cached clip duration */\r
30     gint64 desired_position;     /* Position to seek to, once the pipeline is running */\r
31     GstClockTime last_seek_time; /* For seeking overflow prevention (throttling) */\r
32     gboolean is_live;            /* Live streams do not use buffering */\r
33 }\r
34 \r
35 /*\r
36  * Interface methods\r
37  */\r
38 \r
39 -(id) init:(id) uiDelegate videoView:(UIView *)video_view\r
40 {\r
41     if (self = [super init])\r
42     {\r
43         self->ui_delegate = uiDelegate;\r
44         self->ui_video_view = video_view;\r
45         self->duration = GST_CLOCK_TIME_NONE;\r
46 \r
47         GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-4", 0, "iOS tutorial 4");\r
48         gst_debug_set_threshold_for_name("tutorial-4", GST_LEVEL_DEBUG);\r
49 \r
50         /* Start the bus monitoring task */\r
51         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{\r
52             [self app_function];\r
53         });\r
54     }\r
55 \r
56     return self;\r
57 }\r
58 \r
59 -(void) deinit\r
60 {\r
61     if (main_loop) {\r
62         g_main_loop_quit(main_loop);\r
63     }\r
64 }\r
65 \r
66 -(void) play\r
67 {\r
68     target_state = GST_STATE_PLAYING;\r
69     is_live = (gst_element_set_state (pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);\r
70 }\r
71 \r
72 -(void) pause\r
73 {\r
74     target_state = GST_STATE_PAUSED;\r
75     is_live = (gst_element_set_state (pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);\r
76 }\r
77 \r
78 -(void) setUri:(NSString*)uri\r
79 {\r
80     const char *char_uri = [uri UTF8String];\r
81     g_object_set(pipeline, "uri", char_uri, NULL);\r
82     GST_DEBUG ("URI set to %s", char_uri);\r
83 }\r
84 \r
85 -(void) setPosition:(NSInteger)milliseconds\r
86 {\r
87     gint64 position = (gint64)(milliseconds * GST_MSECOND);\r
88     if (state >= GST_STATE_PAUSED) {\r
89         execute_seek(position, self);\r
90     } else {\r
91         GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));\r
92         self->desired_position = position;\r
93     }\r
94 }\r
95 \r
96 /*\r
97  * Private methods\r
98  */\r
99 \r
100 /* Change the message on the UI through the UI delegate */\r
101 -(void)setUIMessage:(gchar*) message\r
102 {\r
103     NSString *string = [NSString stringWithUTF8String:message];\r
104     if(ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerSetUIMessage:)])\r
105     {\r
106         [ui_delegate gstreamerSetUIMessage:string];\r
107     }\r
108 }\r
109 \r
110 /* Tell the application what is the current position and clip duration */\r
111 -(void) setCurrentUIPosition:(gint)pos duration:(gint)dur\r
112 {\r
113     if(ui_delegate && [ui_delegate respondsToSelector:@selector(setCurrentPosition:duration:)])\r
114     {\r
115         [ui_delegate setCurrentPosition:pos duration:dur];\r
116     }\r
117 }\r
118 \r
119 /* If we have pipeline and it is running, query the current position and clip duration and inform\r
120  * the application */\r
121 static gboolean refresh_ui (GStreamerBackend *self) {\r
122     gint64 position;\r
123 \r
124     /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */\r
125     if (!self || !self->pipeline || self->state < GST_STATE_PAUSED)\r
126         return TRUE;\r
127 \r
128     /* If we didn't know it yet, query the stream duration */\r
129     if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {\r
130         gst_element_query_duration (self->pipeline, GST_FORMAT_TIME, &self->duration);\r
131     }\r
132 \r
133     if (gst_element_query_position (self->pipeline, GST_FORMAT_TIME, &position)) {\r
134         /* The UI expects these values in milliseconds, and GStreamer provides nanoseconds */\r
135         [self setCurrentUIPosition:position / GST_MSECOND duration:self->duration / GST_MSECOND];\r
136     }\r
137     return TRUE;\r
138 }\r
139 \r
140 /* Forward declaration for the delayed seek callback */\r
141 static gboolean delayed_seek_cb (GStreamerBackend *self);\r
142 \r
143 /* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for\r
144  * some time in the future. */\r
145 static void execute_seek (gint64 position, GStreamerBackend *self) {\r
146     gint64 diff;\r
147 \r
148     if (position == GST_CLOCK_TIME_NONE)\r
149         return;\r
150 \r
151     diff = gst_util_get_timestamp () - self->last_seek_time;\r
152 \r
153     if (GST_CLOCK_TIME_IS_VALID (self->last_seek_time) && diff < SEEK_MIN_DELAY) {\r
154         /* The previous seek was too close, delay this one */\r
155         GSource *timeout_source;\r
156 \r
157         if (self->desired_position == GST_CLOCK_TIME_NONE) {\r
158             /* There was no previous seek scheduled. Setup a timer for some time in the future */\r
159             timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);\r
160             g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, (__bridge void *)self, NULL);\r
161             g_source_attach (timeout_source, self->context);\r
162             g_source_unref (timeout_source);\r
163         }\r
164         /* Update the desired seek position. If multiple requests are received before it is time\r
165          * to perform a seek, only the last one is remembered. */\r
166         self->desired_position = position;\r
167         GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,\r
168                    GST_TIME_ARGS (position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));\r
169     } else {\r
170         /* Perform the seek now */\r
171         GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (position));\r
172         self->last_seek_time = gst_util_get_timestamp ();\r
173         gst_element_seek_simple (self->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, position);\r
174         self->desired_position = GST_CLOCK_TIME_NONE;\r
175     }\r
176 }\r
177 \r
178 /* Delayed seek callback. This gets called by the timer setup in the above function. */\r
179 static gboolean delayed_seek_cb (GStreamerBackend *self) {\r
180     GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (self->desired_position));\r
181     execute_seek (self->desired_position, self);\r
182     return FALSE;\r
183 }\r
184 \r
185 /* Retrieve errors from the bus and show them on the UI */\r
186 static void error_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self)\r
187 {\r
188     GError *err;\r
189     gchar *debug_info;\r
190     gchar *message_string;\r
191     \r
192     gst_message_parse_error (msg, &err, &debug_info);\r
193     message_string = g_strdup_printf ("Error received from element %s: %s", GST_OBJECT_NAME (msg->src), err->message);\r
194     g_clear_error (&err);\r
195     g_free (debug_info);\r
196     [self setUIMessage:message_string];\r
197     g_free (message_string);\r
198     gst_element_set_state (self->pipeline, GST_STATE_NULL);\r
199 }\r
200 \r
201 /* Called when the End Of the Stream is reached. Just move to the beginning of the media and pause. */\r
202 static void eos_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {\r
203     self->target_state = GST_STATE_PAUSED;\r
204     self->is_live = (gst_element_set_state (self->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);\r
205     execute_seek (0, self);\r
206 }\r
207 \r
208 /* Called when the duration of the media changes. Just mark it as unknown, so we re-query it in the next UI refresh. */\r
209 static void duration_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {\r
210     self->duration = GST_CLOCK_TIME_NONE;\r
211 }\r
212 \r
213 /* Called when buffering messages are received. We inform the UI about the current buffering level and\r
214  * keep the pipeline paused until 100% buffering is reached. At that point, set the desired state. */\r
215 static void buffering_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {\r
216     gint percent;\r
217 \r
218     if (self->is_live)\r
219         return;\r
220 \r
221     gst_message_parse_buffering (msg, &percent);\r
222     if (percent < 100 && self->target_state >= GST_STATE_PAUSED) {\r
223         gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);\r
224         gst_element_set_state (self->pipeline, GST_STATE_PAUSED);\r
225         [self setUIMessage:message_string];\r
226         g_free (message_string);\r
227     } else if (self->target_state >= GST_STATE_PLAYING) {\r
228         gst_element_set_state (self->pipeline, GST_STATE_PLAYING);\r
229     } else if (self->target_state >= GST_STATE_PAUSED) {\r
230         [self setUIMessage:"Buffering complete"];\r
231     }\r
232 }\r
233 \r
234 /* Called when the clock is lost */\r
235 static void clock_lost_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {\r
236     if (self->target_state >= GST_STATE_PLAYING) {\r
237         gst_element_set_state (self->pipeline, GST_STATE_PAUSED);\r
238         gst_element_set_state (self->pipeline, GST_STATE_PLAYING);\r
239     }\r
240 }\r
241 \r
242 /* Retrieve the video sink's Caps and tell the application about the media size */\r
243 static void check_media_size (GStreamerBackend *self) {\r
244     GstElement *video_sink;\r
245     GstPad *video_sink_pad;\r
246     GstCaps *caps;\r
247     GstVideoInfo info;\r
248 \r
249     /* Retrieve the Caps at the entrance of the video sink */\r
250     g_object_get (self->pipeline, "video-sink", &video_sink, NULL);\r
251 \r
252     /* Do nothing if there is no video sink (this might be an audio-only clip */\r
253     if (!video_sink) return;\r
254 \r
255     video_sink_pad = gst_element_get_static_pad (video_sink, "sink");\r
256     caps = gst_pad_get_current_caps (video_sink_pad);\r
257 \r
258     if (gst_video_info_from_caps (&info, caps)) {\r
259         info.width = info.width * info.par_n / info.par_d;\r
260         GST_DEBUG ("Media size is %dx%d, notifying application", info.width, info.height);\r
261 \r
262         if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:height:)])\r
263         {\r
264             [self->ui_delegate mediaSizeChanged:info.width height:info.height];\r
265         }\r
266     }\r
267 \r
268     gst_caps_unref(caps);\r
269     gst_object_unref (video_sink_pad);\r
270     gst_object_unref(video_sink);\r
271 }\r
272 \r
273 /* Notify UI about pipeline state changes */\r
274 static void state_changed_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self)\r
275 {\r
276     GstState old_state, new_state, pending_state;\r
277     gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);\r
278     /* Only pay attention to messages coming from the pipeline, not its children */\r
279     if (GST_MESSAGE_SRC (msg) == GST_OBJECT (self->pipeline)) {\r
280         self->state = new_state;\r
281         gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));\r
282         [self setUIMessage:message];\r
283         g_free (message);\r
284 \r
285         if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)\r
286         {\r
287             check_media_size(self);\r
288 \r
289             /* If there was a scheduled seek, perform it now that we have moved to the Paused state */\r
290             if (GST_CLOCK_TIME_IS_VALID (self->desired_position))\r
291                 execute_seek (self->desired_position, self);\r
292         }\r
293     }\r
294 }\r
295 \r
296 /* Check if all conditions are met to report GStreamer as initialized.\r
297  * These conditions will change depending on the application */\r
298 -(void) check_initialization_complete\r
299 {\r
300     if (!initialized && main_loop) {\r
301         GST_DEBUG ("Initialization complete, notifying application.");\r
302         if (ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerInitialized)])\r
303         {\r
304             [ui_delegate gstreamerInitialized];\r
305         }\r
306         initialized = TRUE;\r
307     }\r
308 }\r
309 \r
310 /* Main method for the bus monitoring code */\r
311 -(void) app_function\r
312 {\r
313     GstBus *bus;\r
314     GSource *timeout_source;\r
315     GSource *bus_source;\r
316     GError *error = NULL;\r
317 \r
318     GST_DEBUG ("Creating pipeline");\r
319 \r
320     /* Create our own GLib Main Context and make it the default one */\r
321     context = g_main_context_new ();\r
322     g_main_context_push_thread_default(context);\r
323     \r
324     /* Build pipeline */\r
325     pipeline = gst_parse_launch("playbin", &error);\r
326     if (error) {\r
327         gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);\r
328         g_clear_error (&error);\r
329         [self setUIMessage:message];\r
330         g_free (message);\r
331         return;\r
332     }\r
333 \r
334     /* Set the pipeline to READY, so it can already accept a window handle */\r
335     gst_element_set_state(pipeline, GST_STATE_READY);\r
336     \r
337     video_sink = gst_bin_get_by_interface(GST_BIN(pipeline), GST_TYPE_VIDEO_OVERLAY);\r
338     if (!video_sink) {\r
339         GST_ERROR ("Could not retrieve video sink");\r
340         return;\r
341     }\r
342     gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(video_sink), (guintptr) (id) ui_video_view);\r
343 \r
344     /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */\r
345     bus = gst_element_get_bus (pipeline);\r
346     bus_source = gst_bus_create_watch (bus);\r
347     g_source_set_callback (bus_source, (GSourceFunc) gst_bus_async_signal_func, NULL, NULL);\r
348     g_source_attach (bus_source, context);\r
349     g_source_unref (bus_source);\r
350     g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, (__bridge void *)self);\r
351     g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, (__bridge void *)self);\r
352     g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, (__bridge void *)self);\r
353     g_signal_connect (G_OBJECT (bus), "message::duration", (GCallback)duration_cb, (__bridge void *)self);\r
354     g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, (__bridge void *)self);\r
355     g_signal_connect (G_OBJECT (bus), "message::clock-lost", (GCallback)clock_lost_cb, (__bridge void *)self);\r
356     gst_object_unref (bus);\r
357 \r
358     /* Register a function that GLib will call 4 times per second */\r
359     timeout_source = g_timeout_source_new (250);\r
360     g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, (__bridge void *)self, NULL);\r
361     g_source_attach (timeout_source, context);\r
362     g_source_unref (timeout_source);\r
363 \r
364     /* Create a GLib Main Loop and set it to run */\r
365     GST_DEBUG ("Entering main loop...");\r
366     main_loop = g_main_loop_new (context, FALSE);\r
367     [self check_initialization_complete];\r
368     g_main_loop_run (main_loop);\r
369     GST_DEBUG ("Exited main loop");\r
370     g_main_loop_unref (main_loop);\r
371     main_loop = NULL;\r
372     \r
373     /* Free resources */\r
374     g_main_context_pop_thread_default(context);\r
375     g_main_context_unref (context);\r
376     gst_element_set_state (pipeline, GST_STATE_NULL);\r
377     gst_object_unref (pipeline);\r
378     pipeline = NULL;\r
379     \r
380     ui_delegate = NULL;\r
381     ui_video_view = NULL;\r
382 \r
383     return;\r
384 }\r
385 \r
386 @end\r
387 \r