mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2025-02-17 19:55:32 +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 */
|
/* Set the URI to be played */
|
||||||
-(void) setUri:(NSString*)uri;
|
-(void) setUri:(NSString*)uri;
|
||||||
|
|
||||||
|
/* Set the position to seek to, in milliseconds */
|
||||||
|
-(void) setPosition:(NSInteger)milliseconds;
|
||||||
|
|
||||||
@end
|
@end
|
|
@ -7,6 +7,10 @@
|
||||||
GST_DEBUG_CATEGORY_STATIC (debug_category);
|
GST_DEBUG_CATEGORY_STATIC (debug_category);
|
||||||
#define GST_CAT_DEFAULT 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()
|
@interface GStreamerBackend()
|
||||||
-(void)setUIMessage:(gchar*) message;
|
-(void)setUIMessage:(gchar*) message;
|
||||||
-(void)app_function;
|
-(void)app_function;
|
||||||
|
@ -14,15 +18,17 @@ GST_DEBUG_CATEGORY_STATIC (debug_category);
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GStreamerBackend {
|
@implementation GStreamerBackend {
|
||||||
id ui_delegate; /* Class that we use to interact with the user interface */
|
id ui_delegate; /* Class that we use to interact with the user interface */
|
||||||
GstElement *pipeline; /* The running pipeline */
|
GstElement *pipeline; /* The running pipeline */
|
||||||
GstElement *video_sink;/* The video sink element which receives XOverlay commands */
|
GstElement *video_sink; /* The video sink element which receives XOverlay commands */
|
||||||
GMainContext *context; /* GLib context used to run the main loop */
|
GMainContext *context; /* GLib context used to run the main loop */
|
||||||
GMainLoop *main_loop; /* GLib main loop */
|
GMainLoop *main_loop; /* GLib main loop */
|
||||||
gboolean initialized; /* To avoid informing the UI multiple times about the initialization */
|
gboolean initialized; /* To avoid informing the UI multiple times about the initialization */
|
||||||
UIView *ui_video_view; /* UIView that holds the video */
|
UIView *ui_video_view; /* UIView that holds the video */
|
||||||
GstState state; /* Current pipeline state */
|
GstState state; /* Current pipeline state */
|
||||||
gint64 duration; /* Cached clip duration */
|
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);
|
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
|
* Private methods
|
||||||
*/
|
*/
|
||||||
|
@ -108,9 +125,7 @@ static gboolean refresh_ui (GStreamerBackend *self) {
|
||||||
|
|
||||||
/* If we didn't know it yet, query the stream duration */
|
/* If we didn't know it yet, query the stream duration */
|
||||||
if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {
|
if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {
|
||||||
if (!gst_element_query_duration (self->pipeline, &fmt, &self->duration)) {
|
gst_element_query_duration (self->pipeline, &fmt, &self->duration);
|
||||||
GST_WARNING ("Could not query current duration");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gst_element_query_position (self->pipeline, &fmt, &position)) {
|
if (gst_element_query_position (self->pipeline, &fmt, &position)) {
|
||||||
|
@ -120,6 +135,51 @@ static gboolean refresh_ui (GStreamerBackend *self) {
|
||||||
return TRUE;
|
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) {
|
static void check_media_size (GStreamerBackend *self) {
|
||||||
GstElement *video_sink;
|
GstElement *video_sink;
|
||||||
GstPad *video_sink_pad;
|
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 */
|
/* Retrieve the Caps at the entrance of the video sink */
|
||||||
g_object_get (self->pipeline, "video-sink", &video_sink, NULL);
|
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");
|
video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
|
||||||
caps = gst_pad_get_negotiated_caps (video_sink_pad);
|
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)
|
if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
|
||||||
{
|
{
|
||||||
check_media_size(self);
|
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]];
|
[entries addObject:[NSString stringWithFormat:@"%@/%@",docsPath, e]];
|
||||||
}
|
}
|
||||||
self->mediaEntries = entries;
|
self->mediaEntries = entries;
|
||||||
self->onlineEntries = [NSArray arrayWithObjects:
|
|
||||||
@"http://docs.gstreamer.com/media/sintel_trailer-368p.ogv",
|
entries = [[NSMutableArray alloc] init];
|
||||||
@"http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_640x360.m4v",
|
|
||||||
nil];
|
// 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
|
@end
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
|
|
||||||
-(IBAction) play:(id)sender;
|
-(IBAction) play:(id)sender;
|
||||||
-(IBAction) pause:(id)sender;
|
-(IBAction) pause:(id)sender;
|
||||||
|
-(IBAction) sliderValueChanged:(id)sender;
|
||||||
|
-(IBAction) sliderTouchDown:(id)sender;
|
||||||
|
-(IBAction) sliderTouchUp:(id)sender;
|
||||||
|
|
||||||
/* From GStreamerBackendDelegate */
|
/* From GStreamerBackendDelegate */
|
||||||
-(void) gstreamerInitialized;
|
-(void) gstreamerInitialized;
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
GStreamerBackend *gst_backend;
|
GStreamerBackend *gst_backend;
|
||||||
int media_width;
|
int media_width;
|
||||||
int media_height;
|
int media_height;
|
||||||
|
Boolean dragging_slider;
|
||||||
|
Boolean is_local_media;
|
||||||
|
Boolean is_playing_desired;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -56,7 +59,7 @@
|
||||||
play_button.enabled = FALSE;
|
play_button.enabled = FALSE;
|
||||||
pause_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_width = 320;
|
||||||
media_height = 240;
|
media_height = 240;
|
||||||
|
|
||||||
|
@ -81,12 +84,37 @@
|
||||||
-(IBAction) play:(id)sender
|
-(IBAction) play:(id)sender
|
||||||
{
|
{
|
||||||
[gst_backend play];
|
[gst_backend play];
|
||||||
|
is_playing_desired = YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Called when the Pause button is pressed */
|
/* Called when the Pause button is pressed */
|
||||||
-(IBAction) pause:(id)sender
|
-(IBAction) pause:(id)sender
|
||||||
{
|
{
|
||||||
[gst_backend pause];
|
[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
|
/* Called when the size of the main view has changed, so we can
|
||||||
|
@ -121,6 +149,8 @@
|
||||||
pause_button.enabled = TRUE;
|
pause_button.enabled = TRUE;
|
||||||
message_label.text = @"Ready";
|
message_label.text = @"Ready";
|
||||||
[gst_backend setUri:uri];
|
[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
|
-(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(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
time_slider.maximumValue = duration;
|
time_slider.maximumValue = duration;
|
||||||
time_slider.value = position;
|
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">
|
<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"/>
|
<rect key="frame" x="240" y="11" width="118" height="23"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<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>
|
</slider>
|
||||||
</barButtonItem>
|
</barButtonItem>
|
||||||
</items>
|
</items>
|
||||||
|
|
Loading…
Reference in a new issue