1 /*
2  * gst_mediaplayer is placed in the
3  * public domain.
4  */
5 
6 module gst_mediaplayer;
7 
8 //Tango imports
9 import tango.util.log.Trace;//Thread safe console output.
10 import Util = tango.text.Util;
11 import Integer = tango.text.convert.Integer;
12 import Stringz = tango.stdc.stringz;
13 
14 import tango.sys.Environment;
15 import tango.io.FilePath;
16 import tango.io.Path;
17 
18 //gtkD imports:
19 import gtk.Main;
20 import gtk.MainWindow;
21 
22 import gtk.Widget;
23 import gdk.Drawable;
24 import gdk.Window;
25 import gdk.Event;
26 import gtk.DrawingArea;
27 import gtk.AspectFrame;
28 
29 import gtk.FileChooserDialog;
30 
31 import gdk.X11;//Needed for XOverlay
32 
33 import gtk.VBox;
34 import gtk.HBox;
35 import gtk.Button;
36 import gtk.ComboBox;
37 
38 import gobject.Value;
39 
40 //gstreamerD imports:
41 
42 import gstreamer.gstreamer;
43 
44 import gobject.ObjectG;
45 import glib.ErrorG;
46 import gstreamer.Element;
47 import gstreamer.Bin;
48 import gstreamer.Pipeline;
49 import gstreamer.ElementFactory;
50 import gstreamer.Pad;
51 import gstreamer.Message;
52 import gstreamer.Structure;
53 import gstreamer.Bus;
54 
55 import gstreamerc.gstreamertypes;
56 import gstreamerc.gstreamer;
57 
58 import gstreamerc.gstinterfacestypes;//For GstXOverlay*
59 
60 import gstinterfaces.XOverlay;
61 
62 import gtkc.glib;
63 import gtkc.gobject;
64 
65 
66 class MonitorOverlay : public DrawingArea
67 {
68 public:
69 	
70 	this()
71 	{
72 		debug(MonitorOverlay) Trace.formatln("Monitor.this() START.");
73 		debug(MonitorOverlay) scope(exit) Trace.formatln("Monitor.this() END.");
74 		
75 		setAppPaintable( true );
76 		
77 		//For some reason this does more harm than good?
78 		//addOnExpose( &onExpose );
79 	}
80 	
81 	/*
82 	//For some reason this does more harm than good?
83 	int onExpose(GdkEventExpose* event, Widget widget)
84 	{
85 		if( xoverlay !is null )
86 		{
87 			//Trace.formatln("And we even have an xoverlay");
88 			xoverlay.expose();//For some reason this does more harm than good?
89 		}
90 		return false;//?
91 	}
92 	*/
93 	
94 	public XOverlay xoverlay() { return m_xoverlay; }
95 	/**
96 	* Give this method an XOverlay that you've created from
97 	* a videoSink. like: monitorOverlay.xoverlay = new XOverlay( videoSink );
98 	*/
99 	public XOverlay xoverlay(XOverlay set)
100 	{
101 		debug(MonitorOverlay) Trace.formatln("Monitor.xoverlay(set) START.");
102 		debug(MonitorOverlay) scope(exit) Trace.formatln("Monitor.xoverlay(set) END.");
103 		m_xoverlay = set;
104 		
105 		debug(MonitorOverlay) Trace.formatln("Monitor.xoverlay(set) xoverlay set. Now setting XwindowId.");
106 		m_xoverlay.setXwindowId( X11.drawableGetXid( getWindow() ) );
107 		
108 		debug(MonitorOverlay) Trace.formatln("X11.drawableGetXid: {}", X11.drawableGetXid( getWindow() ) );
109 		
110 		return m_xoverlay;
111 	}
112 	protected XOverlay m_xoverlay;
113 	
114 }
115 
116 
117 class GstMediaPlayer : public MainWindow
118 {
119 public:
120 
121 	GstBusSyncReply createXOverlayWindowCb( Message msg )
122 	{
123 		// ignore anything but 'prepare-xwindow-id' element messages
124 		if( msg.type() != GstMessageType.ELEMENT )
125 		//if (GST_MESSAGE_TYPE (message) != GST_MESSAGE_ELEMENT)
126 			return GstBusSyncReply.PASS;
127 		
128 		/*GstMessageType typ = msg.type();
129 		Stdout("The message type is: ")( cast(int)typ ).newline;
130 		
131 		ObjectGst obu = msg.src();
132 		Stdout("The message source is named: ")( obu.getName() ).newline;
133 		*/
134 		Structure str = msg.structure();
135 		if( str.hasName("prepare-xwindow-id") == false )
136 		//if (!gst_structure_has_name (message->structure, "prepare-xwindow-id"))
137 			return GstBusSyncReply.PASS;
138 		
139 		debug(MonitorOverlay) Trace.formatln("Now we should create the X window.");
140 		
141 		monitorOverlay.xoverlay = new XOverlay( videosink );
142 		
143 		debug(MonitorOverlay) Trace.formatln("Created an xoverlay.");
144 		
145 		return GstBusSyncReply.DROP;
146 	}
147 
148 	bool busCall( Message msg )
149 	{
150 		debug(gstreamer) Trace.formatln("GstMediaPlayer.busCall(msg) START.");
151 		debug(gstreamer) scope(exit) Trace.formatln("GstMediaPlayer.busCall(msg) END.");
152 
153 		switch( msg.type )
154 		{
155 			case GstMessageType.UNKNOWN:
156 				Trace.formatln("Unknown message type.");
157 			break;
158 			case GstMessageType.EOS:
159 				Trace.formatln("End-of-stream. Looping from the start.");
160 				//Main.quit();
161 				onSeekToStart(null);
162 			break;
163 
164 			case GstMessageType.ERROR:
165 			{
166 				string dbug;
167 				ErrorG err;
168 				msg.parseError(err, dbug);
169 				//g_free (dbug);
170 				Trace.formatln("Error: {} dbug: {}", Stringz.fromStringz(err.getErrorGStruct().message), dbug );
171 				//g_error_free (err);
172 				Main.quit();
173 			break;
174 			}
175 			default:
176 			break;
177 		}
178 
179 		return true;
180 	}
181 
182 	char[] g_appDir;
183 
184 	this(char[][] args)
185 	{
186 
187 		super("GstMediaPlayer");
188 		
189 		setSizeRequest(600, 400);
190 		
191 		vbox = new VBox(false,0);
192 		
193 		monitorOverlay = new MonitorOverlay();
194 		monitorAspectFrame = new AspectFrame("", 0.5, 0.5, 16.0/9.0, false );
195 		
196 		monitorAspectFrame.add( monitorOverlay );
197 		monitorAspectFrame.setShadowType( GtkShadowType.NONE );
198 		monitorAspectFrame.setLabelWidget( null );//Yes! This get's rid of that stupid label on top of the aspectframe! More room for the monitor.
199 		monitorAspectFrame.setBorderWidth(0);//No effect. Trying to get rid of that one pixel border...
200 		monitorAspectFrame.setSizeRequest(360, -1);//No effect. Why?
201 		
202 		vbox.packStart( monitorAspectFrame, true, true, 0 );
203 		
204 		buttonsHBox = new HBox(false,0);
205 		
206 		openButton = new Button("Open...");
207 		openButton.addOnClicked( &onOpen );
208 		buttonsHBox.packStart( openButton, false, false, 0 );
209 		
210 		playButton = new Button("Play");
211 		playButton.addOnClicked( &onPlay );
212 		buttonsHBox.packStart( playButton, false, false, 0 );
213 		
214 		aspectComboBox = new ComboBox(true);//Create a new text ComboBox.
215 		aspectComboBox.appendText("16:9");
216 		aspectComboBox.appendText("4:3");
217 		aspectComboBox.setActiveText("16:9");
218 		aspectComboBox.addOnChanged( &onAspectComboBoxChanged );
219 		
220 		buttonsHBox.packStart( aspectComboBox, false, false, 0 );
221 		
222 		quitButton = new Button(StockID.QUIT);
223 		quitButton.addOnClicked( &onQuit );
224 		buttonsHBox.packStart( quitButton, false, false, 0 );
225 		
226 		vbox.packStart( buttonsHBox, false, false, 0 );
227 		
228 		add( vbox );
229 		
230 		showAll();
231 
232 		scope mypath = new FilePath( args[0] );
233 		
234 		bool remove_trailing_dotslash = false;
235 		
236 		g_appDir = mypath.path();
237 		
238 		Trace.formatln("g_appDir before: {}", g_appDir );
239 		
240 		if( g_appDir == "./" )
241 		{
242 			remove_trailing_dotslash = true;
243 		}
244 		
245 		bool starts_with_two_dots = false;
246 		if( args[0][0] == '.' && args[0][1] == '.' )
247 			starts_with_two_dots = true;
248 		
249 		mypath = mypath.absolute(Environment.cwd());//This will add /home/user...
250 		if( starts_with_two_dots )
251 			g_appDir = normalize( mypath.path() );//This will get rid of the trailing /../
252 		else g_appDir = mypath.path();
253 		
254 		if( remove_trailing_dotslash == true )
255 		{
256 			//This will get rid of the trailing ./
257 			g_appDir = g_appDir[0..length-2];
258 		}
259 		
260 		Trace.formatln("g_appDir after: {}", g_appDir );
261 		
262 		if (args.length > 1)
263 		{
264 			mediaFileUri = args[1];
265 		
266 			//This will construct the filename to be a URI, but it will only
267 			//work for files in the same directory.
268 			if( mediaFileUri[0..7] != "file://" && mediaFileUri[0..7] != "http://" )
269 				mediaFileUri = "file://" ~ g_appDir ~ mediaFileUri;
270 		}
271 		
272 		/*
273 		// check input arguments
274 		if (args.length > 1)
275 		{
276 			for( uint i = 0; i < args.length; i++ )
277 			{
278 				char[] ar = args[i];
279 				
280 				if( ar == "--help" )
281 				{
282 					Trace.formatln("Usage: {} <mediafilename>", args[0]);
283 					return -1;
284 				}
285 			}
286 		}
287 		*/
288 
289 		if( mediaFileUri != "" )
290 			playMediaFile(mediaFileUri);
291 		
292 	}
293 	
294 	void playMediaFile(char[] file)
295 	{
296 		if( source !is null )
297 		{
298 			fullStop();
299 			delete source;
300 		}
301 		if( videosink !is null )
302 			delete videosink;
303 	
304 		// create elements
305 
306 		source = ElementFactory.make("playbin", "ourplaybin");
307 		//source = ElementFactory.make("playbin2", "ourplaybin");//playbin2 doesn't work,
308 		//correctly with XOverlay.
309 		videosink = ElementFactory.make("xvimagesink", "video-output-xvimagesink");
310 		//Only xvimagesink work (almost) correctly with XOverlay, but even it still
311 		//has some problems. It won't work with compositing enabled...
312 		//videosink = ElementFactory.make("autovideosink", "video-output-sink");
313 		//videosink = ElementFactory.make("fakesink", "video-sink");
314 
315 		if( source is null )
316 		{
317 			Trace.formatln("PlayBin could not be created");
318 			throw new Exception("One or more gstreamerD elements could not be created.");
319 		}
320 		
321 		if( videosink is null )
322 		{
323 			Trace.formatln("videosink could not be created");
324 			throw new Exception("One or more gstreamerD elements could not be created.");
325 		}
326 
327 		//add message handlers
328 		source.getBus().setSyncHandler( &createXOverlayWindowCb );
329 		source.getBus().addWatch( &busCall );
330 
331 		//Some Value handling, to get our videosink C GstElement*
332 		//to be accepted by setProperty. This could propably made cleaner.
333 		//One idea is to add a Element.setProperty(char[], void*); method.
334 		
335 		Value val = new Value();
336 		//val.init(GType.POINTER);
337 		//val.setPointer( cast(void*)(videosink.getElementStruct()) );
338 		val.init(GType.OBJECT);
339 		//val.setObject( cast(GstElement*)videosink.getElementStruct() );
340 		val.setObject( videosink.getElementStruct() );
341 		source.setProperty( "video-sink", val );
342 	
343 		source.setProperty("uri", file);
344 		play();
345 	}
346 	
347 
348 	~this()
349 	{
350 		fullStop();
351 	}
352 
353 	void onPlay(Button button)
354 	{
355 		play();
356 	}
357 	
358 	void play()
359 	{
360 		if( isPlaying == false )
361 		{
362 			isPlaying = true;
363 			// Now set to playing and iterate.
364 			debug(1) Trace.formatln("Setting to PLAYING.");
365 			//pipeline.setState( GstState.PLAYING );
366 			source.setState( GstState.PLAYING );
367 			debug(1) Trace.formatln("Running.");
368 		}
369 		else
370 		{
371 			isPlaying = false;
372 			source.setState( GstState.PAUSED );
373 		}
374 	}
375 	
376 	void fullStop()
377 	{
378 		if( source !is null )
379 			source.setState( GstState.NULL );
380 		isPlaying = false;
381 	}
382 	
383 	void onSeekToStart(Button button)
384 	{
385 		//source.seek( 5 * GST_SECOND );//seek to 5 seconds.
386 		source.seek( 0 );//seek to start.
387 	}
388 
389 	void onOpen(Button button)
390 	{
391 		runImportMaterialFileChooser();
392 	}
393 	
394 	void onQuit(Button button)
395 	{
396 		fullStop();
397 		Main.quit();
398 	}
399 	
400 	void onAspectComboBoxChanged( ComboBox combo )
401 	{
402 		char[] asp = combo.getActiveText();
403 		if( asp == "16:9" )
404 			monitorAspectFrame.set( 0.5, 0.5, 16.0/9.0, false );
405 		else //if( asp == "4:3" )
406 			monitorAspectFrame.set( 0.5, 0.5, 4.0/3.0, false );
407 			
408 	}
409 	
410 	void runImportMaterialFileChooser()
411 	{
412 		char[][] a;
413 		ResponseType[] r;
414 		a ~= "Play file";
415 		a ~= "Close";
416 		r ~= ResponseType.APPLY;//OK;
417 		r ~= ResponseType.CANCEL;
418 		if ( importMaterialFileChooserDialog  is  null )
419 		{
420 			importMaterialFileChooserDialog = new FileChooserDialog("Play mediafile", this, FileChooserAction.OPEN, a, r);
421 		}
422 		
423 		if( importMaterialFileChooserDialog.run() != ResponseType.CANCEL )
424 		{
425 			//mediaFileUri = importMaterialFileChooserDialog.getFilename();
426 			mediaFileUri = importMaterialFileChooserDialog.getUri();
427 			
428 			Trace.formatln( "file selected: {}", mediaFileUri );
429 			playMediaFile(mediaFileUri);
430 		}
431 		
432 		importMaterialFileChooserDialog.hide();
433 	}
434 	
435 protected:
436 
437 	char[] mediaFileUri = "";
438 
439 	Element source, videosink;
440 	
441 	VBox vbox;
442 	HBox buttonsHBox;
443 	Button openButton;
444 	Button playButton;
445 	bool isPlaying(bool set)
446 	{
447 		m_isPlaying = set;
448 		/*
449 		//For some reason enabling this change of playButton
450 		//label, from Play to Pause, will cause the XOverlay
451 		//not to show the picture while we're on Pause.
452 		//That's why it's disabled. XOverlay just doesn't work
453 		//properly...
454 		if( playButton !is null )
455 		{
456 			if( m_isPlaying == true )
457 			{
458 				playButton.setLabel("Pause");
459 			}
460 			else
461 			{
462 				playButton.setLabel("Play");
463 			}
464 		}
465 		*/
466 		return m_isPlaying;
467 	}
468 	bool isPlaying() { return m_isPlaying; }
469 	bool m_isPlaying = false;
470 	
471 	ComboBox aspectComboBox;
472 	Button quitButton;
473 	
474 	MonitorOverlay monitorOverlay;
475 	AspectFrame monitorAspectFrame;
476 	
477 	FileChooserDialog importMaterialFileChooserDialog;
478 }
479 
480 
481 int main(char[][] args)
482 {
483 	Trace.formatln("gstreamerD GstMediaPlayer");
484 
485 	uint major, minor, micro, nano;
486 
487 	Trace.formatln("Trying to init...");
488 
489 	Main.init(args);
490 	GStreamer.init(args);
491 
492 	Trace.formatln("Checking version of GStreamer...");
493 	GStreamer.versio(major, minor, micro, nano);
494 	Trace.formatln("The installed version of GStreamer is {}.{}.{}", major, minor, micro );
495 
496 	GstMediaPlayer gstMediaPlayer = new GstMediaPlayer(args);
497 
498 	//We must use the gtkD mainloop to run gstreamerD apps.
499 	Main.run();
500 
501 	return 0;
502 }
503 
504