mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-11-24 18:51:11 +00:00
Enable seeking by dragging the time slider. Add a bunch of online clips to the hardcoded library.
This commit is contained in:
parent
079ad1bf15
commit
76f7e541ec
6 changed files with 152 additions and 17 deletions
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue