Enable seeking by dragging the time slider. Add a bunch of online clips to the hardcoded library.

This commit is contained in:
Xavi Artigas 2013-05-17 11:46:48 +02:00
parent 079ad1bf15
commit 76f7e541ec
6 changed files with 152 additions and 17 deletions

View file

@ -22,4 +22,7 @@
/* Set the URI to be played */
-(void) setUri:(NSString*)uri;
/* Set the position to seek to, in milliseconds */
-(void) setPosition:(NSInteger)milliseconds;
@end

View file

@ -7,6 +7,10 @@
GST_DEBUG_CATEGORY_STATIC (debug_category);
#define GST_CAT_DEFAULT debug_category
/* Do not allow seeks to be performed closer than this distance. It is visually useless, and will probably
* confuse some demuxers. */
#define SEEK_MIN_DELAY (500 * GST_MSECOND)
@interface GStreamerBackend()
-(void)setUIMessage:(gchar*) message;
-(void)app_function;
@ -14,15 +18,17 @@ GST_DEBUG_CATEGORY_STATIC (debug_category);
@end
@implementation GStreamerBackend {
id ui_delegate; /* Class that we use to interact with the user interface */
GstElement *pipeline; /* The running pipeline */
GstElement *video_sink;/* The video sink element which receives XOverlay commands */
GMainContext *context; /* GLib context used to run the main loop */
GMainLoop *main_loop; /* GLib main loop */
gboolean initialized; /* To avoid informing the UI multiple times about the initialization */
UIView *ui_video_view; /* UIView that holds the video */
GstState state; /* Current pipeline state */
gint64 duration; /* Cached clip duration */
id ui_delegate; /* Class that we use to interact with the user interface */
GstElement *pipeline; /* The running pipeline */
GstElement *video_sink; /* The video sink element which receives XOverlay commands */
GMainContext *context; /* GLib context used to run the main loop */
GMainLoop *main_loop; /* GLib main loop */
gboolean initialized; /* To avoid informing the UI multiple times about the initialization */
UIView *ui_video_view; /* UIView that holds the video */
GstState state; /* Current pipeline state */
gint64 duration; /* Cached clip duration */
gint64 desired_position; /* Position to seek to, once the pipeline is running */
GstClockTime last_seek_time; /* For seeking overflow prevention (throttling) */
}
/*
@ -73,6 +79,17 @@ GST_DEBUG_CATEGORY_STATIC (debug_category);
GST_DEBUG ("URI set to %s", char_uri);
}
-(void) setPosition:(NSInteger)milliseconds
{
gint64 position = (gint64)(milliseconds * GST_MSECOND);
if (state >= GST_STATE_PAUSED) {
execute_seek(position, self);
} else {
GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));
self->desired_position = position;
}
}
/*
* Private methods
*/
@ -108,9 +125,7 @@ static gboolean refresh_ui (GStreamerBackend *self) {
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {
if (!gst_element_query_duration (self->pipeline, &fmt, &self->duration)) {
GST_WARNING ("Could not query current duration");
}
gst_element_query_duration (self->pipeline, &fmt, &self->duration);
}
if (gst_element_query_position (self->pipeline, &fmt, &position)) {
@ -120,6 +135,51 @@ static gboolean refresh_ui (GStreamerBackend *self) {
return TRUE;
}
/* Forward declaration for the delayed seek callback */
static gboolean delayed_seek_cb (GStreamerBackend *self);
/* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for
* some time in the future. */
static void execute_seek (gint64 position, GStreamerBackend *self) {
gint64 diff;
if (position == GST_CLOCK_TIME_NONE)
return;
diff = gst_util_get_timestamp () - self->last_seek_time;
if (GST_CLOCK_TIME_IS_VALID (self->last_seek_time) && diff < SEEK_MIN_DELAY) {
/* The previous seek was too close, delay this one */
GSource *timeout_source;
if (self->desired_position == GST_CLOCK_TIME_NONE) {
/* There was no previous seek scheduled. Setup a timer for some time in the future */
timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);
g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, (__bridge void *)self, NULL);
g_source_attach (timeout_source, self->context);
g_source_unref (timeout_source);
}
/* Update the desired seek position. If multiple petitions are received before it is time
* to perform a seek, only the last one is remembered. */
self->desired_position = position;
GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,
GST_TIME_ARGS (position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));
} else {
/* Perform the seek now */
GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (position));
self->last_seek_time = gst_util_get_timestamp ();
gst_element_seek_simple (self->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, position);
self->desired_position = GST_CLOCK_TIME_NONE;
}
}
/* Delayed seek callback. This gets called by the timer setup in the above function. */
static gboolean delayed_seek_cb (GStreamerBackend *self) {
GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (self->desired_position));
execute_seek (self->desired_position, self);
return FALSE;
}
static void check_media_size (GStreamerBackend *self) {
GstElement *video_sink;
GstPad *video_sink_pad;
@ -130,6 +190,10 @@ static void check_media_size (GStreamerBackend *self) {
/* Retrieve the Caps at the entrance of the video sink */
g_object_get (self->pipeline, "video-sink", &video_sink, NULL);
/* Do nothing if there is no video sink (this might be an audio-only clip */
if (!video_sink) return;
video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
caps = gst_pad_get_negotiated_caps (video_sink_pad);
@ -182,6 +246,10 @@ static void state_changed_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *se
if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
{
check_media_size(self);
/* If there was a scheduled seek, perform it now that we have moved to the Paused state */
if (GST_CLOCK_TIME_IS_VALID (self->desired_position))
execute_seek (self->desired_position, self);
}
}
}

View file

@ -97,10 +97,31 @@ static NSString *CellIdentifier = @"CellIdentifier";
[entries addObject:[NSString stringWithFormat:@"%@/%@",docsPath, e]];
}
self->mediaEntries = entries;
self->onlineEntries = [NSArray arrayWithObjects:
@"http://docs.gstreamer.com/media/sintel_trailer-368p.ogv",
@"http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_640x360.m4v",
nil];
entries = [[NSMutableArray alloc] init];
// Big Buck Bunny
[entries addObject:@"http://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_surround-fix.avi"];
[entries addObject:@"http://mirrorblender.top-ix.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_h264.mov"];
[entries addObject:@"http://mirrorblender.top-ix.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_stereo.ogg"];
[entries addObject:@"http://mirrorblender.top-ix.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_stereo.avi"];
// Sintel
[entries addObject:@"http://ftp.nluug.nl/ftp/graphics/blender/apricot/trailer/Sintel_Trailer1.480p.DivX_Plus_HD.mkv"];
[entries addObject:@"http://ftp.nluug.nl/ftp/graphics/blender/apricot/trailer/sintel_trailer-480p.mp4"];
[entries addObject:@"http://ftp.nluug.nl/ftp/graphics/blender/apricot/trailer/sintel_trailer-480p.ogv"];
[entries addObject:@"http://mirrorblender.top-ix.org/movies/sintel-1024-surround.mp4"];
// Tears of Steel
[entries addObject:@"http://blender-mirror.kino3d.org/mango/download.blender.org/demo/movies/ToS/tears_of_steel_720p.mkv"];
[entries addObject:@"http://blender-mirror.kino3d.org/mango/download.blender.org/demo/movies/ToS/tears_of_steel_720p.mov"];
[entries addObject:@"http://media.xiph.org/mango/tears_of_steel_1080p.webm"];
// Radio stations
[entries addObject:@"http://radio.hbr1.com:19800/trance.ogg"];
[entries addObject:@"http://radio.hbr1.com:19800/tronic.aac"];
self->onlineEntries = entries;
}
@end

View file

@ -18,6 +18,9 @@
-(IBAction) play:(id)sender;
-(IBAction) pause:(id)sender;
-(IBAction) sliderValueChanged:(id)sender;
-(IBAction) sliderTouchDown:(id)sender;
-(IBAction) sliderTouchUp:(id)sender;
/* From GStreamerBackendDelegate */
-(void) gstreamerInitialized;

View file

@ -6,6 +6,9 @@
GStreamerBackend *gst_backend;
int media_width;
int media_height;
Boolean dragging_slider;
Boolean is_local_media;
Boolean is_playing_desired;
}
@end
@ -56,7 +59,7 @@
play_button.enabled = FALSE;
pause_button.enabled = FALSE;
/* Make these constant for now, later tutorials will change them */
/* As soon as the GStreamer backend knows the real values, these ones will be replaced */
media_width = 320;
media_height = 240;
@ -81,12 +84,37 @@
-(IBAction) play:(id)sender
{
[gst_backend play];
is_playing_desired = YES;
}
/* Called when the Pause button is pressed */
-(IBAction) pause:(id)sender
{
[gst_backend pause];
is_playing_desired = NO;
}
- (IBAction)sliderValueChanged:(id)sender {
if (!dragging_slider) return;
// If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.
if (is_local_media)
[gst_backend setPosition:time_slider.value];
[self updateTimeWidget];
}
- (IBAction)sliderTouchDown:(id)sender {
[gst_backend pause];
dragging_slider = YES;
}
- (IBAction)sliderTouchUp:(id)sender {
dragging_slider = NO;
// If this is a remote file, scrub seeking is probably not going to work smoothly enough.
// Therefore, perform only the seek when the slider is released.
if (!is_local_media)
[gst_backend setPosition:time_slider.value];
if (is_playing_desired)
[gst_backend play];
}
/* Called when the size of the main view has changed, so we can
@ -121,6 +149,8 @@
pause_button.enabled = TRUE;
message_label.text = @"Ready";
[gst_backend setUri:uri];
is_local_media = [uri hasPrefix:@"file://"];
is_playing_desired = NO;
});
}
@ -144,6 +174,9 @@
-(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration
{
/* Ignore messages from the pipeline if the time sliders is being dragged */
if (dragging_slider) return;
dispatch_async(dispatch_get_main_queue(), ^{
time_slider.maximumValue = duration;
time_slider.value = position;

View file

@ -80,6 +80,13 @@
<slider key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" id="ufs-E5-87w" userLabel="Slider">
<rect key="frame" x="240" y="11" width="118" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="sliderTouchDown:" destination="z7O-8l-Zeo" eventType="touchDown" id="yV6-eN-VUb"/>
<action selector="sliderTouchUp:" destination="z7O-8l-Zeo" eventType="touchCancel" id="OyS-WZ-sEk"/>
<action selector="sliderTouchUp:" destination="z7O-8l-Zeo" eventType="touchUpOutside" id="Vfz-se-pJg"/>
<action selector="sliderTouchUp:" destination="z7O-8l-Zeo" eventType="touchUpInside" id="kZF-uW-GRo"/>
<action selector="sliderValueChanged:" destination="z7O-8l-Zeo" eventType="valueChanged" id="cwm-pm-BfT"/>
</connections>
</slider>
</barButtonItem>
</items>