diff options
author | Michele Calgaro <[email protected]> | 2020-06-13 22:45:28 +0900 |
---|---|---|
committer | Michele Calgaro <[email protected]> | 2020-06-13 22:45:28 +0900 |
commit | 5f44f7b187093ef290315b7f8766b540a31de35f (patch) | |
tree | 27ffb7b218199ca04f240c390c52426c65f45dce /src | |
download | codeine-5f44f7b187093ef290315b7f8766b540a31de35f.tar.gz codeine-5f44f7b187093ef290315b7f8766b540a31de35f.zip |
Initial code import from debian snapshot
https://snapshot.debian.org/package/codeine/1.0.1-3.dfsg-3.1/
Signed-off-by: Michele Calgaro <[email protected]>
Diffstat (limited to 'src')
54 files changed, 6680 insertions, 0 deletions
@@ -0,0 +1,13 @@ +================================================================================ +FAQ +================================================================================ +This FAQ regards the source code and its layout. + + +Q: Why reimplement code in the part rather than consolidate it with the app? +A: Mainly to make the application start faster, loading a part is not free. But + also to make the part start faster as it has much lower requirements. + I admit the maintainance consequences aren't pretty, but the part has very + little code, so theoretically it should be easy to keep bug fixes in sync. + +=========1=========2=========3=========4=========5=========6=========7=========8
\ No newline at end of file diff --git a/src/SConscript b/src/SConscript new file mode 100644 index 0000000..31c1933 --- /dev/null +++ b/src/SConscript @@ -0,0 +1,21 @@ + +############################ +## load the config + +## Use the environment and the tools set in the top-level +## SConstruct file (set with 'Export') - this is very important + +Import( '*' ) +myenv=env.Copy() + +myenv.SConscript( dirs = Split( "app part") ) + +KDEinstall( env['KDEDATA']+'/codeine', '../misc/codeineui.rc', myenv ) +KDEinstall( env['KDEDATA']+'/konqueror/servicemenus', '../misc/codeine_play_dvd.desktop', myenv ) +KDEinstall( env['KDESERV'], '../misc/codeine_part.desktop', myenv ) +KDEinstall( env['KDEXDG'], '../misc/codeine.desktop', myenv ) + +for size in ['16', '22', '32', '48', '64', '128']: + KDEinstallas( env['KDEICONS']+'/crystalsvg/'+size+'x'+size+'/apps/codeine.png', '../misc/cr'+size+'-app-codeine.png', myenv ) + +#print env['KDECXXFLAGS']
\ No newline at end of file diff --git a/src/TODO b/src/TODO new file mode 100644 index 0000000..a8d427d --- /dev/null +++ b/src/TODO @@ -0,0 +1,38 @@ +1.0.1
+ Volume state saving
+
+1.0.x
+ Improve error messages and error handling
+ Consolidate error handling code in part and application so we don't diverge
+ No audio icon in analyzer when no audio
+ Playback finished message (amaroK statusbar code?)
+ Mouse move to show toolbar in fullscreen mode
+ People are likely to use "F" keyboard shortcut or escape after film ends in fullscreen mode
+ but the popup of the menu prevents this, as for some reason Qt stops handling keyboard
+ shortcuts. We need a solution. The escape issue makes the popup menu less useful. So
+ escape with the menu should exit fullscreen too. All shortcuts in the menu should work.
+ Shortcuts not in the menu shouldn't work as this is a modal menu! Could be dangerous.
+ Consider adding quit to the fullscreen context menu always, or make the menubar show with
+ toolbar when mouse moves.
+ Add tooltips to PlayMedia dialog for recent file listview entries (directory info etc.)
+ Show zoom percentage in statusbar/OSD when resizing window, snap to 100%?
+ More feedback at playback end
+ Better loading/buffering status/feedback
+ Save prettyTitle with url and show that in recent file list,
+ - save m3u's? <-- needs thought
+ - mark remote files?
+ Support .srt files like Kaffeine/xine-ui does, ask berkus if stuck.
+ DVD fullscreen, DVD menu shows or something
+
+1.1
+ Volume button (not there by default)
+ Two entries in Konqueror: 1. open in new window 2. open in current window
+ Play from begin scroll up popup when resuming playback
+ Much better DVD support
+ Show length information in normal window
+ 'o' in fullscreen mode shows OSD of length and elapsed time info, <-- emulate mplayer
+
+ACTION
+ xine config dialog is modal
+REACTION
+ none, at least yet, it is far easier to maintain modal dialogs
\ No newline at end of file diff --git a/src/app/SConscript b/src/app/SConscript new file mode 100644 index 0000000..3c35c32 --- /dev/null +++ b/src/app/SConscript @@ -0,0 +1,59 @@ + +############################ +## load the config + +## Use the environment and the tools set in the top-level +## SConstruct file (set with 'Export') - this is very important + +Import( '*' ) +myenv=env.Copy() + +############################# +## the programs to build + +# we put the stuff that could fail due to bad xine.h locations, etc. at the beginning +# so if the build fails the user knows quickly +app_sources = Split(""" + xineEngine.cpp + xineConfig.cpp + xineScope.c + theStream.cpp + videoWindow.cpp + videoSettings.cpp + captureFrame.cpp + + actions.cpp + stateChange.cpp + slider.cpp + analyzer.cpp + playDialog.cpp + listView.cpp + adjustSizeButton.cpp + fullScreenAction.cpp + insertAspectRatioMenuItems.cpp + playlistFile.cpp + volumeAction.cpp + + ../mxcl.library.cpp + + main.cpp + mainWindow.cpp""") + +KDEprogram( "codeine", app_sources, myenv ) + + +############################ +## Customization + +## Additional include paths for compiling the source files +## Always add '../' (top-level directory) because moc makes code that needs it +KDEaddpaths( ['./', '../', '../../'], myenv ) + +## Necessary libraries to link against +KDEaddlibs( ['qt-mt', 'kio', 'kdecore', 'kdeui', 'xine', 'Xtst'], myenv ) + +## This shows how to add other link flags to the program +myenv['LINKFLAGS'].append('-L/usr/X11R6/lib') + +## If you are using QThread, add this line +# myenv.AppendUnique( CPPFLAGS = ['-DQT_THREAD_SUPPORT'] ) diff --git a/src/app/actions.cpp b/src/app/actions.cpp new file mode 100644 index 0000000..a767a2c --- /dev/null +++ b/src/app/actions.cpp @@ -0,0 +1,27 @@ +// Copyright 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "actions.h" +#include "debug.h" +#include "mxcl.library.h" +#include <qtoolbutton.h> +#include "xineEngine.h" + +namespace Codeine +{ + PlayAction::PlayAction( QObject *receiver, const char *slot, KActionCollection *ac ) + : KToggleAction( i18n("Play"), "player_play", Qt::Key_Space, receiver, slot, ac, "play" ) + {} + + void + PlayAction::setChecked( bool b ) + { + if( videoWindow()->state() == Engine::Empty && sender() && QCString(sender()->className()) == "KToolBarButton" ) { + // clicking play when empty means open PlayMediaDialog, but we have to uncheck the toolbar button + // as KDElibs sets that checked automatically.. + ((QToolButton*)sender())->setOn( false ); + } + else + KToggleAction::setChecked( b ); + } +} diff --git a/src/app/actions.h b/src/app/actions.h new file mode 100644 index 0000000..4f2f589 --- /dev/null +++ b/src/app/actions.h @@ -0,0 +1,26 @@ +// (c) 2004 Max Howell ([email protected])
+// See COPYING file for licensing information
+
+#ifndef CODEINEACTIONS_H
+#define CODEINEACTIONS_H
+
+#include <kactionclasses.h> //baseclass
+#include <kactioncollection.h> //convenience
+
+namespace Codeine
+{
+ KActionCollection *actionCollection(); ///defined in mainWindow.cpp
+ KAction *action( const char* ); ///defined in mainWindow.cpp
+ inline KToggleAction *toggleAction( const char *name ) { return (KToggleAction*)action( name ); }
+
+ class PlayAction : public KToggleAction
+ {
+ public:
+ PlayAction( QObject *receiver, const char *slot, KActionCollection* );
+
+ protected:
+ virtual void setChecked( bool );
+ };
+}
+
+#endif
diff --git a/src/app/adjustSizeButton.cpp b/src/app/adjustSizeButton.cpp new file mode 100644 index 0000000..041b01c --- /dev/null +++ b/src/app/adjustSizeButton.cpp @@ -0,0 +1,125 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "adjustSizeButton.h" +#include "extern.h" +#include <kpushbutton.h> +#include <qapplication.h> +#include <qevent.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qpainter.h> +#include "theStream.h" +#include "xineEngine.h" //videoWindow() + + +QString i18n( const char *text ); + +namespace Codeine +{ + AdjustSizeButton::AdjustSizeButton( QWidget *parent ) + : QFrame( parent ) + , m_counter( 0 ) + , m_stage( 1 ) + , m_offset( 0 ) + { + parent->installEventFilter( this ); + + setPalette( QApplication::palette() ); //videoWindow has different palette + setFrameStyle( QFrame::Plain | QFrame::Box ); + + m_preferred = new KPushButton( KGuiItem( i18n("Preferred Scale"), "viewmag" ), this ); + connect( m_preferred, SIGNAL(clicked()), qApp->mainWidget(), SLOT(adjustSize()) ); + connect( m_preferred, SIGNAL(clicked()), SLOT(deleteLater()) ); + + m_oneToOne = new KPushButton( KGuiItem( i18n("Scale 100%"), "viewmag1" ), this ); + connect( m_oneToOne, SIGNAL(clicked()), (QObject*)videoWindow(), SLOT(resetZoom()) ); + connect( m_oneToOne, SIGNAL(clicked()), SLOT(deleteLater()) ); + + QBoxLayout *hbox = new QHBoxLayout( this, 8, 6 ); + QBoxLayout *vbox = new QVBoxLayout( hbox ); + vbox->addWidget( new QLabel( i18n( "<b>Adjust video scale?" ), this ) ); + vbox->addWidget( m_preferred ); + vbox->addWidget( m_oneToOne ); + hbox->addWidget( m_thingy = new QFrame( this ) ); + + m_thingy->setFixedWidth( fontMetrics().width( "X" ) ); + m_thingy->setFrameStyle( QFrame::Plain | QFrame::Box ); + m_thingy->setPaletteForegroundColor( paletteBackgroundColor().dark() ); + + QEvent e( QEvent::Resize ); + eventFilter( 0, &e ); + + adjustSize(); + show(); + + m_timerId = startTimer( 5 ); + } + + void + AdjustSizeButton::timerEvent( QTimerEvent* ) + { + QFrame *&h = m_thingy; + + switch( m_stage ) + { + case 1: //raise + move(); + m_offset++; + + if( m_offset > height() ) + killTimer( m_timerId ), + m_timerId = startTimer( 40 ), + m_stage = 2; + + break; + + case 2: //fill in pause timer bar + if( m_counter < h->height() - 3 ) + QPainter( h ).fillRect( 2, 2, h->width() - 4, m_counter, palette().active().highlight() ); + + if( !hasMouse() ) + m_counter++; + + if( m_counter > h->height() + 5 ) //pause for 360ms before lowering + m_stage = 3, + killTimer( m_timerId ), + m_timerId = startTimer( 6 ); + + break; + + case 3: //lower + if( hasMouse() ) { + m_stage = 1; + m_counter = 0; + m_thingy->repaint(); + break; } + + m_offset--; + move(); + + if( m_offset < 0 ) + deleteLater(); + } + } + + bool + AdjustSizeButton::eventFilter( QObject *o, QEvent *e ) + { + if( e->type() == QEvent::Resize ) { + const QSize preferredSize = TheStream::profile()->readSizeEntry( "Preferred Size" ); + const QSize defaultSize = TheStream::defaultVideoSize(); + const QSize parentSize = parentWidget()->size(); + + m_preferred->setEnabled( preferredSize.isValid() && parentSize != preferredSize && defaultSize != preferredSize ); + m_oneToOne->setEnabled( defaultSize != parentSize ); + + move(); + + if( !m_preferred->isEnabled() && !m_oneToOne->isEnabled() && m_counter == 0 ) + deleteLater(); + } + + return false; + } +} diff --git a/src/app/adjustSizeButton.h b/src/app/adjustSizeButton.h new file mode 100644 index 0000000..6eed27c --- /dev/null +++ b/src/app/adjustSizeButton.h @@ -0,0 +1,37 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_ADJUST_SIZE_BUTTON_H +#define CODEINE_ADJUST_SIZE_BUTTON_H + +#include <qframe.h> + +namespace Codeine +{ + class AdjustSizeButton : public QFrame + { + int m_counter; + int m_stage; + int m_offset; + int m_timerId; + + QWidget *m_preferred; + QWidget *m_oneToOne; + + QFrame *m_thingy; + + public: + AdjustSizeButton( QWidget *parent ); + + private: + virtual void timerEvent( QTimerEvent* ); + virtual bool eventFilter( QObject*, QEvent* ); + + inline void move() + { + QWidget::move( parentWidget()->width() - width(), parentWidget()->height() - m_offset ); + } + }; +} + +#endif diff --git a/src/app/analyzer.cpp b/src/app/analyzer.cpp new file mode 100644 index 0000000..c9b8637 --- /dev/null +++ b/src/app/analyzer.cpp @@ -0,0 +1,131 @@ +// (c) 2004 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "analyzer.h" +#include "codeine.h" +#include "debug.h" +#include <math.h> //interpolate() +#include <qevent.h> //event() +#include "xineEngine.h" + +#include "fht.cpp" + +template<class W> +Analyzer::Base<W>::Base( QWidget *parent, uint timeout ) + : W( parent, "Analyzer" ) + , m_timeout( timeout ) +{} + +template<class W> bool +Analyzer::Base<W>::event( QEvent *e ) +{ + switch( e->type() ) { + case QEvent::Hide: + m_timer.stop(); + break; + + case QEvent::Show: + m_timer.start( timeout() ); + break; + + default: + ; + } + + return QWidget::event( e ); +} + + +Analyzer::Base2D::Base2D( QWidget *parent, uint timeout ) + : Base<QWidget>( parent, timeout ) +{ + setWFlags( Qt::WNoAutoErase ); //no flicker + connect( &m_timer, SIGNAL(timeout()), SLOT(draw()) ); +} + +void +Analyzer::Base2D::draw() +{ + switch( Codeine::engine()->state() ) { + case Engine::Playing: + { + const Engine::Scope &thescope = Codeine::engine()->scope(); + static Analyzer::Scope scope( Analyzer::SCOPE_SIZE ); + + for( int x = 0; x < Analyzer::SCOPE_SIZE; ++x ) + scope[x] = double(thescope[x]) / (1<<15); + + transform( scope ); + analyze( scope ); + + scope.resize( Analyzer::SCOPE_SIZE ); + + bitBlt( this, 0, 0, canvas() ); + break; + } + case Engine::Paused: + break; + + default: + erase(); + } +} + +void +Analyzer::Base2D::resizeEvent( QResizeEvent* ) +{ + m_canvas.resize( size() ); + m_canvas.fill( colorGroup().background() ); +} + + + +// Author: Max Howell <[email protected]>, (C) 2003 +// Copyright: See COPYING file that comes with this distribution + +#include <qpainter.h> + +Analyzer::Block::Block( QWidget *parent ) + : Analyzer::Base2D( parent, 20 ) +{ + setMinimumWidth( 64 ); //-1 is padding, no drawing takes place there + setMaximumWidth( 128 ); + + //TODO yes, do height for width +} + +void +Analyzer::Block::transform( Analyzer::Scope &scope ) //pure virtual +{ + static FHT fht( Analyzer::SCOPE_SIZE_EXP ); + + for( uint x = 0; x < scope.size(); ++x ) + scope[x] *= 2; + + float *front = static_cast<float*>( &scope.front() ); + + fht.spectrum( front ); + fht.scale( front, 1.0 / 40 ); +} + +#include <math.h> +void +Analyzer::Block::analyze( const Analyzer::Scope &s ) +{ + canvas()->fill( colorGroup().foreground().light() ); + + QPainter p( canvas() ); + p.setPen( colorGroup().background() ); + + const double F = double(height()) / (log10( 256 ) * 1.1 /*<- max. amplitude*/); + + for( uint x = 0; x < s.size(); ++x ) + //we draw the blank bit + p.drawLine( x, 0, x, int(height() - log10( s[x] * 256.0 ) * F) ); +} + +int +Analyzer::Block::heightForWidth( int w ) const +{ + return w / 2; +} diff --git a/src/app/analyzer.h b/src/app/analyzer.h new file mode 100644 index 0000000..6fdb12f --- /dev/null +++ b/src/app/analyzer.h @@ -0,0 +1,75 @@ +// (c) 2004 Max Howell ([email protected])
+// See COPYING file for licensing information
+
+#ifndef ANALYZER_H
+#define ANALYZER_H
+
+#ifdef __FreeBSD__
+ #include <sys/types.h>
+#endif
+
+#include <qpixmap.h> //stack allocated and convenience
+#include <qtimer.h> //stack allocated
+#include <qwidget.h> //baseclass
+#include <vector> //included for convenience
+
+namespace Analyzer
+{
+ typedef std::vector<float> Scope;
+
+ template<class W> class Base : public W
+ {
+ public:
+ uint timeout() const { return m_timeout; }
+
+ protected:
+ Base( QWidget*, uint );
+
+ virtual void transform( Scope& ) = 0;
+ virtual void analyze( const Scope& ) = 0;
+
+ private:
+ virtual bool event( QEvent* );
+
+ protected:
+ QTimer m_timer;
+ uint m_timeout;
+ };
+
+ class Base2D : public Base<QWidget>
+ {
+ Q_OBJECT
+ public:
+ const QPixmap *canvas() const { return &m_canvas; }
+
+ private slots:
+ void draw();
+
+ protected:
+ Base2D( QWidget*, uint timeout );
+
+ QPixmap *canvas() { return &m_canvas; }
+
+ void paintEvent( QPaintEvent* ) { if( !m_canvas.isNull() ) bitBlt( this, 0, 0, canvas() ); }
+ void resizeEvent( QResizeEvent* );
+
+ private:
+ QPixmap m_canvas;
+ };
+
+ class Block : public Analyzer::Base2D
+ {
+ public:
+ Block( QWidget* );
+
+ protected:
+ virtual void transform( Analyzer::Scope& );
+ virtual void analyze( const Analyzer::Scope& );
+
+ virtual int heightForWidth( int ) const;
+
+ virtual void show() {} //TODO temporary as the scope plugin causes freezes
+ };
+}
+
+#endif
diff --git a/src/app/captureFrame.cpp b/src/app/captureFrame.cpp new file mode 100644 index 0000000..4cba5fd --- /dev/null +++ b/src/app/captureFrame.cpp @@ -0,0 +1,296 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "debug.h" +#include <kfiledialog.h> +#include <kpreviewwidgetbase.h> +#include <kpushbutton.h> +#include <kstatusbar.h> +#include <kstdguiitem.h> +#include "mainWindow.h" +#include "mxcl.library.h" +#include <qdialog.h> +#include <qhbox.h> +#include <qlabel.h> +#include <qimage.h> +#include <qlayout.h> +#include <qpainter.h> +#include <qstringlist.h> +#include "theStream.h" +#include "xineEngine.h" +#include <xine.h> + + +namespace Codeine { + +class FrameCapturePreview : public KPreviewWidgetBase +{ + QImage m_frame; + + virtual void showPreview( const KURL& ) {} + virtual void clearPreview() {} + + virtual void paintEvent( QPaintEvent* ) + { + QPainter painter( this ); + + const uint h = int( double(m_frame.height()) / m_frame.width() * (width()-5) ); + const uint y = (height() - h) / 2; + painter.drawImage( QRect( 5, y, width(), h ), m_frame ); + + const QString text = QString("%1x%2").arg( m_frame.width() ).arg( m_frame.height() ); + const uint x = (width() - fontMetrics().width( text ))/2; + painter.drawText( x, y + h + fontMetrics().height() + 5, text ); + } + +public: + FrameCapturePreview( const QImage& frame, QWidget *parent ) + : KPreviewWidgetBase( parent ) + , m_frame( frame ) + { + setMinimumWidth( 200 ); + } +}; + + +class FrameCaptureDialog : public QDialog +{ + const QImage m_frame; + const QString m_time; + const QString m_title; + + void message( const QString &text ) { ((MainWindow*)parentWidget())->statusBar()->message( text, 4000 ); } + +public: + FrameCaptureDialog( const QImage &frame, const QString &time, MainWindow *parent ) + : QDialog( parent, 0, false /*modal*/, Qt::WDestructiveClose ) + , m_frame( frame ) + , m_time( time ) + , m_title( TheStream::prettyTitle() ) + { + (new QVBoxLayout( this ))->setAutoAdd( true ); + (new QLabel( this ))->setPixmap( frame ); + + QHBox *box = new QHBox( this ); + KPushButton *o = new KPushButton( KStdGuiItem::save(), box ); + connect( o, SIGNAL(clicked()), SLOT(accept()) ); + + o = new KPushButton( KStdGuiItem::cancel(), box ); + o->setText( i18n("Discard") ); + connect( o, SIGNAL(clicked()), SLOT(reject()) ); + + setCaption( i18n("Capture - %1").arg( time ) ); + setFixedSize( sizeHint() ); + + show(); + + //TODO don't activate + //TODO move to the parent's side - not centrally aligned + } + + ~FrameCaptureDialog() + { + delete [] m_frame.bits(); + } + + virtual void accept() + { + KFileDialog dialog( ":frame_capture", i18n("*.png|PNG Format\n*.jpeg|JPEG Format"), this, 0, false ); + dialog.setOperationMode( KFileDialog::Saving ); + dialog.setCaption( i18n("Save Frame") ); + dialog.setSelection( m_title + " - " + m_time + ".png" ); + dialog.setPreviewWidget( new FrameCapturePreview( m_frame, &dialog ) ); + + if( dialog.exec() == Accepted ) { + const QString fileName = dialog.selectedFile(); + if( fileName.isEmpty() ) + return; + + const QString type = dialog.currentFilter().remove( 0, 2 ).upper(); + if( m_frame.save( fileName, type ) ) + message( i18n("%1 saved successfully").arg( fileName ) ); + else + message( i18n("Sorry, could not save %1").arg( fileName ) ); + } + + deleteLater(); + } +}; + + +void +MainWindow::captureFrame() +{ + new FrameCaptureDialog( videoWindow()->captureFrame(), m_timeLabel->text(), this ); +} + + +/************************************************************ + * Helpers to convert yuy and yv12 frames to rgb * + * code from gxine modified for 32bit output * + * Copyright (C) 2000-2003 the xine project * + ************************************************************/ + +static void +yuy2Toyv12( uint8_t *y, uint8_t *u, uint8_t *v, uint8_t *input, int w, int h ) +{ + const int w2 = w / 2; + for( int j, i = 0; i < h; i += 2 ) { + for( j = 0; j < w2; j++ ) + { + // packed YUV 422 is: Y[i] U[i] Y[i+1] V[i] + *(y++) = *(input++); + *(u++) = *(input++); + *(y++) = *(input++); + *(v++) = *(input++); + } + + // down sampling + for( j = 0; j < w2; j++ ) { + // skip every second line for U and V + *(y++) = *(input++); + input++; + *(y++) = *(input++); + input++; + } + } +} + +static uchar* +yv12ToRgb( uint8_t *src_y, uint8_t *src_u, uint8_t *src_v, const int w, const int h ) +{ + /// Create rgb data from yv12 + + #define clip_8_bit(val) \ + { \ + if( val < 0 ) \ + val = 0; \ + else if( val > 255 ) \ + val = 255; \ + } + + int y, u, v; + int r, g, b; + + int sub_i_uv; + int sub_j_uv; + + const int uv_width = w / 2; + const int uv_height = h / 2; + + uchar * const rgb = new uchar[(w * h * 4)]; //qt needs a 32bit align + if( !rgb ) + return 0; + + for( int i = 0; i < h; ++i ) { + // calculate u & v rows + sub_i_uv = ((i * uv_height) / h); + + for( int j = 0; j < w; ++j ) { + // calculate u & v columns + sub_j_uv = (j * uv_width) / w; + + /*************************************************** + * + * Colour conversion from + * http://www.inforamp.net/~poynton/notes/colour_and_gamma/ColorFAQ.html#RTFToC30 + * + * Thanks to Billy Biggs <[email protected]> + * for the pointer and the following conversion. + * + * R' = [ 1.1644 0 1.5960 ] ([ Y' ] [ 16 ]) + * G' = [ 1.1644 -0.3918 -0.8130 ] * ([ Cb ] - [ 128 ]) + * B' = [ 1.1644 2.0172 0 ] ([ Cr ] [ 128 ]) + * + * Where in xine the above values are represented as + * + * Y' == image->y + * Cb == image->u + * Cr == image->v + * + ***************************************************/ + + y = src_y[(i * w) + j] - 16; + u = src_u[(sub_i_uv * uv_width) + sub_j_uv] - 128; + v = src_v[(sub_i_uv * uv_width) + sub_j_uv] - 128; + + r = (int)((1.1644 * (double)y) + (1.5960 * (double)v)); + g = (int)((1.1644 * (double)y) - (0.3918 * (double)u) - (0.8130 * (double)v)); + b = (int)((1.1644 * (double)y) + (2.0172 * (double)u)); + + clip_8_bit( r ); + clip_8_bit( g ); + clip_8_bit( b ); + + rgb[(i * w + j) * 4 + 0] = b; + rgb[(i * w + j) * 4 + 1] = g; + rgb[(i * w + j) * 4 + 2] = r; + rgb[(i * w + j) * 4 + 3] = 0; + } + } + + return rgb; +} + +/************************************************************/ + + +QImage +VideoWindow::captureFrame() const +{ + DEBUG_BLOCK + + int ratio, format, w, h; + if( !xine_get_current_frame( *engine(), &w, &h, &ratio, &format, NULL ) ) + return QImage(); + + uint8_t *yuv = new uint8_t[((w+8) * (h+1) * 2)]; + if( yuv == 0 ) { + Debug::error() << "Not enough memory to make screenframe!\n"; + return QImage(); } + + xine_get_current_frame( *engine(), &w, &h, &ratio, &format, yuv ); + + // convert to yv12 if necessary + uint8_t *y = 0, *u = 0, *v = 0; + switch( format ) + { + case XINE_IMGFMT_YUY2: { + uint8_t *yuy2 = yuv; + + yuv = new uint8_t[(w * h * 2)]; + if( yuv == 0 ) { + Debug::error() << "Not enough memory to make screenframe!\n"; + delete [] yuy2; + return QImage(); } + + y = yuv; + u = yuv + w * h; + v = yuv + w * h * 5 / 4; + + yuy2Toyv12( y, u, v, yuy2, w, h ); + + delete [] yuy2; + } break; + + case XINE_IMGFMT_YV12: + y = yuv; + u = yuv + w * h; + v = yuv + w * h * 5 / 4; + break; + + default: + Debug::warning() << "Format " << format << " not supported!\n"; + delete [] yuv; + return QImage(); + } + + // convert to rgb + uchar *rgb = yv12ToRgb( y, u, v, w, h ); + QImage frame( rgb, w, h, 32, 0, 0, QImage::IgnoreEndian ); + delete [] yuv; + + return frame; +} + +} diff --git a/src/app/config.h b/src/app/config.h new file mode 100644 index 0000000..fbab5e8 --- /dev/null +++ b/src/app/config.h @@ -0,0 +1,20 @@ +// (c) 2004 Max Howell ([email protected])
+// See COPYING file for licensing information
+
+#ifndef CODEINECONFIG_H
+#define CODEINECONFIG_H
+
+#include <kconfig.h>
+#include <kglobal.h>
+
+namespace Codeine
+{
+ static inline KConfig *config( const QString &group )
+ {
+ KConfig* const instance = KGlobal::config();
+ instance->setGroup( group );
+ return instance;
+ }
+}
+
+#endif
diff --git a/src/app/extern.h b/src/app/extern.h new file mode 100644 index 0000000..20e49fd --- /dev/null +++ b/src/app/extern.h @@ -0,0 +1,28 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_EXTERN_H +#define CODEINE_EXTERN_H + +extern "C" +{ + typedef struct xine_s xine_t; +} + +class QPopupMenu; +class QWidget; + +namespace Codeine +{ + class VideoWindow; + class XineEngine; + + VideoWindow* const engine(); //defined in xineEngine.h + VideoWindow* const videoWindow(); //defined in xineEngine.h + + void showVideoSettingsDialog( QWidget* ); + void showXineConfigurationDialog( QWidget*, xine_t* ); + void insertAspectRatioMenuItems( QPopupMenu* ); +} + +#endif diff --git a/src/app/fht.cpp b/src/app/fht.cpp new file mode 100644 index 0000000..4d03851 --- /dev/null +++ b/src/app/fht.cpp @@ -0,0 +1,262 @@ +// FHT - Fast Hartley Transform Class +// +// Copyright (C) 2004 Melchior FRANZ - [email protected] +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA +// +// $Id: fht.cpp,v 1.3 2004/06/05 20:20:36 mfranz Exp $ + +#include <math.h> +#include <string.h> +#include "fht.h" + + +FHT::FHT(int n) : + m_buf(0), + m_tab(0), + m_log(0) +{ + if (n < 3) { + m_num = 0; + m_exp2 = -1; + return; + } + m_exp2 = n; + m_num = 1 << n; + if (n > 3) { + m_buf = new float[m_num]; + m_tab = new float[m_num * 2]; + makeCasTable(); + } +} + + +FHT::~FHT() +{ + delete[] m_buf; + delete[] m_tab; + delete[] m_log; +} + + +void FHT::makeCasTable(void) +{ + float d, *costab, *sintab; + int ul, ndiv2 = m_num / 2; + + for (costab = m_tab, sintab = m_tab + m_num / 2 + 1, ul = 0; ul < m_num; ul++) { + d = M_PI * ul / ndiv2; + *costab = *sintab = cos(d); + + costab += 2, sintab += 2; + if (sintab > m_tab + m_num * 2) + sintab = m_tab + 1; + } +} + + +float* FHT::copy(float *d, float *s) +{ + return (float *)memcpy(d, s, m_num * sizeof(float)); +} + + +float* FHT::clear(float *d) +{ + return (float *)memset(d, 0, m_num * sizeof(float)); +} + + +void FHT::scale(float *p, float d) +{ + for (int i = 0; i < (m_num / 2); i++) + *p++ *= d; +} + + +void FHT::ewma(float *d, float *s, float w) +{ + for (int i = 0; i < (m_num / 2); i++, d++, s++) + *d = *d * w + *s * (1 - w); +} + + +static inline float sind(float d) { return sin(d * M_PI / 180); } +void FHT::pattern(float *p, bool rect = false) +{ + static float f = 1.0; + static float h = 0.1; + int i; + for (i = 0; i < 3 * m_num / 4; i++, p++) { + float o = 360.0 * i / m_num; + *p = sind(f * o); + if (rect) + *p = *p < 0 ? -1.0 : 1.0; + } + for (; i < m_num; i++) + *p++ = 0.0; + if (f > m_num / 2.0 || f < .05) + h = -h; + f += h; +} + + +void FHT::logSpectrum(float *out, float *p) +{ + int n = m_num / 2, i, j, k, *r; + if (!m_log) { + m_log = new int[n]; + float f = n / log10(n); + for (i = 0, r = m_log; i < n; i++, r++) { + j = int(rint(log10(i + 1.0) * f)); + *r = j >= n ? n - 1 : j; + } + } + semiLogSpectrum(p); + *out++ = *p = *p / 100; + for (k = i = 1, r = m_log; i < n; i++) { + j = *r++; + if (i == j) + *out++ = p[i]; + else { + float base = p[k - 1]; + float step = (p[j] - base) / (j - (k - 1)); + for (float corr = 0; k <= j; k++, corr += step) + *out++ = base + corr; + } + } +} + + +void FHT::semiLogSpectrum(float *p) +{ + float e; + power2(p); + for (int i = 0; i < (m_num / 2); i++, p++) { + e = 10.0 * log10(sqrt(*p * .5)); + *p = e < 0 ? 0 : e; + } +} + + +void FHT::spectrum(float *p) +{ + power2(p); + for (int i = 0; i < (m_num / 2); i++, p++) + *p = (float)sqrt(*p * .5); +} + + +void FHT::power(float *p) +{ + power2(p); + for (int i = 0; i < (m_num / 2); i++) + *p++ *= .5; +} + + +void FHT::power2(float *p) +{ + int i; + float *q; + _transform(p, m_num, 0); + + *p = (*p * *p), *p += *p, p++; + + for (i = 1, q = p + m_num - 2; i < (m_num / 2); i++, --q) + *p++ = (*p * *p) + (*q * *q); +} + + +void FHT::transform(float *p) +{ + if (m_num == 8) + transform8(p); + else + _transform(p, m_num, 0); +} + + +void FHT::transform8(float *p) +{ + float a, b, c, d, e, f, g, h, b_f2, d_h2; + float a_c_eg, a_ce_g, ac_e_g, aceg, b_df_h, bdfh; + + a = *p++, b = *p++, c = *p++, d = *p++; + e = *p++, f = *p++, g = *p++, h = *p; + b_f2 = (b - f) * M_SQRT2; + d_h2 = (d - h) * M_SQRT2; + + a_c_eg = a - c - e + g; + a_ce_g = a - c + e - g; + ac_e_g = a + c - e - g; + aceg = a + c + e + g; + + b_df_h = b - d + f - h; + bdfh = b + d + f + h; + + *p = a_c_eg - d_h2; + *--p = a_ce_g - b_df_h; + *--p = ac_e_g - b_f2; + *--p = aceg - bdfh; + *--p = a_c_eg + d_h2; + *--p = a_ce_g + b_df_h; + *--p = ac_e_g + b_f2; + *--p = aceg + bdfh; +} + + +void FHT::_transform(float *p, int n, int k) +{ + if (n == 8) { + transform8(p + k); + return; + } + + int i, j, ndiv2 = n / 2; + float a, *t1, *t2, *t3, *t4, *ptab, *pp; + + for (i = 0, t1 = m_buf, t2 = m_buf + ndiv2, pp = &p[k]; i < ndiv2; i++) + *t1++ = *pp++, *t2++ = *pp++; + + memcpy(p + k, m_buf, sizeof(float) * n); + + _transform(p, ndiv2, k); + _transform(p, ndiv2, k + ndiv2); + + j = m_num / ndiv2 - 1; + t1 = m_buf; + t2 = t1 + ndiv2; + t3 = p + k + ndiv2; + ptab = m_tab; + pp = p + k; + + a = *ptab++ * *t3++; + a += *ptab * *pp; + ptab += j; + + *t1++ = *pp + a; + *t2++ = *pp++ - a; + + for (i = 1, t4 = p + k + n; i < ndiv2; i++, ptab += j) { + a = *ptab++ * *t3++; + a += *ptab * *--t4; + + *t1++ = *pp + a; + *t2++ = *pp++ - a; + } + memcpy(p + k, m_buf, sizeof(float) * n); +} + diff --git a/src/app/fht.h b/src/app/fht.h new file mode 100644 index 0000000..3dc5387 --- /dev/null +++ b/src/app/fht.h @@ -0,0 +1,126 @@ +// FHT - Fast Hartley Transform Class +// +// Copyright (C) 2004 Melchior FRANZ - [email protected] +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA +// +// $Id: fht.h,v 1.3 2004/06/05 20:20:36 mfranz Exp $ + +#ifndef FHT_H +#define FHT_H + +/** + * Implementation of the Hartley Transform after Bracewell's discrete + * algorithm. The algorithm is subject to US patent No. 4,646,256 (1987) + * but was put into public domain by the Board of Trustees of Stanford + * University in 1994 and is now freely available[1]. + * + * [1] Computer in Physics, Vol. 9, No. 4, Jul/Aug 1995 pp 373-379 + */ +class FHT +{ + int m_exp2; + int m_num; + float *m_buf; + float *m_tab; + int *m_log; + + /** + * Create a table of CAS (cosine and sine) values. + * Has only to be done in the constructor and saves from + * calculating the same values over and over while transforming. + */ + void makeCasTable(); + + /** + * Recursive in-place Hartley transform. For internal use only! + */ + void _transform(float *, int, int); + + public: + /** + * Prepare transform for data sets with @f$2^n@f$ numbers, whereby @f$n@f$ + * should be at least 3. Values of more than 3 need a trigonometry table. + * @see makeCasTable() + */ + FHT(int); + + ~FHT(); + inline int sizeExp() const { return m_exp2; } + inline int size() const { return m_num; } + float *copy(float *, float *); + float *clear(float *); + void scale(float *, float); + + /** + * Exponentially Weighted Moving Average (EWMA) filter. + * @param d is the filtered data. + * @param s is fresh input. + * @param w is the weighting factor. + */ + void ewma(float *d, float *s, float w); + + /** + * Test routine to create wobbling sine or rectangle wave. + * @param d destination vector. + * @param rect rectangle if true, sine otherwise. + */ + void pattern(float *d, bool rect); + + /** + * Logarithmic audio spectrum. Maps semi-logarithmic spectrum + * to logarithmic frequency scale, interpolates missing values. + * A logarithmic index map is calculated at the first run only. + * @param p is the input array. + * @param out is the spectrum. + */ + void logSpectrum(float *out, float *p); + + /** + * Semi-logarithmic audio spectrum. + */ + void semiLogSpectrum(float *); + + /** + * Fourier spectrum. + */ + void spectrum(float *); + + /** + * Calculates a mathematically correct FFT power spectrum. + * If further scaling is applied later, use power2 instead + * and factor the 0.5 in the final scaling factor. + * @see FHT::power2() + */ + void power(float *); + + /** + * Calculates an FFT power spectrum with doubled values as a + * result. The values need to be multiplied by 0.5 to be exact. + * Note that you only get @f$2^{n-1}@f$ power values for a data set + * of @f$2^n@f$ input values. + * @see FHT::power() + */ + void power2(float *); + + /** + * Discrete Hartley transform of data sets with 8 values. + */ + void transform8(float *); + + void transform(float *); +}; + +#endif diff --git a/src/app/fullScreenAction.cpp b/src/app/fullScreenAction.cpp new file mode 100644 index 0000000..f28da84 --- /dev/null +++ b/src/app/fullScreenAction.cpp @@ -0,0 +1,96 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "extern.h" +#include "fullScreenAction.h" +#include <klocale.h> +#include <kwin.h> +#include <qwidget.h> +#include "xineEngine.h" //videoWindow() + + +FullScreenAction::FullScreenAction( QWidget* window, KActionCollection *parent ) + : KToggleAction( QString::null, Key_F, 0, 0, parent, "fullscreen" ) + , m_window( window ) + , m_shouldBeDisabled( false ) + , m_state( 0 ) +{ + window->installEventFilter( this ); + setChecked( false ); +} + +void +FullScreenAction::setChecked( bool setChecked ) +{ + KToggleAction::setChecked( setChecked ); + + m_window->raise(); + + const int id = m_window->winId(); + if( setChecked ) { + setText( i18n("Exit F&ull Screen Mode") ); + setIcon("window_nofullscreen"); + m_state = KWin::windowInfo( id ).state(); + KWin::setState( id, NET::FullScreen ); + } + else { + setText(i18n("F&ull Screen Mode")); + setIcon("window_fullscreen"); + KWin::clearState( id, NET::FullScreen ); + KWin::setState( id, m_state ); // get round bug in KWin where it forgets maximisation state + } + + if( setChecked == false && m_shouldBeDisabled ) + setEnabled( false ); +} + +void +FullScreenAction::setEnabled( bool setEnabled ) +{ + if( setEnabled == false && isChecked() ) + // don't disable the action if we are currently in fullscreen mode + // as then the user can't exit fullscreen mode! Instead disable it + // when we next get toggled out of fullscreen mode + m_shouldBeDisabled = true; + + else { + //FIXME Codeine specific (because videoWindow isn't the window we control, we control the KMainWindow) + //NOTE also if the videoWindow is hidden at some point, this is broken.. + //TODO new type of actionclass that event filters and is always correct state + if( setEnabled && reinterpret_cast<QWidget*>(Codeine::videoWindow())->isHidden() ) + setEnabled = false; + + m_shouldBeDisabled = false; + KToggleAction::setEnabled( setEnabled ); + } +} + +bool +FullScreenAction::eventFilter( QObject *o, QEvent *e ) +{ + if( o == m_window ) + switch( e->type() ) { + #if QT_VERSION >= 0x030300 + case QEvent::WindowStateChange: + #else + case QEvent::ShowFullScreen: + case QEvent::ShowNormal: + case QEvent::ShowMaximized: + case QEvent::ShowMinimized: + #endif + if (m_window->isFullScreen() != isChecked()) + slotActivated(); // setChecked( window->isFullScreen()) wouldn't emit signals + + if (m_window->isFullScreen() && !isEnabled()) { + m_shouldBeDisabled = true; + setEnabled( true ); + } + + break; + + default: + ; + } + + return false; +} diff --git a/src/app/fullScreenAction.h b/src/app/fullScreenAction.h new file mode 100644 index 0000000..4234633 --- /dev/null +++ b/src/app/fullScreenAction.h @@ -0,0 +1,27 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include <kaction.h> + + +/** + * @class FullSCreenAction + * @author Max Howell <[email protected]> + * @short Adapted KToggleFullScreenAction, mainly because that class is shit + */ +class FullScreenAction : public KToggleAction +{ +public: + FullScreenAction( QWidget *window, KActionCollection* ); + + virtual void setChecked( bool ); + virtual void setEnabled( bool ); + +protected: + virtual bool eventFilter( QObject* o, QEvent* e ); + +private: + QWidget *m_window; + bool m_shouldBeDisabled; + unsigned long m_state; +}; diff --git a/src/app/insertAspectRatioMenuItems.cpp b/src/app/insertAspectRatioMenuItems.cpp new file mode 100644 index 0000000..353fe43 --- /dev/null +++ b/src/app/insertAspectRatioMenuItems.cpp @@ -0,0 +1,24 @@ +// Copyright 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include <qpopupmenu.h> +#include <xine.h> + +QString i18n( const char *text ); + + +namespace Codeine +{ + void + insertAspectRatioMenuItems( QPopupMenu *menu ) + { + menu->insertItem( i18n( "Determine &Automatically" ), XINE_VO_ASPECT_AUTO ); + menu->insertSeparator(); + menu->insertItem( i18n( "&Square (1:1)" ), XINE_VO_ASPECT_SQUARE ); + menu->insertItem( i18n( "&4:3" ), XINE_VO_ASPECT_4_3 ); + menu->insertItem( i18n( "Ana&morphic (16:9)" ), XINE_VO_ASPECT_ANAMORPHIC ); + menu->insertItem( i18n( "&DVB (2.11:1)" ), XINE_VO_ASPECT_DVB ); + + menu->setItemChecked( XINE_VO_ASPECT_AUTO, true ); + } +} diff --git a/src/app/listView.cpp b/src/app/listView.cpp new file mode 100644 index 0000000..b7990ec --- /dev/null +++ b/src/app/listView.cpp @@ -0,0 +1,39 @@ +// (c) 2004 Max Howell ([email protected])
+// See COPYING file for licensing information
+
+#ifndef CODEINELISTVIEW_CPP
+#define CODEINELISTVIEW_CPP
+
+#include <klistview.h>
+
+namespace Codeine
+{
+ class ListView : public KListView
+ {
+ public:
+ ListView( QWidget *parent ) : KListView( parent )
+ {
+ addColumn( QString::null, 0 );
+ addColumn( QString::null );
+
+ setResizeMode( LastColumn );
+ setMargin( 2 );
+ setSorting( -1 );
+ setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
+ setAllColumnsShowFocus( true );
+ setItemMargin( 3 );
+ }
+
+ virtual QSize sizeHint() const
+ {
+ const QSize sh = KListView::sizeHint();
+
+ return QSize( sh.width(),
+ childCount() == 0
+ ? 50
+ : QMIN( sh.height(), childCount() * (firstChild()->height()) + margin() * 2 + 4 + reinterpret_cast<QWidget*>(header())->height() ) );
+ }
+ };
+}
+
+#endif
diff --git a/src/app/main.cpp b/src/app/main.cpp new file mode 100644 index 0000000..7c0f6fc --- /dev/null +++ b/src/app/main.cpp @@ -0,0 +1,52 @@ +// (c) 2004 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "codeine.h" +#include <kaboutdata.h> +#include <kapplication.h> +#include <kcmdlineargs.h> +#include "mainWindow.h" +#include <X11/Xlib.h> + + +static KAboutData aboutData( APP_NAME, + I18N_NOOP(PRETTY_NAME), APP_VERSION, + I18N_NOOP("A video player that has a usability focus"), KAboutData::License_GPL_V2, + I18N_NOOP("Copyright 2006, Max Howell"), 0, + "http://www.methylblue.com/codeine/", + "[email protected]" ); + +static const KCmdLineOptions options[] = { + { "+[URL]", I18N_NOOP( "Play 'URL'" ), 0 }, + { "play-dvd", I18N_NOOP( "Play DVD Video" ), 0 }, + { 0, 0, 0 } }; + +int +main( int argc, char **argv ) +{ + //we need to do this, says adrianS from SuSE + if( !XInitThreads() ) + return 1; + + aboutData.addCredit( "Mike Diehl", I18N_NOOP("Handbook") ); + aboutData.addCredit( "The Kaffeine Developers", I18N_NOOP("Great reference code") ); + aboutData.addCredit( "Eric Prydz", I18N_NOOP("The video for \"Call on Me\" encouraged plenty of debugging! ;)") ); + aboutData.addCredit( "David Vignoni", I18N_NOOP("The current Codeine icon") ); + aboutData.addCredit( "Ian Monroe", I18N_NOOP("Patches, advice and moral support") ); + + + KCmdLineArgs::init( argc, argv, &aboutData ); + KCmdLineArgs::addCmdLineOptions( options ); + + KApplication application; + int returnValue; + + { + Codeine::MainWindow mainWindow; + mainWindow.show(); + + returnValue = application.exec(); + } + + return returnValue; +} diff --git a/src/app/mainWindow.cpp b/src/app/mainWindow.cpp new file mode 100644 index 0000000..856e0b6 --- /dev/null +++ b/src/app/mainWindow.cpp @@ -0,0 +1,714 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "actions.h" +#include "analyzer.h" +#include "config.h" +#include "configure.h" +#include <cstdlib> +#include "debug.h" +#include "extern.h" //dialog creation function definitions +#include "fullScreenAction.h" +#include <kapplication.h> +#include <kcmdlineargs.h> +#include <kcursor.h> +#include <kfiledialog.h> //::open() +#include <kglobalsettings.h> //::timerEvent() +#include <kio/netaccess.h> +#include <ksqueezedtextlabel.h> +#include <kstatusbar.h> +#include <ktoolbar.h> +#include <kurldrag.h> +#include <kwin.h> +#include "mainWindow.h" +#include "playDialog.h" //::play() +#include "playlistFile.h" +#include "mxcl.library.h" +#include <qcstring.h> +#include <qdesktopwidget.h> +#include <qevent.h> //::stateChanged() +#include <qlayout.h> //ctor +#include <qpopupmenu.h> //because XMLGUI is poorly designed +#include <qobjectlist.h> +#include "slider.h" +#include "theStream.h" +#include "volumeAction.h" +#include "xineEngine.h" + +#ifndef NO_XTEST_EXTENSION +extern "C" +{ + #include <X11/extensions/XTest.h> + #include <X11/keysym.h> +} +#endif + + +namespace Codeine { + + + /// @see codeine.h + QWidget *mainWindow() { return kapp->mainWidget(); } + + +MainWindow::MainWindow() + : KMainWindow() + , m_positionSlider( new Slider( this, 65535 ) ) + , m_timeLabel( new QLabel( " 0:00:00 ", this ) ) + , m_titleLabel( new KSqueezedTextLabel( this ) ) +{ + DEBUG_BLOCK + + clearWFlags( WDestructiveClose ); //we are allocated on the stack + + kapp->setMainWidget( this ); + + new VideoWindow( this ); + setCentralWidget( videoWindow() ); + setFocusProxy( videoWindow() ); // essential! See VideoWindow::event(), QEvent::FocusOut + + // these have no affect beccause "KDE Knows Best" FFS + setDockEnabled( toolBar(), Qt::DockRight, false ); //doesn't make sense due to our large horizontal slider + setDockEnabled( toolBar(), Qt::DockLeft, false ); //as above + + m_titleLabel->setMargin( 2 ); + m_timeLabel->setFont( KGlobalSettings::fixedFont() ); + m_timeLabel->setAlignment( AlignCenter ); + m_timeLabel->setMinimumSize( m_timeLabel->sizeHint() ); + + // work around a bug in KStatusBar + // sizeHint width of statusbar seems to get stupidly large quickly + statusBar()->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Maximum ); + + statusBar()->addWidget( m_titleLabel, 1, false ); + statusBar()->addWidget( m_analyzer = new Analyzer::Block( this ), 0, true ); + statusBar()->addWidget( m_timeLabel, 0, true ); + setupActions(); + setupGUI(); + setStandardToolBarMenuEnabled( false ); //bah to setupGUI()! + toolBar()->show(); //it's possible it would be hidden, but we don't want that as no UI way to show it! + + // only show dvd button when playing a dvd + { + struct KdeIsTehSuck : public QObject + { + virtual bool eventFilter( QObject*, QEvent *e ) + { + if (e->type() != QEvent::LayoutHint) + return false; + + // basically, KDE shows all tool-buttons, even if they are + // hidden after it does any layout operation. Yay for KDE. Yay. + QWidget *button = (QWidget*)((KMainWindow*)mainWindow())->toolBar()->child( "toolbutton_toggle_dvd_menu" ); + if (button) + button->setShown( TheStream::url().protocol() == "dvd" ); + return false; + } + } *o; + o = new KdeIsTehSuck; + toolBar()->installEventFilter( o ); + insertChild( o ); + } + + { + QPopupMenu *menu = 0, *settings = static_cast<QPopupMenu*>(factory()->container( "settings", this )); + int id = SubtitleChannelsMenuItemId, index = 0; + + #define make_menu( name, text ) \ + menu = new QPopupMenu( this, name ); \ + menu->setCheckable( true ); \ + connect( menu, SIGNAL(activated( int )), engine(), SLOT(setStreamParameter( int )) ); \ + connect( menu, SIGNAL(aboutToShow()), SLOT(aboutToShowMenu()) ); \ + settings->insertItem( text, menu, id, index ); \ + settings->setItemEnabled( id, false ); \ + id++, index++; + + make_menu( "subtitle_channels_menu", i18n( "&Subtitles" ) ); + make_menu( "audio_channels_menu", i18n( "A&udio Channels" ) ); + make_menu( "aspect_ratio_menu", i18n( "Aspect &Ratio" ) ); + #undef make_menu + + Codeine::insertAspectRatioMenuItems( menu ); //so we don't have to include xine.h here + + settings->insertSeparator( index ); + } + + QObjectList *list = toolBar()->queryList( "KToolBarButton" ); + if (list->isEmpty()) { + MessageBox::error( i18n( + "<qt>" PRETTY_NAME " could not load its interface, this probably means that " PRETTY_NAME " is not " + "installed to the correct prefix. If you installed from packages please contact the packager, if " + "you installed from source please try running the <b>configure</b> script again like this: " + "<pre> % ./configure --prefix=`kde-config --prefix`</pre>" ) ); + + std::exit( 1 ); + } + delete list; + + KXMLGUIClient::stateChanged( "empty" ); + + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + if( args->count() || args->isSet( "play-dvd" ) || kapp->isRestored() ) + //we need to resize the window, so we can't show the window yet + init(); + else { + //"faster" startup + //TODO if we have a size stored for this video, do the "faster" route + QTimer::singleShot( 0, this, SLOT(init()) ); + QApplication::setOverrideCursor( KCursor::waitCursor() ); } +} + +void +MainWindow::init() +{ + DEBUG_BLOCK + + connect( engine(), SIGNAL(statusMessage( const QString& )), this, SLOT(engineMessage( const QString& )) ); + connect( engine(), SIGNAL(stateChanged( Engine::State )), this, SLOT(engineStateChanged( Engine::State )) ); + connect( engine(), SIGNAL(channelsChanged( const QStringList& )), this, SLOT(setChannels( const QStringList& )) ); + connect( engine(), SIGNAL(titleChanged( const QString& )), m_titleLabel, SLOT(setText( const QString& )) ); + connect( m_positionSlider, SIGNAL(valueChanged( int )), this, SLOT(showTime( int )) ); + + if( !engine()->init() ) { + KMessageBox::error( this, i18n( + "<qt>xine could not be successfully initialised. " PRETTY_NAME " will now exit. " + "You can try to identify what is wrong with your xine installation using the <b>xine-check</b> command at a command-prompt.") ); + std::exit( 2 ); + } + + //would be dangerous for these to65535 happen before the videoWindow() is initialised + setAcceptDrops( true ); + connect( m_positionSlider, SIGNAL(sliderReleased( uint )), engine(), SLOT(seek( uint )) ); + connect( statusBar(), SIGNAL(messageChanged( const QString& )), engine(), SLOT(showOSD( const QString& )) ); + + QApplication::restoreOverrideCursor(); + + if( !kapp->isRestored() ) { + KCmdLineArgs &args = *KCmdLineArgs::parsedArgs(); + if (args.isSet( "play-dvd" )) + open( "dvd:/" ); + else if (args.count() > 0 ) { + open( args.url( 0 ) ); + args.clear(); + adjustSize(); //will resize us to reflect the videoWindow's sizeHint() + } + else + //show the welcome dialog + playMedia( true ); // true = show in style of welcome dialog + } + else + //session management must be done after the videoWindow() has been initialised + restore( 1, false ); + + //don't do until videoWindow() is initialised! + startTimer( 50 ); +} + +MainWindow::~MainWindow() +{ + DEBUG_FUNC_INFO + + hide(); //so we appear to have quit, and then sound fades out below + + delete videoWindow(); //fades out sound in dtor +} + +bool +MainWindow::queryExit() +{ + if( toggleAction( "fullscreen" )->isChecked() ) { + // there seems to be no other way to stop KMainWindow + // saving the window state without any controls + fullScreenToggled( false ); + showNormal(); + QApplication::sendPostedEvents( this, 0 ); + // otherwise KMainWindow saves the screensize as maximised + Codeine::MessageBox::sorry( + "This annoying messagebox is to get round a bug in either KDE or Qt. " + "Just press OK and Codeine will quit." ); + //NOTE not actually needed + saveAutoSaveSettings(); + hide(); + } + + return true; +} + +void +MainWindow::setupActions() +{ + DEBUG_BLOCK + + KActionCollection * const ac = actionCollection(); + + KStdAction::quit( kapp, SLOT(quit()), ac ); + KStdAction::open( this, SLOT(playMedia()), ac, "play_media" )->setText( i18n("Play &Media...") ); + connect( new FullScreenAction( this, ac ), SIGNAL(toggled( bool )), SLOT(fullScreenToggled( bool )) ); + + new PlayAction( this, SLOT(play()), ac ); + new KAction( i18n("Stop"), "player_stop", Key_S, engine(), SLOT(stop()), ac, "stop" ); + + new KToggleAction( i18n("Record"), "player_record", CTRL + Key_R, engine(), SLOT(record()), ac, "record" ); + + new KAction( i18n("Reset Video Scale"), "viewmag1", Key_Equal, videoWindow(), SLOT(resetZoom()), ac, "reset_zoom" ); + new KAction( i18n("Media Information"), "messagebox_info", Key_I, this, SLOT(streamInformation()), ac, "information" ); + new KAction( i18n("Menu Toggle"), "dvd_unmount", Key_R, engine(), SLOT(toggleDVDMenu()), ac, "toggle_dvd_menu" ); + new KAction( i18n("&Capture Frame"), "frame_image", Key_C, this, SLOT(captureFrame()), ac, "capture_frame" ); + + new KAction( i18n("Video Settings..."), "configure", Key_V, this, SLOT(configure()), ac, "video_settings" ); + new KAction( i18n("Configure xine..."), "configure", 0, this, SLOT(configure()), ac, "xine_settings" ); + + (new KWidgetAction( m_positionSlider, i18n("Position Slider"), 0, 0, 0, ac, "position_slider" ))->setAutoSized( true ); + + new VolumeAction( toolBar(), ac ); +} + +void +MainWindow::saveProperties( KConfig *config ) +{ + config->writeEntry( "url", TheStream::url().url() ); + config->writeEntry( "time", engine()->time() ); +} + +void +MainWindow::readProperties( KConfig *config ) +{ + if( engine()->load( config->readPathEntry( "url" ) ) ) + engine()->play( config->readNumEntry( "time" ) ); +} + +void +MainWindow::timerEvent( QTimerEvent* ) +{ + static int counter = 0; + + if( engine()->state() == Engine::Playing ) { + ++counter &= 1023; + + m_positionSlider->setValue( engine()->position() ); + if( !m_positionSlider->isEnabled() && counter % 10 == 0 ) // 0.5 seconds + // usually the slider emits a signal that updates the timeLabel + // but not if the slider isn't moving because there is no length + showTime(); + + #ifndef NO_XTEST_EXTENSION + if( counter == 0 /*1020*/ ) { // 51 seconds //do at 0 to ensure screensaver doesn't happen before 51 seconds is up (somehow) + const bool isOnThisDesktop = KWin::windowInfo( winId() ).isOnDesktop( KWin::currentDesktop() ); + + if( videoWindow()->isVisible() && isOnThisDesktop ) { + int key = XKeysymToKeycode( x11Display(), XK_Shift_R ); + + XTestFakeKeyEvent( x11Display(), key, true, CurrentTime ); + XTestFakeKeyEvent( x11Display(), key, false, CurrentTime ); + XSync( x11Display(), false ); + } + } + #endif + } +} + +void +MainWindow::showTime( int pos ) +{ + #define zeroPad( n ) n < 10 ? QString("0%1").arg( n ) : QString::number( n ) + + const int ms = (pos == -1) ? engine()->time() : int(engine()->length() * (pos / 65535.0)); + const int s = ms / 1000; + const int m = s / 60; + const int h = m / 60; + + QString time = zeroPad( s % 60 ); //seconds + time.prepend( ':' ); + time.prepend( zeroPad( m % 60 ) ); //minutes + time.prepend( ':' ); + time.prepend( QString::number( h ) ); //hours + + m_timeLabel->setText( time ); +} + +void +MainWindow::engineMessage( const QString &message ) +{ + statusBar()->message( message, 3500 ); +} + +bool +MainWindow::open( const KURL &url ) +{ + DEBUG_BLOCK + debug() << url << endl; + + if( load( url ) ) { + const int offset = TheStream::hasProfile() + // adjust offset if we have session history for this video + ? TheStream::profile()->readNumEntry( "Position", 0 ) + : 0; + + return engine()->play( offset ); + } + + return false; +} + +bool +MainWindow::load( const KURL &url ) +{ + //FileWatch the file that is opened + + if( url.isEmpty() ) { + MessageBox::sorry( i18n( "Codeine was asked to open an empty URL; it cannot." ) ); + return false; + } + + PlaylistFile playlist( url ); + if( playlist.isPlaylist() ) { + //TODO: problem is we return out of the function + //statusBar()->message( i18n("Parsing playlist file...") ); + + if( playlist.isValid() ) + return engine()->load( playlist.firstUrl() ); + else { + MessageBox::sorry( playlist.error() ); + return false; + } + } + + if (url.protocol() == "media") { + #define UDS_LOCAL_PATH (72 | KIO::UDS_STRING) + KIO::UDSEntry e; + if (!KIO::NetAccess::stat( url, e, 0 )) + MessageBox::sorry( "There was an internal error with the media slave..." ); + else { + KIO::UDSEntry::ConstIterator end = e.end(); + for (KIO::UDSEntry::ConstIterator it = e.begin(); it != end; ++it) + if ((*it).m_uds == UDS_LOCAL_PATH && !(*it).m_str.isEmpty()) + return engine()->load( KURL::fromPathOrURL( (*it).m_str ) ); + } + } + + //let xine handle invalid, etc, KURLS + //TODO it handles non-existant files with bad error message + return engine()->load( url ); +} + +void +MainWindow::play() +{ + switch( engine()->state() ) { + case Engine::Loaded: + engine()->play(); + break; + + case Engine::Playing: + case Engine::Paused: + engine()->pause(); + break; + + case Engine::Empty: + default: + playMedia(); + break; + } +} + +void +MainWindow::playMedia( bool show_welcome_dialog ) +{ + PlayDialog dialog( this, show_welcome_dialog ); + + switch( dialog.exec() ) { + case PlayDialog::FILE: { + const QString filter = engine()->fileFilter() + '|' + i18n("Supported Media Formats") + "\n*|" + i18n("All Files"); + const KURL url = KFileDialog::getOpenURL( ":default", filter, this, i18n("Select A File To Play") ); + open( url ); + } break; + case PlayDialog::RECENT_FILE: + open( dialog.url() ); + break; + case PlayDialog::CDDA: + open( "cdda:/1" ); + break; + case PlayDialog::VCD: + open( "vcd://" ); // one / is not enough + break; + case PlayDialog::DVD: + open( "dvd:/" ); + break; + } +} + +class FullScreenToolBarHandler : QObject +{ + KToolBar *m_toolbar; + int m_timer_id; + bool m_stay_hidden_for_a_bit; + QPoint m_home; + +public: + FullScreenToolBarHandler( KMainWindow *parent ) + : QObject( parent ) + , m_toolbar( parent->toolBar() ) + , m_timer_id( 0 ) + , m_stay_hidden_for_a_bit( false ) + { + DEBUG_BLOCK + + parent->installEventFilter( this ); + m_toolbar->installEventFilter( this ); + } + + bool eventFilter( QObject *o, QEvent *e ) + { + if (o == parent() && e->type() == QEvent::MouseMove) { + killTimer( m_timer_id ); + + QMouseEvent const * const me = (QMouseEvent*)e; + if (m_stay_hidden_for_a_bit) { + // wait for a small pause before showing the toolbar again + // usage = user removes mouse from toolbar after using it + // toolbar disappears (usage is over) but usually we show + // toolbar immediately when mouse is moved.. so we need this hack + + // HACK if user thrusts mouse to top, we assume they really want the toolbar + // back. Is hack as 80% of users have at top, but 20% at bottom, we don't cater + // for the 20% as lots more code, for now. + if (me->pos().y() < m_toolbar->height()) + goto show_toolbar; + + m_timer_id = startTimer( 100 ); + } + else { + if (m_toolbar->isHidden()) { + if (m_home.isNull()) + m_home = me->pos(); + else if ((m_home - me->pos()).manhattanLength() > 6) + // then cursor has moved far enough to trigger show toolbar +show_toolbar: + m_toolbar->show(), + m_home = QPoint(); + else + // cursor hasn't moved far enough yet + // don't reset timer below, return instead + return false; + } + + // reset the hide timer + m_timer_id = startTimer( VideoWindow::CURSOR_HIDE_TIMEOUT ); + } + } + + if (o == parent() && e->type() == QEvent::Resize) + { + //we aren't managed by mainWindow when at FullScreen + videoWindow()->move( 0, 0 ); + videoWindow()->resize( ((QWidget*)o)->size() ); + videoWindow()->lower(); + } + + if (o == m_toolbar) + switch (e->type()) { + case QEvent::Enter: + m_stay_hidden_for_a_bit = false; + killTimer( m_timer_id ); + break; + + case QEvent::Leave: + m_toolbar->hide(); + m_stay_hidden_for_a_bit = true; + killTimer( m_timer_id ); + m_timer_id = startTimer( 100 ); + break; + + default: break; + } + + return false; + } + + void timerEvent( QTimerEvent* ) + { + if (m_stay_hidden_for_a_bit) + ; + + else if (!m_toolbar->hasMouse()) + m_toolbar->hide(); + + m_stay_hidden_for_a_bit = false; + } +}; + + +void +MainWindow::fullScreenToggled( bool isFullScreen ) +{ + static FullScreenToolBarHandler *s_handler; + + DEBUG_FUNC_INFO + + if( isFullScreen ) + toolBar()->setPalette( palette() ), // due to 2px spacing in QMainWindow :( + setPaletteBackgroundColor( Qt::black ); // due to 2px spacing + else + toolBar()->unsetPalette(), + unsetPalette(); + + toolBar()->setMovingEnabled( !isFullScreen ); + toolBar()->setHidden( isFullScreen && engine()->state() == Engine::Playing ); + + reinterpret_cast<QWidget*>(menuBar())->setHidden( isFullScreen ); + statusBar()->setHidden( isFullScreen ); + + setMouseTracking( isFullScreen ); /// @see mouseMoveEvent() + + if (isFullScreen) + s_handler = new FullScreenToolBarHandler( this ); + else + delete s_handler; + + // prevent videoWindow() moving around when mouse moves + setCentralWidget( isFullScreen ? 0 : videoWindow() ); +} + +void +MainWindow::configure() +{ + const QCString sender = this->sender()->name(); + + if( sender == "video_settings" ) + Codeine::showVideoSettingsDialog( this ); + + else if( sender == "xine_settings" ) + Codeine::showXineConfigurationDialog( this, *engine() ); +} + +void +MainWindow::streamInformation() +{ + MessageBox::information( TheStream::information(), i18n("Media Information") ); +} + +void +MainWindow::setChannels( const QStringList &channels ) +{ + DEBUG_FUNC_INFO + + //TODO -1 = auto + + QStringList::ConstIterator it = channels.begin(); + + QPopupMenu *menu = (QPopupMenu*)child( (*it).latin1() ); + menu->clear(); + + menu->insertItem( i18n("&Determine Automatically"), 1 ); + menu->insertSeparator(); + + //the id is crucial, since the slot this menu is connected to requires + //that information to set the correct channel + //NOTE we subtract 2 in xineEngine because QMenuData doesn't allow negative id + int id = 2; + ++it; + for( QStringList::ConstIterator const end = channels.end(); it != end; ++it, ++id ) + menu->insertItem( *it, id ); + + menu->insertSeparator(); + menu->insertItem( i18n("&Off"), 0 ); + + id = channels.first() == "subtitle_channels_menu" ? SubtitleChannelsMenuItemId : AudioChannelsMenuItemId; + MainWindow::menu( "settings" )->setItemEnabled( id, channels.count() > 1 ); +} + +void +MainWindow::aboutToShowMenu() +{ + QPopupMenu *menu = (QPopupMenu*)sender(); + QCString name( sender() ? sender()->name() : 0 ); + + // uncheck all items first + for( uint x = 0; x < menu->count(); ++x ) + menu->setItemChecked( menu->idAt( x ), false ); + + int id; + if( name == "subtitle_channels_menu" ) + id = TheStream::subtitleChannel() + 2; + else if( name == "audio_channels_menu" ) + id = TheStream::audioChannel() + 2; + else + id = TheStream::aspectRatio(); + + menu->setItemChecked( id, true ); +} + +void +MainWindow::dragEnterEvent( QDragEnterEvent *e ) +{ + e->accept( KURLDrag::canDecode( e ) ); +} + +void +MainWindow::dropEvent( QDropEvent *e ) +{ + KURL::List list; + KURLDrag::decode( e, list ); + + if( !list.isEmpty() ) + open( list.first() ); + else + engineMessage( i18n("Sorry, no media was found in the drop") ); +} + +void +MainWindow::keyPressEvent( QKeyEvent *e ) +{ + #define seek( step ) { \ + const int new_pos = m_positionSlider->value() step; \ + engine()->seek( new_pos > 0 ? (uint)new_pos : 0 ); \ + } + + switch( e->key() ) + { + case Qt::Key_Left: seek( -500 ); break; + case Qt::Key_Right: seek( +500 ); break; + case Key_Escape: KWin::clearState( winId(), NET::FullScreen ); + default: ; + } + + #undef seek +} + +QPopupMenu* +MainWindow::menu( const char *name ) +{ + // KXMLGUI is "really good". + return static_cast<QPopupMenu*>(factory()->container( name, this )); +} + + +/// Convenience class for other classes that need access to the actionCollection +KActionCollection* +actionCollection() +{ + return static_cast<MainWindow*>(kapp->mainWidget())->actionCollection(); +} + +/// Convenience class for other classes that need access to the actions +KAction* +action( const char *name ) +{ + #define QT_FATAL_ASSERT + + MainWindow *mainWindow = 0; + KActionCollection *actionCollection = 0; + KAction *action = 0; + + if( mainWindow = (MainWindow*)kapp->mainWidget() ) + if( actionCollection = mainWindow->actionCollection() ) + action = actionCollection->action( name ); + + Q_ASSERT( mainWindow ); + Q_ASSERT( actionCollection ); + Q_ASSERT( action ); + + return action; +} + +} //namespace Codeine diff --git a/src/app/mainWindow.h b/src/app/mainWindow.h new file mode 100644 index 0000000..63d8468 --- /dev/null +++ b/src/app/mainWindow.h @@ -0,0 +1,75 @@ +// (c) 2004 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINEMAINWINDOW_H +#define CODEINEMAINWINDOW_H + +#include "codeine.h" +#include <kmainwindow.h> + +class KURL; +class QLabel; +class QPopupMenu; +class QSlider; + + +namespace Codeine +{ + class MainWindow : public KMainWindow + { + Q_OBJECT + + MainWindow(); + ~MainWindow(); + + friend int ::main( int, char** ); + + enum { SubtitleChannelsMenuItemId = 2000, AudioChannelsMenuItemId, AspectRatioMenuItemId }; + + public slots: + void play(); + void playMedia( bool show_welcome_dialog = false ); + + void configure(); + void streamInformation(); + void captureFrame(); + + private slots: + void engineMessage( const QString& ); + void engineStateChanged( Engine::State ); + void init(); + void showTime( int = -1 ); + void setChannels( const QStringList& ); + void aboutToShowMenu(); + void fullScreenToggled( bool ); + + private: + void setupActions(); + + bool load( const KURL& ); + bool open( const KURL& ); + + QPopupMenu *menu( const char *name ); + + virtual void timerEvent( QTimerEvent* ); + virtual void dragEnterEvent( QDragEnterEvent* ); + virtual void dropEvent( QDropEvent* ); + virtual void keyPressEvent( QKeyEvent* ); + + virtual void saveProperties( KConfig* ); + virtual void readProperties( KConfig* ); + + virtual bool queryExit(); + + QSlider *m_positionSlider; + QLabel *m_timeLabel; + QLabel *m_titleLabel; + QWidget *m_analyzer; + + //undefined + MainWindow( const MainWindow& ); + MainWindow &operator=( const MainWindow& ); + }; +} + +#endif diff --git a/src/app/playDialog.cpp b/src/app/playDialog.cpp new file mode 100644 index 0000000..50a9ca2 --- /dev/null +++ b/src/app/playDialog.cpp @@ -0,0 +1,114 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "config.h" +#include "listView.cpp" +#include <kapplication.h> +#include <kconfig.h> +#include <kguiitem.h> +#include <klistview.h> +#include <kpushbutton.h> +#include <kstdguiitem.h> +#include "playDialog.h" +#include "mxcl.library.h" +#include <qfile.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qsignalmapper.h> + +QString i18n( const char *text ); + + +namespace Codeine { + + +PlayDialog::PlayDialog( QWidget *parent, bool be_welcome_dialog ) + : QDialog( parent ) +{ + setCaption( kapp->makeStdCaption( i18n("Play Media") ) ); + + QSignalMapper *mapper = new QSignalMapper( this ); + QWidget *o, *closeButton = new KPushButton( KStdGuiItem::close(), this ); + QBoxLayout *hbox, *vbox = new QVBoxLayout( this, 15, 20 ); + + vbox->addWidget( new QLabel( i18n( "What media would you like to play?" ), this ) ); + + QGridLayout *grid = new QGridLayout( vbox, 1, 3, 20 ); + + //TODO use the kguiItems from the actions + mapper->setMapping( o = new KPushButton( KGuiItem( i18n("Play File..."), "fileopen" ), this ), FILE ); + connect( o, SIGNAL(clicked()), mapper, SLOT(map()) ); + grid->QLayout::add( o ); + + mapper->setMapping( o = new KPushButton( KGuiItem( i18n("Play VCD"), "cdaudio_unmount" ), this ), VCD ); + connect( o, SIGNAL(clicked()), mapper, SLOT(map()) ); + grid->QLayout::add( o ); + + mapper->setMapping( o = new KPushButton( KGuiItem( i18n("Play DVD"), "dvd_unmount" ), this ), DVD ); + connect( o, SIGNAL(clicked()), mapper, SLOT(map()) ); + grid->QLayout::add( o ); + + mapper->setMapping( closeButton, QDialog::Rejected ); + connect( closeButton, SIGNAL(clicked()), mapper, SLOT(map()) ); + + createRecentFileWidget( vbox ); + + hbox = new QHBoxLayout( vbox ); + hbox->addItem( new QSpacerItem( 10, 10, QSizePolicy::Expanding ) ); + + if( be_welcome_dialog ) { + QWidget *w = new KPushButton( KStdGuiItem::quit(), this ); + hbox->addWidget( w ); + connect( w, SIGNAL(clicked()), kapp, SLOT(quit()) ); + } + + hbox->addWidget( closeButton ); + + connect( mapper, SIGNAL(mapped( int )), SLOT(done( int )) ); +} + +void +PlayDialog::createRecentFileWidget( QBoxLayout *layout ) +{ + KListView *lv; + lv = new Codeine::ListView( this ); + lv->setColumnText( 1, i18n("Recently Played Media") ); + + const QStringList list1 = Codeine::config( "General" )->readPathListEntry( "Recent Urls" ); + KURL::List urls; + + foreach( list1 ) + urls += *it; + + for( KURL::List::Iterator it = urls.begin(), end = urls.end(); it != end; ) { + if( urls.contains( *it ) > 1 ) + //remove duplicates + it = urls.remove( it ); + else if( (*it).protocol() == "file" && !QFile::exists( (*it).path() ) ) + //remove stale entries + it = urls.remove( it ); + else + ++it; + } + + for( KURL::List::ConstIterator it = urls.begin(), end = urls.end(); it != end; ++it ) { + const QString fileName = (*it).fileName(); + new KListViewItem( lv, 0, (*it).url(), fileName.isEmpty() ? (*it).prettyURL() : fileName ); + } + + if( lv->childCount() ) { + layout->addWidget( lv, 1 ); + connect( lv, SIGNAL(executed( QListViewItem* )), SLOT(done( QListViewItem* )) ); + } + else + delete lv; +} + +void +PlayDialog::done( QListViewItem *item ) +{ + m_url = item->text( 0 ); + QDialog::done( RECENT_FILE ); +} + +} diff --git a/src/app/playDialog.h b/src/app/playDialog.h new file mode 100644 index 0000000..020f9f1 --- /dev/null +++ b/src/app/playDialog.h @@ -0,0 +1,36 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINEPLAYDIALOG_H +#define CODEINEPLAYDIALOG_H + +#include <kurl.h> +#include <qdialog.h> + +class KListView; +class QBoxLayout; +class QListViewItem; + +namespace Codeine +{ + class PlayDialog : public QDialog + { + Q_OBJECT + public: + PlayDialog( QWidget*, bool show_welcome_dialog = false ); + + const KURL &url() const { return m_url; } + + enum DialogCode { FILE = QDialog::Accepted + 2, VCD, CDDA, DVD, RECENT_FILE }; + + private slots: + void done( QListViewItem* ); + + private: + void createRecentFileWidget( QBoxLayout* ); + + KURL m_url; + }; +} + +#endif diff --git a/src/app/playlistFile.cpp b/src/app/playlistFile.cpp new file mode 100644 index 0000000..19acd30 --- /dev/null +++ b/src/app/playlistFile.cpp @@ -0,0 +1,123 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + + +//TODO error messages that vary depending on if the file is remote or not + + +#include "codeine.h" +#include "debug.h" +#include <kio/netaccess.h> +#include "playlistFile.h" +#include <qfile.h> +#include <qtextstream.h> +#include <mxcl.library.h> + + +PlaylistFile::PlaylistFile( const KURL &url ) + : m_url( url ) + , m_isRemoteFile( !url.isLocalFile() ) + , m_isValid( false ) +{ + mxcl::WaitCursor allocateOnStack; + + QString &path = m_path = url.path(); + + if( path.endsWith( ".pls", false ) ) + m_type = PLS; else + if( path.endsWith( ".m3u", false ) ) + m_type = M3U; + else { + m_type = Unknown; + m_error = i18n( "The file is not a playlist" ); + return; + } + + if( m_isRemoteFile ) { + path = QString(); + if( !KIO::NetAccess::download( url, path, Codeine::mainWindow() ) ) { + m_error = i18n( "Codeine could not download the remote playlist: %1" ).arg( url.prettyURL() ); + return; + } + } + + QFile file( path ); + if( file.open( IO_ReadOnly ) ) { + QTextStream stream( &file ); + switch( m_type ) { + case M3U: parseM3uFile( stream ); break; + case PLS: parsePlsFile( stream ); break; + default: ; + } + + if( m_contents.isEmpty() ) + m_error = i18n( "<qt>The playlist, <i>'%1'</i>, could not be interpreted. Perhaps it is empty?" ).arg( path ), + m_isValid = false; + } + else + m_error = i18n( "Codeine could not open the file: %1" ).arg( path ); +} + + +PlaylistFile::~PlaylistFile() +{ + if( m_isRemoteFile ) + KIO::NetAccess::removeTempFile( m_path ); +} + + +void +PlaylistFile::parsePlsFile( QTextStream &stream ) +{ + DEBUG_BLOCK + + for( QString line = stream.readLine(); !line.isNull(); ) + { + if( line.startsWith( "File" ) ) { + const KURL url = line.section( '=', -1 ); + const QString title = stream.readLine().section( '=', -1 ); + + debug() << url << endl << title << endl; + + m_contents += url; + m_isValid = true; + + return; //TODO continue for all urls + } + line = stream.readLine(); + } +} + + +void +PlaylistFile::parseM3uFile( QTextStream &stream ) +{ + DEBUG_BLOCK + + for( QString line; !stream.atEnd(); ) + { + line = stream.readLine(); + + if( line.startsWith( "#EXTINF", false ) ) + continue; + + else if( !line.startsWith( "#" ) && !line.isEmpty() ) + { + KURL url; + + // KURL::isRelativeURL() expects absolute URLs to start with a protocol, so prepend it if missing + if( line.startsWith( "/" ) ) + line.prepend( "file://" ); + + if( KURL::isRelativeURL( line ) ) + url.setPath( m_url.directory() + line ); + else + url = KURL::fromPathOrURL( line ); + + m_contents += url; + m_isValid = true; + + return; + } + } +} diff --git a/src/app/playlistFile.h b/src/app/playlistFile.h new file mode 100644 index 0000000..0302a85 --- /dev/null +++ b/src/app/playlistFile.h @@ -0,0 +1,36 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_PLAYLIST_FILE_H +#define CODEINE_PLAYLIST_FILE_H + +#include <kurl.h> + +class PlaylistFile +{ +public: + PlaylistFile( const KURL &url ); + ~PlaylistFile(); + + enum FileFormat { M3U, PLS, Unknown, NotPlaylistFile = Unknown }; + + bool isPlaylist() const { return m_type != Unknown; } + bool isValid() const { return m_isValid; } + KURL firstUrl() const { return m_contents.isEmpty() ? KURL() : m_contents.first(); } + QString error() const { return m_error; } + +private: + /// both only return first url currently + void parsePlsFile( QTextStream& ); + void parseM3uFile( QTextStream& ); + + KURL m_url; + bool m_isRemoteFile; + bool m_isValid; + QString m_error; + FileFormat m_type; + QString m_path; + KURL::List m_contents; +}; + +#endif diff --git a/src/app/slider.cpp b/src/app/slider.cpp new file mode 100644 index 0000000..89b5ced --- /dev/null +++ b/src/app/slider.cpp @@ -0,0 +1,145 @@ +// (c) 2004 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "debug.h" +#include "slider.h" +#include <qapplication.h> +#include <qlabel.h> +#include <qsize.h> +#include <qtooltip.h> + +#include <qpainter.h> +#include "xineEngine.h" + +using Codeine::Slider; + + +Slider *Slider::s_instance = 0; + + +Slider::Slider( QWidget *parent, uint max ) + : QSlider( Qt::Horizontal, parent ) + , m_sliding( false ) + , m_outside( false ) + , m_prevValue( 0 ) +{ + s_instance = this; + + setRange( 0, max ); + setFocusPolicy( NoFocus ); + setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding ); +} + +void +Slider::wheelEvent( QWheelEvent *e ) +{ + //if you use this class elsewhere, NOTE this is Codeine specific + e->ignore(); //pass to VideoWindow +} + +void +Slider::mouseMoveEvent( QMouseEvent *e ) +{ + if( m_sliding ) + { + //feels better, but using set value of 20 is bad of course + QRect rect = this->rect(); + rect.addCoords( -20, -20, 20, 20 ); + + if( !rect.contains( e->pos() ) ) { + if( !m_outside ) + QSlider::setValue( m_prevValue ); + m_outside = true; + } else { + m_outside = false; + + QSlider::setValue( + QRangeControl::valueFromPosition( + e->pos().x() - sliderRect().width()/2, + width() - sliderRect().width() ) ); + + emit sliderMoved( value() ); + } + } + else + QSlider::mouseMoveEvent( e ); +} + +void +Slider::mousePressEvent( QMouseEvent *e ) +{ + m_sliding = true; + m_prevValue = QSlider::value(); + + if( !sliderRect().contains( e->pos() ) ) + mouseMoveEvent( e ); +} + +void +Slider::mouseReleaseEvent( QMouseEvent* ) +{ + if( !m_outside && QSlider::value() != m_prevValue ) + emit sliderReleased( value() ); + + m_sliding = false; + m_outside = false; +} + +static inline QString timeAsString( const int s ) +{ + #define zeroPad( n ) n < 10 ? QString("0%1").arg( n ) : QString::number( n ) + using Codeine::engine; + + const int m = s / 60; + const int h = m / 60; + + QString time; + time.prepend( zeroPad( s % 60 ) ); //seconds + time.prepend( ':' ); + time.prepend( zeroPad( m % 60 ) ); //minutes + time.prepend( ':' ); + time.prepend( QString::number( h ) ); //hours + + return time; +} + +void +Slider::setValue( int newValue ) +{ + static QLabel *w1 = 0; + static QLabel *w2 = 0; + + if (!w1) { + w1 = new QLabel( this ); + w1->setPalette( QToolTip::palette() ); + w1->setFrameStyle( QFrame::Plain | QFrame::Box ); + + w2 = new QLabel( this ); + w2->setPalette( QToolTip::palette() ); + w2->setFrameStyle( QFrame::Plain | QFrame::Box ); + } + + //TODO stupidly inefficeint! :) + w1->setShown( mainWindow()->isFullScreen() ); + w2->setShown( mainWindow()->isFullScreen() ); + + + //don't adjust the slider while the user is dragging it! + + if( !m_sliding || m_outside ) { + const int l = engine()->length() / 1000; + const int left = int(l * (newValue / 65535.0)); + const int right = l - left; + + QSlider::setValue( newValue ); + w1->move( 0, height() - w1->height() - 1 ); + w1->setText( timeAsString( left ) + ' ' ); + w1->adjustSize(); + + w2->move( width() - w2->width(), height() - w1->height() - 1 ); + w2->setText( timeAsString( right ) + ' ' ); + w2->adjustSize(); + } + else + m_prevValue = newValue; +} diff --git a/src/app/slider.h b/src/app/slider.h new file mode 100644 index 0000000..7e06b6b --- /dev/null +++ b/src/app/slider.h @@ -0,0 +1,52 @@ +// (c) 2004 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINESLIDER_H +#define CODEINESLIDER_H + +#include <qslider.h> + +namespace Codeine +{ + class Slider : public QSlider + { + Q_OBJECT + + public: + static Slider *instance() { return s_instance; } + + public: + Slider( QWidget*, uint max = 0 ); + + virtual void setValue( int ); + + signals: + //we emit this when the user has specifically changed the slider + //so connect to it if valueChanged() is too generic + //Qt also emits valueChanged( int ) + void sliderReleased( uint ); + + protected: + virtual void wheelEvent( QWheelEvent* ); + virtual void mouseMoveEvent( QMouseEvent* ); + virtual void mouseReleaseEvent( QMouseEvent* ); + virtual void mousePressEvent( QMouseEvent* ); + virtual void keyPressEvent( QKeyEvent *e ) { e->ignore(); } //so that MainWindow gets the keypress + + virtual QSize sizeHint() const { return QSlider::sizeHint() + QSize( 0, 6 ); } + virtual QSize minimumSizeHint() const { return sizeHint(); } + + bool m_sliding; + + private: + static Slider *s_instance; + + bool m_outside; + int m_prevValue; + + Slider( const Slider& ); //undefined + Slider &operator=( const Slider& ); //undefined + }; +} + +#endif diff --git a/src/app/stateChange.cpp b/src/app/stateChange.cpp new file mode 100644 index 0000000..be15aeb --- /dev/null +++ b/src/app/stateChange.cpp @@ -0,0 +1,195 @@ +// Copyright 2004 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "actions.h" +#include "adjustSizeButton.h" +#include "debug.h" +#include "mainWindow.h" +#include <kconfig.h> +#include <kglobal.h> +#include "mxcl.library.h" +#include <qapplication.h> +#include <qevent.h> +#include <qlabel.h> +#include <qpopupmenu.h> +#include <qslider.h> +#include "theStream.h" +#include "videoSettings.h" //FIXME unfortunate +#include "xineEngine.h" + + +//TODO do in Sconstruct +#define QT_FATAL_ASSERT + + +//TODO make the XineEngine into xine::Stream and then make singleton and add functions like Stream::hasVideo() etc. +//TODO make convenience function to get fullscreen state + + +namespace Codeine { + + +void +MainWindow::engineStateChanged( Engine::State state ) +{ + Q_ASSERT( state != Engine::Uninitialised ); + + KURL const &url = TheStream::url(); + bool const isFullScreen = toggleAction("fullscreen")->isChecked(); + QWidget *const toolbar = reinterpret_cast<QWidget*>(toolBar()); + + Debug::Block block( state == Engine::Empty + ? "State: Empty" : state == Engine::Loaded + ? "State: Loaded" : state == Engine::Playing + ? "State: Playing" : state == Engine::Paused + ? "State: Paused" : state == Engine::TrackEnded + ? "State: TrackEnded" : "State: Unknown" ); + + + /// update actions + { + using namespace Engine; + + #define enableIf( name, criteria ) action( name )->setEnabled( state & criteria ); + enableIf( "stop", (Playing | Paused) ); + enableIf( "fullscreen", (Playing | Paused) ); + enableIf( "reset_zoom", ~Empty && !isFullScreen ); + enableIf( "information", ~Empty ); + enableIf( "video_settings", (Playing | Paused) ); + enableIf( "volume", (Playing | Paused) ); + #undef enableIf + + toggleAction( "play" )->setChecked( state == Playing ); + + //FIXME bad design to do this way + QSlider *volume = (QSlider*)toolBar()->child( "volume" ); + if (volume) + volume->setValue( engine()->volume() ); + } + + + /// update VideoSettingsDialog instance + VideoSettingsDialog::stateChanged( this, state ); + + + /// update menus + { + using namespace Engine; + + // the toolbar play button is always enabled, but the menu item + // is disabled if we are empty, this looks more sensible + QPopupMenu * const file_menu = menu( "file" ); + QPopupMenu * const settings_menu = menu( "settings" ); + const int play_id = file_menu->idAt( 2 ); + file_menu->setItemEnabled( play_id, state != Empty ); + + // menus are clearer when handled differently to toolbars + // KDE has a shit special action for this, but it stupidly changes + // the toolbar icon too. + // TODO do this from the playAction since we do it in context menu too + const KGuiItem item = (state == Playing) ? KGuiItem( i18n("&Pause"), "player_pause" ) : KGuiItem( i18n("&Play"), "player_play" ); + file_menu->changeItem( play_id, item.iconSet(), item.text() ); + file_menu->setItemChecked( play_id, false ); + + settings_menu->setItemEnabled( AspectRatioMenuItemId, state & (Playing | Paused) && TheStream::hasVideo() ); + + // set correct aspect ratio + if( state == Loaded ) + static_cast<QPopupMenu*>(child( "aspect_ratio_menu" ))->setItemChecked( TheStream::aspectRatio(), true ); + } + + + /// update statusBar + { + using namespace Engine; + m_analyzer->setShown( state & (Playing | Paused) && TheStream::hasAudio() ); + m_timeLabel->setShown( state & (Playing | Paused) ); + } + + + /// update position slider + switch( state ) + { + case Engine::Empty: + m_positionSlider->setEnabled( false ); + break; + case Engine::Loaded: + case Engine::TrackEnded: + m_positionSlider->setValue( 0 ); + // NO BREAK! + case Engine::Playing: + case Engine::Paused: + m_positionSlider->setEnabled( TheStream::canSeek() ); + break; + } + + + /// update recent files list if necessary + if( state == Engine::Loaded ) { + // update recently played list + + #ifndef NO_SKIP_PR0N + // ;-) + const QString url_string = url.url(); + if( !(url_string.contains( "porn", false ) || url_string.contains( "pr0n", false )) ) + #endif + if( url.protocol() != "dvd" && url.protocol() != "vcd" ) { + KConfig *config = Codeine::config( "General" ); + const QString prettyUrl = url.prettyURL(); + + QStringList urls = config->readPathListEntry( "Recent Urls" ); + urls.remove( prettyUrl ); + config->writePathEntry( "Recent Urls", urls << prettyUrl ); + } + + if( TheStream::hasVideo() && !isFullScreen ) + new AdjustSizeButton( reinterpret_cast<QWidget*>(videoWindow()) ); + } + + + /// set titles + switch( state ) + { + case Engine::Empty: + m_titleLabel->setText( i18n("No media loaded") ); + break; + case Engine::Paused: + m_titleLabel->setText( i18n("Paused") ); + break; + case Engine::Loaded: + case Engine::Playing: + case Engine::TrackEnded: + m_titleLabel->setText( TheStream::prettyTitle() ); + break; + } + + + /// set toolbar states + QWidget *dvd_button = (QWidget*)toolBar()->child( "toolbutton_toggle_dvd_menu" ); + if (dvd_button) + dvd_button->setShown( state != Engine::Empty && url.protocol() == "dvd" ); + + if( isFullScreen && !toolbar->hasMouse() ) { + switch( state ) { + case Engine::TrackEnded: + toolbar->show(); + + if( videoWindow()->isActiveWindow() ) { + //FIXME dual-screen this seems to still show + QContextMenuEvent e( QContextMenuEvent::Other, QPoint(), Qt::MetaButton ); + QApplication::sendEvent( videoWindow(), &e ); + } + break; + case Engine::Empty: + case Engine::Loaded: + case Engine::Paused: + toolBar()->show(); + break; + case Engine::Playing: + toolBar()->hide(); + break; + } + } +} + +} diff --git a/src/app/theStream.cpp b/src/app/theStream.cpp new file mode 100644 index 0000000..5d60d76 --- /dev/null +++ b/src/app/theStream.cpp @@ -0,0 +1,144 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include <kurl.h> +#include "mxcl.library.h" +#include "theStream.h" +#include <xine.h> +#include "xineEngine.h" + +namespace Codeine +{ + #define e VideoWindow::s_instance + + KConfig* + TheStream::profile() + { +//TODO a unique id for discs, and then even to also record chapters etc. +// if( url().protocol() == "dvd" ) +// return Codeine::config( QString( "dvd:/" ) + prettyTitle() ); +// else + return Codeine::config( url().prettyURL() ); + } + + const KURL& + TheStream::url() + { return e->m_url; } + + bool + TheStream::canSeek() + //FIXME! + { return e->m_url.protocol() != "http"; } + + bool + TheStream::hasAudio() + { return xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_HAS_AUDIO ); } + + bool + TheStream::hasVideo() + { return xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_HAS_VIDEO ); } + + QSize + TheStream::defaultVideoSize() + { + return !e->m_stream + ? QSize() + : QSize( + xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_VIDEO_WIDTH ), + xine_get_stream_info( e->m_stream, XINE_STREAM_INFO_VIDEO_HEIGHT ) ); + } + + int TheStream::aspectRatio() + { return xine_get_param( e->m_stream, XINE_PARAM_VO_ASPECT_RATIO ); } + + int TheStream::subtitleChannel() + { return xine_get_param( e->m_stream, XINE_PARAM_SPU_CHANNEL ); } + + int TheStream::audioChannel() + { return xine_get_param( e->m_stream, XINE_PARAM_AUDIO_CHANNEL_LOGICAL ); } + + QString + TheStream::prettyTitle() + { + const KURL &url = e->m_url; + const QString artist = QString::fromUtf8( xine_get_meta_info( e->m_stream, XINE_META_INFO_ARTIST ) ); + const QString title = QString::fromUtf8( xine_get_meta_info( e->m_stream, XINE_META_INFO_TITLE ) ); + + if (hasVideo() && !title.isEmpty()) + return title; + else if (!title.isEmpty() && !artist.isEmpty()) + return artist + " - " + title; + else if (url.protocol() != "http" && !url.fileName().isEmpty()) { + const QString n = url.fileName(); + return KURL::decode_string( n.left( n.findRev( '.' ) ).replace( '_', ' ' ) ); } + else + return url.prettyURL(); + } + + + static inline QString + entryHelper( const QString &plate, const QString &s1, const QString &s2 ) + { + return s2.isEmpty() ? s2 : plate.arg( s1 ).arg( s2 ); + } + + static inline QString + sectionHelper( const QString §ionTitle, const QStringList &entries ) + { + QString s; + + foreach( entries ) + if( !(*it).isEmpty() ) + s += *it; + + return s.isEmpty() ? s : "<h2>" + sectionTitle + "</h2>" + s; + } + + QString + TheStream::information() + { + #define meta( x ) xine_get_meta_info( e->m_stream, x ) + #define info( x, y ) x.arg( xine_get_stream_info( e->m_stream, y ) ) + #define simple( x ) QString::number( xine_get_stream_info( e->m_stream, x ) ) + + const QString plate = "<p><b>%1</b>: %2</p>"; + QString s; + + s += sectionHelper( i18n("Metadata"), + QStringList() + << entryHelper( plate, i18n("Title"), meta( XINE_META_INFO_TITLE ) ) + << entryHelper( plate, i18n("Comment"), meta( XINE_META_INFO_COMMENT ) ) + << entryHelper( plate, i18n("Artist"), meta( XINE_META_INFO_ARTIST ) ) + << entryHelper( plate, i18n("Genre"), meta( XINE_META_INFO_GENRE ) ) + << entryHelper( plate, i18n("Album"), meta( XINE_META_INFO_ALBUM ) ) + << entryHelper( plate, i18n("Year"), meta( XINE_META_INFO_YEAR ) ) ); + + s += sectionHelper( i18n("Audio Properties"), + QStringList() + << entryHelper( plate, i18n("Bitrate"), info( i18n("%1 bps"), XINE_STREAM_INFO_AUDIO_BITRATE ) ) + << entryHelper( plate, i18n("Sample-rate"), info( i18n("%1 Hz"), XINE_STREAM_INFO_AUDIO_SAMPLERATE ) ) ); + + s += sectionHelper( i18n("Technical Information"), + QStringList() + << entryHelper( plate, i18n("Video Codec"), meta( XINE_META_INFO_VIDEOCODEC ) ) + << entryHelper( plate, i18n("Audio Codec"), meta( XINE_META_INFO_AUDIOCODEC ) ) + << entryHelper( plate, i18n("System Layer"), meta( XINE_META_INFO_SYSTEMLAYER ) ) + << entryHelper( plate, i18n("Input Plugin"), meta( XINE_META_INFO_INPUT_PLUGIN )) + << entryHelper( plate, i18n("CDINDEX_DISCID"), meta( XINE_META_INFO_CDINDEX_DISCID ) ) ); + + QStringList texts; + texts << "BITRATE" << "SEEKABLE" << "VIDEO_WIDTH" << "VIDEO_HEIGHT" << "VIDEO_RATIO" << "VIDEO_CHANNELS" << "VIDEO_STREAMS" << "VIDEO_BITRATE" << "VIDEO_FOURCC" << "VIDEO_HANDLED" << "FRAME_DURATION" << "AUDIO_CHANNELS" << "AUDIO_BITS" << "-AUDIO_SAMPLERATE" << "-AUDIO_BITRATE" << "AUDIO_FOURCC" << "AUDIO_HANDLED" << "HAS_CHAPTERS" << "HAS_VIDEO" << "HAS_AUDIO" << "-IGNORE_VIDEO" << "-IGNORE_AUDIO" << "-IGNORE_SPU" << "VIDEO_HAS_STILL" << "MAX_AUDIO_CHANNEL" << "MAX_SPU_CHANNEL" << "AUDIO_MODE" << "SKIPPED_FRAMES" << "DISCARDED_FRAMES"; + + s += "<h2>Other</h2>"; + for( uint x = 0; x <= 28; ++x ) + s += entryHelper( plate, texts[x], simple( x ) ); + + #undef meta + #undef info + #undef simple + + return s; + } + + #undef e +} diff --git a/src/app/theStream.h b/src/app/theStream.h new file mode 100644 index 0000000..0ffe64f --- /dev/null +++ b/src/app/theStream.h @@ -0,0 +1,50 @@ +// (C) 2005 Max Howell ([email protected])
+// See COPYING file for licensing information
+
+#ifndef CODEINE_THESTREAM_H
+#define CODEINE_THESTREAM_H
+
+#include "config.h" // needed for inline functions
+#include <kurl.h> // larger :( but no macros at least
+#include <qsize.h> // small header
+#include <qstring.h> // small header
+
+/// for purely static classes
+#define CODEINE_NO_EXPORT( T ) \
+ T(); \
+ ~T(); \
+ T( const T& ); \
+ T &operator=( const T& ); \
+ bool operator==( const T& ); \
+ bool operator!=( const T& );
+
+namespace Codeine
+{
+ class TheStream
+ {
+ CODEINE_NO_EXPORT( TheStream )
+
+ public:
+ static const KURL &url();
+
+ static bool canSeek();
+ static bool hasAudio();
+ static bool hasVideo();
+
+ static QSize defaultVideoSize();
+
+ static int aspectRatio();
+ static int subtitleChannel();
+ static int audioChannel();
+
+ static QString prettyTitle();
+ static QString information();
+
+ static inline bool hasProfile()
+ { return KGlobal::config()->hasGroup( url().prettyURL() ); }
+
+ static KConfig *profile();
+ };
+}
+
+#endif
diff --git a/src/app/videoSettings.cpp b/src/app/videoSettings.cpp new file mode 100644 index 0000000..945e4d3 --- /dev/null +++ b/src/app/videoSettings.cpp @@ -0,0 +1,135 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include <kwin.h> +#include "mxcl.library.h" +#include <qlabel.h> +#include <qlayout.h> +#include <qslider.h> +#include "videoSettings.h" +#include <xine.h> +#include "xineEngine.h" + +extern "C" +{ + // #include <X11/Xlib.h> is just dangerous! Here, there is a macro for Below that conflicts + // with QSlider::Below. Stupid X11 people. + typedef unsigned long XID; + typedef XID Window; + extern int XSetTransientForHint( Display*, Window, Window ); +} + + +//TODO update from engine when new video is played +//TODO show a warning that when paused the changes aren't updated to the display, show an unpause button too + + +class SnapSlider : public QSlider +{ + int m_offset; + +public: + SnapSlider( const int value, QWidget *parent, const char *name ) + : QSlider( (65536/4)-1, (3*(65536/4))-1, 1000, value, Qt::Horizontal, parent, name ) + , m_offset( 0 ) + { + setTickmarks( QSlider::Below ); + setTickInterval( 65536 / 4 ); + setMinimumWidth( fontMetrics().width( name ) * 3 ); + connect( this, SIGNAL(valueChanged( int )), Codeine::engine(), SLOT(setStreamParameter( int )) ); + } + + virtual void mousePressEvent( QMouseEvent *e ) + { + m_offset = e->pos().x() - (sliderStart() + (sliderRect().width()/2)); + QSlider::mousePressEvent( e ); + } + + virtual void mouseMoveEvent( QMouseEvent *e ) + { + const int MIDDLE = width() / 2; + const int x = e->pos().x() - m_offset; + const int F = sliderRect().width() / 2; + + if( x > MIDDLE - F && x < MIDDLE + F ) { + QMouseEvent e2( e->type(), QPoint( MIDDLE + m_offset, e->pos().y() ), e->button(), e->state() ); + QSlider::mouseMoveEvent( &e2 ); + QRangeControl::setValue( 65536 / 2 - 1 ); // to ensure we are absolutely exact + } + else + QSlider::mouseMoveEvent( e ); + } +}; + + +Codeine::VideoSettingsDialog::VideoSettingsDialog( QWidget *parent ) + : KDialog( parent, "video_settings_dialog", false, WType_TopLevel | WDestructiveClose ) +{ + XSetTransientForHint( x11Display(), winId(), parent->winId() ); + KWin::setType( winId(), NET::Utility ); + KWin::setState( winId(), NET::SkipTaskbar ); + + QFrame *frame = new QFrame( this ); + (new QVBoxLayout( this, 10 ))->addWidget( frame ); + frame->setFrameStyle( QFrame::StyledPanel | QFrame::Sunken ); + frame->setPaletteBackgroundColor( backgroundColor().dark( 102 ) ); + + QGridLayout *grid = new QGridLayout( frame, 4, 2, 15, 10 ); + grid->setAutoAdd( true ); + + #define makeSlider( PARAM, name ) \ + new QLabel( name, frame ); \ + new SnapSlider( xine_get_param( *Codeine::engine(), PARAM ), frame, name ); + + makeSlider( XINE_PARAM_VO_BRIGHTNESS, "brightness" ); + makeSlider( XINE_PARAM_VO_CONTRAST, "contrast" ); + makeSlider( XINE_PARAM_VO_SATURATION, "saturation" ); + makeSlider( XINE_PARAM_VO_HUE, "hue" ); + + #undef makeSlider + + setCaption( i18n("Video Settings") ); + setMaximumSize( sizeHint().width() * 5, sizeHint().height() ); + + KDialog::show(); +} + +void +Codeine::VideoSettingsDialog::stateChanged( QWidget *parent, Engine::State state ) //static +{ + QWidget *me = (QWidget*)parent->child( "video_settings_dialog" ); + + if( !me ) + return; + + switch( state ) + { + case Engine::Playing: + case Engine::Paused: + me->setEnabled( true ); + break; + + case Engine::Loaded: + #define update( param, name ) static_cast<QSlider*>(me->child( name ))->setValue( xine_get_param( *Codeine::engine(), param ) ); + update( XINE_PARAM_VO_BRIGHTNESS, "brightness" ); + update( XINE_PARAM_VO_CONTRAST, "contrast" ); + update( XINE_PARAM_VO_SATURATION, "saturation" ); + update( XINE_PARAM_VO_HUE, "hue" ); + #undef update + + default: + me->setEnabled( false ); + break; + } +} + +namespace Codeine +{ + void showVideoSettingsDialog( QWidget *parent ) + { + // ensure that the dialog is shown by deleting the old one + delete parent->child( "video_settings_dialog" ); + + new VideoSettingsDialog( parent ); + } +} diff --git a/src/app/videoSettings.h b/src/app/videoSettings.h new file mode 100644 index 0000000..20e01ff --- /dev/null +++ b/src/app/videoSettings.h @@ -0,0 +1,26 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_VIDEO_SETTINGS_H +#define CODEINE_VIDEO_SETTINGS_H + +#include "codeine.h" +#include <kdialog.h> + + +namespace Codeine +{ + class VideoSettingsDialog : public KDialog + { + VideoSettingsDialog(); //disable + VideoSettingsDialog( const VideoSettingsDialog& ); //disable + VideoSettingsDialog &operator=( const VideoSettingsDialog& ); //disable + + public: + VideoSettingsDialog( QWidget *parent ); + + static void stateChanged( QWidget *parent, Engine::State ); + }; +} + +#endif diff --git a/src/app/videoWindow.cpp b/src/app/videoWindow.cpp new file mode 100644 index 0000000..00f2542 --- /dev/null +++ b/src/app/videoWindow.cpp @@ -0,0 +1,380 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "VideoWindow" + +#include "actions.h" +#include <cmath> //std::log10 +#include <cstdlib> +#include "debug.h" +#include <kapplication.h> //::makeStandardCaption +#include <kconfig.h> +#include <kiconloader.h> +#include <kpopupmenu.h> +#include <kwin.h> +#include "mxcl.library.h" +#include <qcursor.h> +#include <qevent.h> +#include "slider.h" +#include "theStream.h" +#include <X11/Xlib.h> +#include <xine.h> +#include "xineEngine.h" + + +namespace Codeine +{ + namespace X + { + // we get thread locks if we don't cache these values + // (I don't know which ones exactly) + Display *d; + int s, w; + } + + +void +VideoWindow::initVideo() +{ + X::d = XOpenDisplay( std::getenv("DISPLAY") ); + X::s = DefaultScreen( X::d ); + X::w = winId(); + + XLockDisplay( X::d ); + XSelectInput( X::d, X::w, ExposureMask ); + + { + using X::d; using X::s; + + //these are Xlib macros + double w = DisplayWidth( d, s ) * 1000 / DisplayWidthMM( d, s ); + double h = DisplayHeight( d, s ) * 1000 / DisplayHeightMM( d, s ); + + m_displayRatio = w / h; + } + + connect( &m_timer, SIGNAL(timeout()), SLOT(hideCursor()) ); + + XUnlockDisplay( X::d ); +} + +void +VideoWindow::cleanUpVideo() +{ + XCloseDisplay( X::d ); +} + +void* +VideoWindow::x11Visual() const +{ + DEBUG_FUNC_INFO + + x11_visual_t* visual = new x11_visual_t; + + visual->display = X::d; + visual->screen = X::s; + visual->d = winId();//X::w; + visual->dest_size_cb = &VideoWindow::destSizeCallBack; + visual->frame_output_cb = &VideoWindow::frameOutputCallBack; + visual->user_data = (void*)this; + + return visual; +} + +void +VideoWindow::destSizeCallBack( + void* p, int /*video_width*/, int /*video_height*/, + double /*video_aspect*/, int* dest_width, + int* dest_height, double* dest_aspect ) +{ + if( !p ) + return; + + #define vw static_cast<VideoWindow*>(p) + + *dest_width = vw->width(); + *dest_height = vw->height(); + *dest_aspect = vw->m_displayRatio; +} + +void +VideoWindow::frameOutputCallBack( + void* p, int video_width, int video_height, double video_aspect, + int* dest_x, int* dest_y, int* dest_width, int* dest_height, + double* dest_aspect, int* win_x, int* win_y ) +{ + if( !p ) + return; + + *dest_x = 0; + *dest_y = 0 ; + *dest_width = vw->width(); + *dest_height = vw->height(); + *win_x = vw->x(); + *win_y = vw->y(); + *dest_aspect = vw->m_displayRatio; + + // correct size with video aspect + // TODO what's this about? + if( video_aspect >= vw->m_displayRatio ) + video_width = (int) ( (double) (video_width * video_aspect / vw->m_displayRatio + 0.5) ); + else + video_height = (int) ( (double) (video_height * vw->m_displayRatio / video_aspect) + 0.5); + + #undef vw +} + +void +VideoWindow::contextMenuEvent( QContextMenuEvent *e ) +{ + e->accept(); + + KPopupMenu popup; + + if( state() == Engine::Playing ) + popup.insertItem( SmallIconSet("player_pause"), i18n("Pause"), 1 ); + else + action( "play" )->plug( &popup ); + + popup.insertSeparator(); + + if( TheStream::url().protocol() == "dvd" ) + action( "toggle_dvd_menu" )->plug( &popup ), + popup.insertSeparator(); + if( !((KToggleAction*)actionCollection()->action( "fullscreen" ))->isChecked() ) + action( "reset_zoom" )->plug( &popup ); + action( "capture_frame" )->plug( &popup ); + popup.insertSeparator(); + action( "video_settings" )->plug( &popup ); + popup.insertSeparator(); + action( "fullscreen" )->plug( &popup ); + //show zoom information? + + if( e->state() & Qt::MetaButton ) { //only on track end, or for special users + popup.insertSeparator(); + action( "file_quit" )->plug( &popup ); + } + + if( popup.exec( e->globalPos() ) == 1 && state() == Engine::Playing ) + // we check we are still paused as the menu generates a modal event loop + // so anything might have happened in the meantime. + pause(); +} + +bool +VideoWindow::event( QEvent *e ) +{ + //TODO it would perhaps make things more responsive to + // deactivate mouse tracking and use the x11Event() function to transfer mouse move events? + // perhaps even better would be a x11 implementation + + switch( e->type() ) + { + case QEvent::DragEnter: + case QEvent::Drop: + //FIXME why don't we just ignore the event? It should propogate down + return QApplication::sendEvent( qApp->mainWidget(), e ); + + case QEvent::Resize: + if( !TheStream::url().isEmpty() ) { + const QSize defaultSize = TheStream::defaultVideoSize(); + const bool notDefaultSize = width() != defaultSize.width() && height() != defaultSize.height(); + + Codeine::action( "reset_zoom" )->setEnabled( notDefaultSize ); + + //showOSD( i18n("Scale: %1%").arg( size() + } + break; + + case QEvent::Leave: + m_timer.stop(); + break; + + // Xlib.h sucks fucking balls!!!!11!!1! + #undef FocusOut + case QEvent::FocusOut: + // if the user summons some dialog via a shortcut or whatever we need to ensure + // the mouse gets shown, because if it is modal, we won't get mouse events after + // it is shown! This works because we are always the focus widget. + // @see MainWindow::MainWindow where we setFocusProxy() + case QEvent::Enter: + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + unsetCursor(); + if( hasFocus() ) + // see above comment + m_timer.start( CURSOR_HIDE_TIMEOUT, true ); + break; + + case QEvent::MouseButtonDblClick: + Codeine::action( "fullscreen" )->activate(); + break; + + default: ; + } + + if( !m_xine ) + return QWidget::event( e ); + + switch( e->type() ) + { + case QEvent::Close: + stop(); + return false; + + case VideoWindow::ExposeEvent: + //see VideoWindow::x11Event() + + return true; + + // Xlib.h sucks fucking balls!!!!11!!1! + #undef KeyPress + case QEvent::KeyPress: { + if( m_url.protocol() != "dvd" ) + // let MainWindow handle this + return QWidget::event( e ); + + //FIXME left and right keys don't work during DVDs + + int keyCode = XINE_EVENT_INPUT_UP; + + //#define XINE_EVENT_INPUT_UP 110 + //#define XINE_EVENT_INPUT_DOWN 111 + //#define XINE_EVENT_INPUT_LEFT 112 + //#define XINE_EVENT_INPUT_RIGHT 113 + //#define XINE_EVENT_INPUT_SELECT 114 + + switch( static_cast<QKeyEvent*>(e)->key() ) { + case Key_Return: + case Key_Enter: keyCode++; + case Key_Right: keyCode++; + case Key_Left: keyCode++; + case Key_Down: keyCode++; + case Key_Up: + { + //this whole shebang is cheeky as xine doesn't + //guarentee the codes will stay the same + + xine_event_t xineEvent; + + xineEvent.type = keyCode; + xineEvent.data = NULL; + xineEvent.data_length = 0; + + xine_event_send( m_stream, &xineEvent ); + + return true; + } + default: + return false; + } + } + + case QEvent::MouseButtonPress: + + #define mouseEvent static_cast<QMouseEvent*>(e) + + if( mouseEvent->button() != Qt::LeftButton ) + return false; + + mouseEvent->accept(); + + //FALL THROUGH + + case QEvent::MouseMove: + { + x11_rectangle_t x11Rect; + xine_event_t xineEvent; + xine_input_data_t xineInput; + + x11Rect.x = mouseEvent->x(); + x11Rect.y = mouseEvent->y(); + x11Rect.w = 0; + x11Rect.h = 0; + + xine_gui_send_vo_data( m_stream, XINE_GUI_SEND_TRANSLATE_GUI_TO_VIDEO, (void*)&x11Rect ); + + xineEvent.type = e->type() == QEvent::MouseMove ? XINE_EVENT_INPUT_MOUSE_MOVE : XINE_EVENT_INPUT_MOUSE_BUTTON; + xineEvent.data = &xineInput; + xineEvent.data_length = sizeof( xine_input_data_t ); + xineInput.button = 1; //HACK e->type() == QEvent::MouseMove ? 0 : 1; + xineInput.x = x11Rect.x; + xineInput.y = x11Rect.y; + xine_event_send( m_stream, &xineEvent ); + + return e->type() == QEvent::MouseMove ? false : true; + + #undef mouseEvent + } + + case QEvent::Wheel: + { + //TODO seek amount should depend on the length, basically seek at most say 30s, and at least 0.5s + //TODO this is replicated (somewhat) in MainWindow::keyPressEvent + + int pos, time, length; + xine_get_pos_length( m_stream, &pos, &time, &length ); + pos += int(std::log10( (double)length ) * static_cast<QWheelEvent*>(e)->delta()); + + seek( pos > 0 ? (uint)pos : 0 ); + + return true; + } + + default: ; + } + + return QWidget::event( e ); +} + +bool +VideoWindow::x11Event( XEvent *e ) +{ + if( m_stream && e->type == Expose && e->xexpose.count == 0 ) { + xine_gui_send_vo_data( + m_stream, + XINE_GUI_SEND_EXPOSE_EVENT, + e ); + + return true; + } + + return false; +} + +void +VideoWindow::hideCursor() +{ + setCursor( Qt::BlankCursor ); +} + +QSize +VideoWindow::sizeHint() const //virtual +{ + QSize s = TheStream::profile()->readSizeEntry( "Preferred Size" ); + + if( !s.isValid() ) + s = TheStream::defaultVideoSize(); + + if( s.isValid() && !s.isNull() ) + return s; + + return minimumSizeHint(); +} + +QSize +VideoWindow::minimumSizeHint() const //virtual +{ + const int x = fontMetrics().width( "x" ) * 4; + + return QSize( x * 12, x * 4 ); //FIXME +} + +void +VideoWindow::resetZoom() +{ + TheStream::profile()->deleteEntry( "Preferred Size" ); + topLevelWidget()->adjustSize(); +} + +} //namespace Codeine diff --git a/src/app/volumeAction.cpp b/src/app/volumeAction.cpp new file mode 100644 index 0000000..4215640 --- /dev/null +++ b/src/app/volumeAction.cpp @@ -0,0 +1,114 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include <klocale.h> +#include <ktoolbar.h> +#include <qevent.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qslider.h> + +#include "debug.h" +#include "volumeAction.h" +#include "volumeAction.moc" +#include "xineEngine.h" + + +class VolumeSlider : public QFrame +{ +public: + VolumeSlider( QWidget *parent ) + : QFrame( parent ) + { + slider = new QSlider( Qt::Vertical, this, "volume" ); + label = new QLabel( this ); + + QBoxLayout *lay = new QVBoxLayout( this ); + lay->addWidget( slider, 0, Qt::AlignHCenter ); + lay->addWidget( label, 0, Qt::AlignHCenter ); + lay->setMargin( 4 ); + + slider->setRange( 0, 100 ); + + setFrameStyle( QFrame::Plain | QFrame::Box ); + setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); + + hide(); + } + + QLabel *label; + QSlider *slider; +}; + + +VolumeAction::VolumeAction( KToolBar *bar, KActionCollection *ac ) + : KToggleAction( i18n("Volume"), "volume", Qt::Key_1, 0, 0, ac, "volume" ) + , m_anchor( 0 ) +{ + m_widget = new VolumeSlider( bar->topLevelWidget() ); + + connect( this, SIGNAL(toggled( bool )), SLOT(toggled( bool )) ); + connect( m_widget->slider, SIGNAL(sliderMoved( int )), SLOT(sliderMoved( int )) ); + connect( m_widget->slider, SIGNAL(sliderMoved( int )), Codeine::engine(), SLOT(setStreamParameter( int )) ); + connect( m_widget->slider, SIGNAL(sliderReleased()), SLOT(sliderReleased()) ); +} + +int +VolumeAction::plug( QWidget *bar, int index ) +{ + DEBUG_BLOCK + + int const id = KAction::plug( bar, index ); + + m_anchor = (QWidget*)bar->child( "toolbutton_volume" ); //KAction creates it with this name + m_anchor->installEventFilter( this ); //so we can keep m_widget anchored + + return id; +} + +void +VolumeAction::toggled( bool const b ) +{ + DEBUG_BLOCK + + m_widget->raise(); + m_widget->setShown( b ); +} + +void +VolumeAction::sliderMoved( int v ) +{ + v = 100 - v; //Qt sliders are wrong way round when vertical + + QString const t = QString::number( v ) + '%'; + + setToolTip( i18n( "Volume: %1" ).arg( t ) ); + m_widget->label->setText( t ); +} + +bool +VolumeAction::eventFilter( QObject *o, QEvent *e ) +{ + switch (e->type()) { + case QEvent::Move: + case QEvent::Resize: { + QWidget const * const &a = m_anchor; + + m_widget->move( a->mapTo( m_widget->parentWidget(), QPoint( 0, a->height() ) ) ); + m_widget->resize( a->width(), m_widget->sizeHint().height() ); + return false; + } + + //TODO one click method, flawed currently in fullscreen mode by palette change in mainwindow.cpp +/* case QEvent::MouseButtonPress: + m_widget->show(); + break; + + case QEvent::MouseButtonRelease: + m_widget->hide(); + break;*/ + + default: + return false; + } +} diff --git a/src/app/volumeAction.h b/src/app/volumeAction.h new file mode 100644 index 0000000..6c0c376 --- /dev/null +++ b/src/app/volumeAction.h @@ -0,0 +1,29 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_VOLUME_ACTION_H +#define CODEINE_VOLUME_ACTION_H + +#include <kactionclasses.h> + +class VolumeAction : public KToggleAction +{ + Q_OBJECT + + QWidget *m_anchor; + class VolumeSlider *m_widget; + + virtual bool eventFilter( QObject *o, QEvent *e ); + + virtual int plug( QWidget*, int ); + +private slots: + void toggled( bool ); + void sliderMoved( int ); + void sliderReleased() { setChecked( false ); toggled( false ); } + +public: + VolumeAction( KToolBar *anchor, KActionCollection *ac ); +}; + +#endif diff --git a/src/app/xineConfig.cpp b/src/app/xineConfig.cpp new file mode 100644 index 0000000..70ca11a --- /dev/null +++ b/src/app/xineConfig.cpp @@ -0,0 +1,321 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "debug.h" +#include <kapplication.h> // XineConfigDialog::ctor -> to get the iconloader +#include <kcombobox.h> +#include <kiconloader.h> // XineConfigDialog::ctor +#include <klineedit.h> +#include <kseparator.h> +#include <kstdguiitem.h> +#include <qcheckbox.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qscrollview.h> +#include <qspinbox.h> +#include <qtabwidget.h> +#include <qtooltip.h> +#include <qvbox.h> +#include <xine.h> +#include "xineConfig.h" + +QString i18n(const char *text); + + +KDialogBase *XineConfigDialog::s_instance = 0; + + +namespace Codeine +{ + void + showXineConfigurationDialog( QWidget *parent, xine_t *xine ) + { + XineConfigDialog d( xine, parent ); + if( d.exec() == QDialog::Accepted ) + d.saveSettings(); + } +} + + +class TabWidget : public QTabWidget +{ +public: + TabWidget( QWidget *parent ) : QTabWidget( parent ) {} + + virtual QSize sizeHint() const + { + // Qt gives a stupid default sizeHint for this widget + return QSize( + reinterpret_cast<QWidget*>(tabBar())->sizeHint().width() + 5, + QTabWidget::sizeHint().height() ); + } +}; + + +///@class XineConfigDialog + +XineConfigDialog::XineConfigDialog( xine_t *xine, QWidget *parent ) + : KDialogBase( parent, "xine_config_dialog", + true, //modal + i18n("Configure xine"), User1 | Stretch | Ok | Cancel, + Ok, //default button + false, //draw separator + KStdGuiItem::reset() ) + , m_xine( xine ) +{ + DEBUG_BLOCK + + s_instance = this; + const int METRIC = fontMetrics().width( 'x' ); + const int METRIC_3B2 = (3*METRIC)/2; + + QVBox *box = new QVBox( this ); + box->setSpacing( METRIC ); + setMainWidget( box ); + + { + QHBox *hbox = new QHBox( box ); + hbox->setSpacing( METRIC_3B2 ); + hbox->setMargin( METRIC_3B2 ); + QPixmap info = kapp->iconLoader()->loadIcon( "messagebox_info", KIcon::NoGroup, KIcon::SizeMedium, KIcon::DefaultState, 0, true ); + QLabel *label = new QLabel( hbox ); + label->setPixmap( info ); + label->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Maximum ); + label = new QLabel( i18n( + "xine's defaults are usually sensible and should not require modification. " + "However, full configurability is provided for your pleasure ;-)." ), hbox ); + label->setAlignment( QLabel::WordBreak | QLabel::AlignVCenter ); + } + + //FIXME after many hours I have discovered that this + // widget somehow sets the minSize of this widget to 0,0 + // whenever you resize the widget. WTF? + TabWidget *tabs = new TabWidget( box ); + + + class XineConfigEntryIterator { + xine_t *m_xine; + xine_cfg_entry_t m_entry; + bool m_valid; + public: + XineConfigEntryIterator( xine_t *xine ) : m_xine( xine ) { m_valid = xine_config_get_first_entry( m_xine, &m_entry ); } + inline XineConfigEntryIterator &operator++() { m_valid = xine_config_get_next_entry( m_xine, &m_entry ); return *this; } + inline xine_cfg_entry_t *operator*() { return m_valid ? &m_entry : 0; } + }; + + + QGridLayout *grid = 0; + QString currentPage; + QScrollView *view = 0; + parent = 0; + + for( XineConfigEntryIterator it( m_xine ); *it; ++it ) + { + const QString pageName = QString::fromUtf8( (*it)->key ).section( '.', 0, 0 ); + + if( (QStringList() << "ui" << "effects" << "subtitles").contains( pageName ) ) + continue; + + if( pageName != currentPage ) { + if( view ) + //NOTE won't be executed for last tab + view->viewport()->setMinimumWidth( grid->sizeHint().width() ); // seems necessary + + QString pageTitle = pageName; + pageTitle[0] = pageTitle[0].upper(); + + tabs->addTab( view = new QScrollView, pageTitle ); + view->setResizePolicy( QScrollView::AutoOneFit ); + view->setHScrollBarMode( QScrollView::AlwaysOff ); + view->setFrameShape( QFrame::NoFrame ); + view->addChild( parent = new QWidget( view->viewport() ) ); + + QBoxLayout *layout = new QVBoxLayout( parent, /*margin*/METRIC_3B2, /*spacing*/0 ); + + parent = new QFrame( parent ); + static_cast<QFrame*>(parent)->setFrameStyle( QFrame::Panel | QFrame::Raised ); + static_cast<QFrame*>(parent)->setLineWidth( 2 ); + grid = new QGridLayout( parent, /*rows*/0, /*cols*/2, /*margin*/20, /*spacing*/int(METRIC*2.5) ); + grid->setColStretch( 0, 3 ); + grid->setColStretch( 1, 2 ); + + layout->addWidget( parent, 0 ); + layout->addStretch( 1 ); + + currentPage = pageName; + } + + m_entrys.append( new XineConfigEntry( parent, grid, *it ) ); + } + + //finishing touches + m_entrys.setAutoDelete( true ); + enableButton( Ok, false ); + enableButton( User1, false ); + + Q_ASSERT( !isUnsavedSettings() ); +} + +void +XineConfigDialog::slotHelp() +{ + /// HACK called when a widget's input value changes + + const bool b = isUnsavedSettings(); + enableButton( Ok, b ); + enableButton( User1, b ); +} + +void +XineConfigDialog::slotUser1() +{ + for( QPtrListIterator<XineConfigEntry> it( m_entrys ); *it != 0; ++it ) + (*it)->reset(); + + slotHelp(); +} + +bool +XineConfigDialog::isUnsavedSettings() const +{ + for( QPtrListIterator<XineConfigEntry> it( m_entrys ); *it != 0; ++it ) + if( (*it)->isChanged() ) + return true; + + return false; +} + +#include <qdir.h> +void +XineConfigDialog::saveSettings() +{ + for( XineConfigEntry *entry = m_entrys.first(); entry; entry = m_entrys.next() ) + if( entry->isChanged() ) + entry->save( m_xine ); + + xine_config_save( m_xine, QFile::encodeName( QDir::homeDirPath() + "/.xine/config" ) ); +} + + +///@class XineConfigEntry + +XineConfigEntry::XineConfigEntry( QWidget *parent, QGridLayout *grid, xine_cfg_entry_t *entry ) + : m_widget( 0 ) + , m_key( entry->key ) + , m_string( entry->str_value ) + , m_number( entry->num_value ) +{ + QWidget *&w = m_widget; + const char *signal = 0; + const int row = grid->numRows(); + + QString description_text = QString::fromUtf8( entry->description ); + description_text[0] = description_text[0].upper(); + + switch( entry->type ) + { + case XINE_CONFIG_TYPE_STRING: { + w = new KLineEdit( m_string, parent ); + signal = SIGNAL(textChanged( const QString& )); + break; + } + case XINE_CONFIG_TYPE_ENUM: { + w = new KComboBox( parent ); + for( int i = 0; entry->enum_values[i]; ++i ) + ((KComboBox*)w)->insertItem( QString::fromUtf8( entry->enum_values[i] ) ); + ((KComboBox*)w)->setCurrentItem( m_number ); + signal = SIGNAL(activated( int )); + break; + } + case XINE_CONFIG_TYPE_RANGE: + case XINE_CONFIG_TYPE_NUM: { + w = new QSpinBox( + QMIN( m_number, entry->range_min ), // xine bug, sometimes the min and max ranges + QMAX( m_number, entry->range_max ), // are both 0 even though this is bullshit + 1, parent ); + ((QSpinBox*)w)->setValue( m_number ); + signal = SIGNAL(valueChanged( int )); + break; + } + case XINE_CONFIG_TYPE_BOOL: { + w = new QCheckBox( description_text, parent ); + ((QCheckBox*)w)->setChecked( m_number ); + + connect( w, SIGNAL(toggled( bool )), XineConfigDialog::instance(), SLOT(slotHelp()) ); + QToolTip::add( w, "<qt>" + QString::fromUtf8( entry->help ) ); + grid->addMultiCellWidget( w, row, row, 0, 1 ); + return; //no need for a description label + } + default: + ; + } + + connect( w, signal, XineConfigDialog::instance(), SLOT(slotHelp()) ); + + QLabel *description = new QLabel( description_text + ':', parent ); + description->setAlignment( QLabel::WordBreak | QLabel::AlignVCenter ); + + const QString tip = "<qt>" + QString::fromUtf8( entry->help ); + QToolTip::add( w, tip ); + QToolTip::add( description, tip ); + +// grid->addWidget( description, row, 0, Qt::AlignVCenter ); + grid->addWidget( w, row, 1, Qt::AlignTop ); +} + +bool +XineConfigEntry::isChanged() const +{ + #define _( x ) static_cast<x*>(m_widget) + + switch( classType( m_widget->className() ) ) { + case LineEdit: return _(KLineEdit)->text().utf8() != m_string; + case ComboBox: return _(KComboBox)->currentItem() != m_number; + case SpinBox: return _(QSpinBox)->value() != m_number; + case CheckBox: return _(QCheckBox)->isChecked() != m_number; + } + return false; +} + +void +XineConfigEntry::reset() +{ + // this is because we only get called by the XineConfigDialog reset button + // and we don't want to cause a check for Ok/Reset button enabled state for + // every XineConfigEntry + m_widget->blockSignals( true ); + + switch( classType( m_widget->className() ) ) { + case LineEdit: _(KLineEdit)->setText( m_string ); break; + case ComboBox: _(KComboBox)->setCurrentItem( m_number ); break; + case SpinBox: _(QSpinBox)->setValue( m_number ); break; + case CheckBox: _(QCheckBox)->setChecked( (bool)m_number ); break; + } + m_widget->blockSignals( false ); +} + +void +XineConfigEntry::save( xine_t *xine ) +{ + xine_cfg_entry_t ent; + + if( xine_config_lookup_entry( xine, key(), &ent ) ) + { + switch( classType( m_widget->className() ) ) { + case LineEdit: m_string = _(KLineEdit)->text().utf8(); break; + case ComboBox: m_number = _(KComboBox)->currentItem(); break; + case SpinBox: m_number = _(QSpinBox)->value(); break; + case CheckBox: m_number = _(QCheckBox)->isChecked(); break; + } + + ent.str_value = qstrdup( m_string ); + ent.num_value = m_number; + + debug() << "Saving setting: " << key() << endl; + xine_config_update_entry( xine, &ent ); + } + else + Debug::warning() << "Couldn't save: " << key() << endl; + + #undef _ +} diff --git a/src/app/xineConfig.h b/src/app/xineConfig.h new file mode 100644 index 0000000..d7999d5 --- /dev/null +++ b/src/app/xineConfig.h @@ -0,0 +1,69 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef XINECONFIG_H +#define XINECONFIG_H + +#include <kdialogbase.h> +#include <qptrlist.h> + +class KComboBox; +class KLineEdit; +class QCheckBox; +class QGridLayout; +class QSpinBox; + +typedef struct xine_s xine_t; +typedef struct xine_cfg_entry_s xine_cfg_entry_t; + + +///stores a single config entry of the config file + +class XineConfigEntry : public QObject +{ + enum ClassType { LineEdit, ComboBox, SpinBox, CheckBox }; + + QWidget *m_widget; + QCString m_key; + QCString m_string; + int m_number; + + static inline ClassType classType( const QCString &name ) + { + return name == "KLineEdit" ? LineEdit + : name == "KComboBox" ? ComboBox + : name == "QSpinBox" ? SpinBox : CheckBox; + } + +public: + XineConfigEntry( QWidget *parent, QGridLayout*, xine_cfg_entry_t* ); + + bool isChanged() const; + void save( xine_t* ); + void reset(); + + inline const QCString &key() const { return m_key; } +}; + + +class XineConfigDialog : public KDialogBase +{ + static KDialogBase *s_instance; + + QPtrList<XineConfigEntry> m_entrys; + xine_t *m_xine; + +public: + XineConfigDialog( xine_t *xine, QWidget *parent ); + + bool isUnsavedSettings() const; + void saveSettings(); + + static KDialogBase *instance() { return s_instance; } + +protected: + virtual void slotUser1(); + virtual void slotHelp(); +}; + +#endif diff --git a/src/app/xineEngine.cpp b/src/app/xineEngine.cpp new file mode 100644 index 0000000..58069c5 --- /dev/null +++ b/src/app/xineEngine.cpp @@ -0,0 +1,876 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "engine" + +#include "actions.h" //::seek() FIXME unfortunate +#include <cmath> //the fade out +#include "config.h" +#include "debug.h" +#include <limits> +#include <klocale.h> +#include "mxcl.library.h" +#include <qapplication.h> //::sendEvent() +#include <qdatetime.h> //record() +#include <qdir.h> //::exists() +#include "slider.h" +#include "theStream.h" +#include <xine.h> +#include "xineEngine.h" +#include "xineScope.h" + + +#define XINE_SAFE_MODE 1 + +extern "C" { void _debug( const char *string ) { debug() << string; } } //FIXME + + +namespace Codeine { + + +VideoWindow *VideoWindow::s_instance = 0; + + +VideoWindow::VideoWindow( QWidget *parent ) + : QWidget( parent, "VideoWindow" ) + , m_osd( 0 ) + , m_stream( 0 ) + , m_eventQueue( 0 ) + , m_videoPort( 0 ) + , m_audioPort( 0 ) + , m_scope( 0 ) + , m_xine( 0 ) + , m_current_vpts( 0 ) +{ + DEBUG_BLOCK + + s_instance = this; + + setWFlags( Qt::WNoAutoErase ); + setMouseTracking( true ); + setAcceptDrops( true ); + setUpdatesEnabled( false ); //to stop Qt drawing over us + setPaletteBackgroundColor( Qt::black ); + setFocusPolicy( ClickFocus ); + + //TODO sucks + //TODO namespace this? + myList->next = myList; //init the buffer list +} + +VideoWindow::~VideoWindow() +{ + DEBUG_BLOCK + + eject(); + + // fade out volume on exit + if( m_stream && xine_get_status( m_stream ) == XINE_STATUS_PLAY ) { + int cum = 0; + for( int v = 99; v >= 0; v-- ) { + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL, v ); + int sleep = int(32000 * (-std::log10( double(v + 1) ) + 2)); + + ::usleep( sleep ); + + cum += sleep; + } + + debug() << "Total sleep: " << cum << "x10^-6 s\n"; + + xine_stop( m_stream ); + + ::sleep( 1 ); + } + + //xine_set_param( m_stream, XINE_PARAM_IGNORE_VIDEO, 1 ); + + if( m_osd ) xine_osd_free( m_osd ); + if( m_stream ) xine_close( m_stream ); + if( m_eventQueue ) xine_event_dispose_queue( m_eventQueue ); + if( m_stream ) xine_dispose( m_stream ); + if( m_audioPort ) xine_close_audio_driver( m_xine, m_audioPort ); + if( m_videoPort ) xine_close_video_driver( m_xine, m_videoPort ); + if( m_scope ) xine_post_dispose( m_xine, m_scope ); + if( m_xine ) xine_exit( m_xine ); + + cleanUpVideo(); +} + +bool +VideoWindow::init() +{ + DEBUG_BLOCK + + initVideo(); + + debug() << "xine_new()\n"; + m_xine = xine_new(); + if( !m_xine ) + return false; + + #ifdef XINE_SAFE_MODE + xine_engine_set_param( m_xine, XINE_ENGINE_PARAM_VERBOSITY, 99 ); + #endif + + debug() << "xine_config_load()\n"; + xine_config_load( m_xine, QFile::encodeName( QDir::homeDirPath() + "/.xine/config" ) ); + + debug() << "xine_init()\n"; + xine_init( m_xine ); + + debug() << "xine_open_video_driver()\n"; + m_videoPort = xine_open_video_driver( m_xine, "auto", XINE_VISUAL_TYPE_X11, videoWindow()->x11Visual() ); + + debug() << "xine_open_audio_driver()\n"; + m_audioPort = xine_open_audio_driver( m_xine, "auto", NULL ); + + debug() << "xine_stream_new()\n"; + m_stream = xine_stream_new( m_xine, m_audioPort, m_videoPort ); + if( !m_stream ) + return false; + + // we do these after creating the stream as they are non-fatal + // and the messagebox creates a modal event loop that allows + // events that require a stream to have been created.. + if( !m_videoPort ) + MessageBox::error( i18n("xine was unable to initialize any video-drivers.") ); + if( !m_audioPort ) + MessageBox::error( i18n("xine was unable to initialize any audio-drivers.") ); + + debug() << "xine_osd_new()\n"; + m_osd = xine_osd_new( m_stream, 10, 10, 1000, 18 * 6 + 10 ); + if( m_osd ) { + xine_osd_set_font( m_osd, "sans", 18 ); + xine_osd_set_text_palette( m_osd, XINE_TEXTPALETTE_WHITE_BLACK_TRANSPARENT, XINE_OSD_TEXT1 ); + } + + #ifndef XINE_SAFE_MODE + debug() << "scope_plugin_new()\n"; + m_scope = scope_plugin_new( m_xine, m_audioPort ); + + //FIXME this one seems to make seeking unstable for Codeine, perhaps + xine_set_param( m_stream, XINE_PARAM_METRONOM_PREBUFFER, 6000 ); //less buffering, faster seeking.. + + // causes an abort currently + //xine_trick_mode( m_stream, XINE_TRICK_MODE_SEEK_TO_TIME, 1 ); + #endif + + + { + typedef QValueList<int> List; + List params( List() + << XINE_PARAM_VO_HUE << XINE_PARAM_VO_SATURATION << XINE_PARAM_VO_CONTRAST << XINE_PARAM_VO_BRIGHTNESS + << XINE_PARAM_SPU_CHANNEL << XINE_PARAM_AUDIO_CHANNEL_LOGICAL << XINE_PARAM_VO_ASPECT_RATIO ); + + for( List::ConstIterator it = params.constBegin(), end = params.constEnd(); it != end; ++it ) + debug1( xine_get_param( m_stream, *it ) ); + } + + + debug() << "xine_event_create_listener_thread()\n"; + xine_event_create_listener_thread( m_eventQueue = xine_event_new_queue( m_stream ), &VideoWindow::xineEventListener, (void*)this ); + + //set the UI up to a default state + announceStateChange(); + + startTimer( 200 ); //prunes the scope + + return true; +} + +void +VideoWindow::eject() +{ + //WARNING! don't xine_stop or that, buggers up dtor + + if( m_url.isEmpty() ) + return; + + KConfig *profile = TheStream::profile(); // the config profile for this video file + + #define writeParameter( param, default ) { \ + const int value = xine_get_param( m_stream, param ); \ + const QString key = QString::number( param ); \ + if( value != default ) \ + profile->writeEntry( key, value ); \ + else \ + profile->deleteEntry( key ); } + + writeParameter( XINE_PARAM_VO_HUE, 32768 ); + writeParameter( XINE_PARAM_VO_SATURATION, 32772 ); + writeParameter( XINE_PARAM_VO_CONTRAST, 32772 ); + writeParameter( XINE_PARAM_VO_BRIGHTNESS, 32800 ) + writeParameter( XINE_PARAM_SPU_CHANNEL, -1 ); + writeParameter( XINE_PARAM_AUDIO_CHANNEL_LOGICAL, -1 ); + writeParameter( XINE_PARAM_VO_ASPECT_RATIO, 0 ); + + #undef writeParameter + + + if( xine_get_status( m_stream ) == XINE_STATUS_PLAY && //XINE_STATUS_PLAY = playing OR paused + length() - time() > 5000 ) // if we are really close to the end, don't remember the position + profile->writeEntry( "Position", position() ); + else + profile->deleteEntry( "Position" ); + + const QSize s = videoWindow()->size(); + const QSize defaultSize = TheStream::defaultVideoSize(); + if( s.width() == defaultSize.width() || s.height() == defaultSize.height() ) + profile->deleteEntry( "Preferred Size" ); + else + profile->writeEntry( "Preferred Size", s ); + + profile->sync(); + + m_url = KURL(); +} + +bool +VideoWindow::load( const KURL &url ) +{ + mxcl::WaitCursor allocateOnStack; + + eject(); //save profile for this video + + m_url = url; + + // only gets shown if there is an error generally, as no event processing + // occurs, so no paint event. This is fine IMO, TODO although if xine_open hangs + // due to something, it would be good to show the message... + emit statusMessage( i18n("Loading media: %1" ).arg( url.fileName() ) ); + + debug() << "xine_open()\n"; + if( xine_open( m_stream, url.url().local8Bit() ) ) + { + KConfig *profile = TheStream::profile(); + #define setParameter( param, default ) xine_set_param( m_stream, param, profile->readNumEntry( QString::number( param ), default ) ); + setParameter( XINE_PARAM_VO_HUE, 32768 ); + setParameter( XINE_PARAM_VO_SATURATION, 32772 ); + setParameter( XINE_PARAM_VO_CONTRAST, 32772 ); + setParameter( XINE_PARAM_VO_BRIGHTNESS, 32800 ) + setParameter( XINE_PARAM_SPU_CHANNEL, -1 ); + setParameter( XINE_PARAM_AUDIO_CHANNEL_LOGICAL, -1 ); + setParameter( XINE_PARAM_VO_ASPECT_RATIO, 0 ); + setParameter( XINE_PARAM_AUDIO_AMP_LEVEL, 100 ); + #undef setParameter + + videoWindow()->setShown( xine_get_stream_info( m_stream, XINE_STREAM_INFO_HAS_VIDEO ) ); + + //TODO popup message for no audio + //TODO popup message for no video + no audio + + #ifndef XINE_SAFE_MODE + // ensure old buffers are deleted + // FIXME leaves one erroneous buffer + timerEvent( 0 ); + + if( m_scope ) { + xine_post_out_t *source = xine_get_audio_source( m_stream ); + xine_post_in_t *target = (xine_post_in_t*)xine_post_input( m_scope, const_cast<char*>("audio in") ); + xine_post_wire( source, target ); + } + #endif + + announceStateChange(); + + return true; + } + + showErrorMessage(); + announceStateChange(); + m_url = KURL(); + return false; +} + +bool +VideoWindow::play( uint offset ) +{ + mxcl::WaitCursor allocateOnStack; + + const bool resume = offset > 0 && /*FIXME*/ m_url.protocol() != "dvd"; + if( resume ) + //HACK because we have to do xine_play() the audio "stutters" + // so we mute it and then unmute it to make it sound better + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 1 ); + + debug() << "xine_play()\n"; + if( xine_play( m_stream, offset, 0 ) ) + { + if( resume ) { + //we have to set this or it stays at 0 + Slider::instance()->setValue( offset ); + + // we come up paused if we are resuming playback from a previous session + pause(); + + // see above from HACK + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 0 ); + } + else + announceStateChange(); + + return true; + } + + showErrorMessage(); + return false; +} + +void +VideoWindow::record() +{ + xine_cfg_entry_t config; + + if( xine_config_lookup_entry( m_xine, "misc.save_dir", &config ) ) + { + //TODO which fricking KDE function tells me this? Who can tell, stupid KDE API + QDir d( QDir::home().filePath( "Desktop" ) ); + config.str_value = qstrdup( d.exists() //FIXME tiny-mem-leak, *shrug* + ? d.path().utf8() + : QDir::homeDirPath().utf8() ); + xine_config_update_entry( m_xine, &config ); + + const QString fileName = m_url.filename(); + + QString + url = m_url.url(); + url += "#save:"; + url += m_url.host(); + url += " ["; + url += QDate::currentDate().toString(); + url += ']'; + url += fileName.mid( fileName.findRev( '.' ) + 1 ).lower(); + + xine_open( m_stream, url.local8Bit() ); + xine_play( m_stream, 0, 0 ); + + emit statusMessage( i18n( "Recording to: %1" ).arg( url ) ); + + debug() << url << endl; + } + else + debug() << "unable to set misc.save_dir\n"; +} + +void +VideoWindow::stop() +{ + xine_stop( m_stream ); + + announceStateChange(); +} + +void +VideoWindow::pause() +{ + if( xine_get_status( m_stream ) == XINE_STATUS_STOP ) + play(); + + else if( m_url.protocol() == "http" ) + // we are playing and it's an HTTP stream + stop(); + + else if( xine_get_param( m_stream, XINE_PARAM_SPEED ) ) { + // do first because xine is slow to pause and is bad feedback otherwise + emit stateChanged( Engine::Paused ); + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + showOSD( i18n( "Playback paused" ) ); + } + else { + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_NORMAL ); + announceStateChange(); + showOSD( i18n( "Playback resumed" ) ); + } +} + +void +VideoWindow::showErrorMessage() +{ + const QString name = m_url.fileName(); + + debug() << "xine_get_error()\n"; + switch( xine_get_error( m_stream ) ) + { + case XINE_ERROR_NO_INPUT_PLUGIN: + MessageBox::sorry( i18n("There is no input plugin that can read: %1.").arg( name ) ); + break; + case XINE_ERROR_NO_DEMUX_PLUGIN: + MessageBox::sorry( i18n("There is no demux plugin available for %1.").arg( name ) ); + break; + case XINE_ERROR_DEMUX_FAILED: + MessageBox::sorry( i18n("Demuxing failed for %1.").arg( name ) ); + break; + case XINE_ERROR_INPUT_FAILED: + case XINE_ERROR_MALFORMED_MRL: + case XINE_ERROR_NONE: + MessageBox::sorry( i18n("Internal error while attempting to play %1.").arg( name ) ); + break; + } +} + +Engine::State +VideoWindow::state() const +{ + //FIXME this is for the analyzer, but I don't like the analyzer being dodgy like this + if( !m_xine || !m_stream ) + return Engine::Uninitialised; + + switch( xine_get_status( m_stream ) ) + { + case XINE_STATUS_PLAY: return xine_get_param( m_stream, XINE_PARAM_SPEED ) ? Engine::Playing : Engine::Paused; + case XINE_STATUS_IDLE: return Engine::Empty; //FIXME this route never used! + case XINE_STATUS_STOP: + default: return m_url.isEmpty() ? Engine::Empty : Engine::Loaded; + } +} + +uint +VideoWindow::posTimeLength( PosTimeLength type ) const +{ + int pos = 0, time = 0, length = 0; + xine_get_pos_length( m_stream, &pos, &time, &length ); + + switch( type ) { + case Pos: return pos; + case Time: return time; + case Length: return length; + } + + return 0; //--warning +} + +uint +VideoWindow::volume() const +{ + //TODO I don't like the design + return xine_get_param( m_stream, XINE_PARAM_AUDIO_AMP_LEVEL ); +} + +void +VideoWindow::seek( uint pos ) +{ + bool wasPaused = false; + + // If we seek to the end the track ended event is sent, but it is + // delayed as it happens in xine-event loop and before that we are + // already processing the next seek event (if user uses mouse wheel + // or keyboard to seek) and this causes the ui to think video is + // stopped but xine is actually playing the track. Tada! + // TODO set state based on events from xine only + if( pos > 65534 ) + pos = 65534; + + switch( state() ) { + case Engine::Uninitialised: + //NOTE should never happen + Debug::warning() << "Seek attempt thwarted! xine not initialised!\n"; + return; + case Engine::Empty: + Debug::warning() << "Seek attempt thwarted! No media loaded!\n"; + return; + case Engine::Loaded: + // then the state is changing and we should announce it + play( pos ); + return; + case Engine::Paused: + // xine_play unpauses stream if stream was paused + // was broken at 1.0.1 still + wasPaused = true; + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 1 ); + break; + default: + ; + } + + if( !TheStream::canSeek() ) { + // for http streaming it is not a good idea to seek as xine freezes + // and/or just breaks, this is xine 1.0.1 + Debug::warning() << "We won't try to seek as the media is not seekable!\n"; + return; + } + + //TODO depend on a version that CAN seek in flacs! + if( m_url.path().endsWith( ".flac", false ) ) { + emit statusMessage( i18n("xine cannot currently seek in flac media") ); + return; + } + + //better feedback + //NOTE doesn't work! I can't tell why.. + Slider::instance()->QSlider::setValue( pos ); + Slider::instance()->repaint( false ); + + const bool fullscreen = toggleAction("fullscreen")->isChecked(); + if( fullscreen ) { + //TODO don't use OSD (sucks) show slider widget instead + QString osd = "["; + QChar separator = '|'; + + for( uint x = 0, y = int(pos / (65535.0/20.0)); x < 20; x++ ) { + if( x > y ) + separator = '.'; + osd += separator; + } + osd += ']'; + + xine_osd_clear( m_osd ); + xine_osd_draw_text( m_osd, 0, 0, osd.utf8(), XINE_OSD_TEXT1 ); + xine_osd_show( m_osd, 0 ); + } + + xine_play( m_stream, (int)pos, 0 ); + + if( fullscreen ) + //after xine_play because the hide command uses stream position + xine_osd_hide( m_osd, xine_get_current_vpts( m_stream ) + 180000 ); //2 seconds + + if( wasPaused ) + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ), + xine_set_param( m_stream, XINE_PARAM_AUDIO_AMP_MUTE, 0 ); +} + +void +VideoWindow::setStreamParameter( int value ) +{ + QCString sender = this->sender()->name(); + int parameter; + + if( sender == "hue" ) + parameter = XINE_PARAM_VO_HUE; + else if( sender == "saturation" ) + parameter = XINE_PARAM_VO_SATURATION; + else if( sender == "contrast" ) + parameter = XINE_PARAM_VO_CONTRAST; + else if( sender == "brightness" ) + parameter = XINE_PARAM_VO_BRIGHTNESS; + else if( sender == "subtitle_channels_menu" ) + parameter = XINE_PARAM_SPU_CHANNEL, + value -= 2; + else if( sender == "audio_channels_menu" ) + parameter = XINE_PARAM_AUDIO_CHANNEL_LOGICAL, + value -= 2; + else if( sender == "aspect_ratio_menu" ) + parameter = XINE_PARAM_VO_ASPECT_RATIO; + else if( sender == "volume" ) + parameter = XINE_PARAM_AUDIO_AMP_LEVEL; + else + return; + + xine_set_param( m_stream, parameter, value ); +} + +const Engine::Scope& +VideoWindow::scope() +{ + using Analyzer::SCOPE_SIZE; + + static Engine::Scope scope( SCOPE_SIZE ); + + if( xine_get_status( m_stream ) != XINE_STATUS_PLAY ) + return scope; + + //prune the buffer list and update the m_current_vpts timestamp + timerEvent( 0 ); + + for( int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_AUDIO_CHANNELS ), frame = 0; frame < SCOPE_SIZE; ) + { + MyNode *best_node = 0; + + for( MyNode *node = myList->next; node != myList; node = node->next ) + if( node->vpts <= m_current_vpts && (!best_node || node->vpts > best_node->vpts) ) + best_node = node; + + if( !best_node || best_node->vpts_end < m_current_vpts ) + break; + + int64_t + diff = m_current_vpts; + diff -= best_node->vpts; + diff *= 1<<16; + diff /= myMetronom->pts_per_smpls; + + const int16_t* + data16 = best_node->mem; + data16 += diff; + + diff += diff % channels; //important correction to ensure we don't overflow the buffer + diff /= channels; + + int + n = best_node->num_frames; + n -= diff; + n += frame; //clipping for # of frames we need + + if( n > SCOPE_SIZE ) + n = SCOPE_SIZE; //bounds limiting + + for( int a, c; frame < n; ++frame, data16 += channels ) { + for( a = c = 0; c < channels; ++c ) + a += data16[c]; + + a /= channels; + scope[frame] = a; + } + + m_current_vpts = best_node->vpts_end; + m_current_vpts++; //FIXME needs to be done for some reason, or you get situations where it uses same buffer again and again + } + + return scope; +} + +void +VideoWindow::timerEvent( QTimerEvent* ) +{ + /// here we prune the buffer list regularly + #ifndef XINE_SAFE_MODE + MyNode * const first_node = myList->next; + MyNode const * const list_end = myList; + + m_current_vpts = (xine_get_status( m_stream ) == XINE_STATUS_PLAY) + ? xine_get_current_vpts( m_stream ) + : std::numeric_limits<int64_t>::max(); + + for( MyNode *prev = first_node, *node = first_node->next; node != list_end; node = node->next ) + { + // we never delete first_node + // this maintains thread-safety + if( node->vpts_end < m_current_vpts ) { + prev->next = node->next; + + free( node->mem ); + free( node ); + + node = prev; + } + + prev = node; + } + #endif +} + +void +VideoWindow::customEvent( QCustomEvent *e ) +{ + switch( e->type() - 2000 ) { + case XINE_EVENT_UI_PLAYBACK_FINISHED: + emit stateChanged( Engine::TrackEnded ); + break; + + case XINE_EVENT_FRAME_FORMAT_CHANGE: + //TODO not ideal really + debug() << "XINE_EVENT_FRAME_FORMAT_CHANGE\n"; + break; + + case XINE_EVENT_UI_CHANNELS_CHANGED: + { + char s[128]; //apparently sufficient + + { + QStringList languages( "subtitle_channels_menu" ); + int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_MAX_SPU_CHANNEL ); + for( int j = 0; j < channels; j++ ) + languages += xine_get_spu_lang( m_stream, j, s ) ? s : i18n("Channel %1").arg( j+1 ); + emit channelsChanged( languages ); + } + + { + QStringList languages( "audio_channels_menu" ); + int channels = xine_get_stream_info( m_stream, XINE_STREAM_INFO_MAX_AUDIO_CHANNEL ); + for( int j = 0; j < channels; j++ ) + languages += xine_get_audio_lang( m_stream, j, s ) ? s : i18n("Channel %1").arg( j+1 ); + emit channelsChanged( languages ); + } + break; + } + + case 1000: + #define message static_cast<QString*>(e->data()) + emit statusMessage( *message ); + delete message; + break; + + case 1001: + MessageBox::sorry( (*message).arg( m_url.prettyURL() ) ); + delete message; + break; + + case 1002: + emit titleChanged( *message ); + delete message; + break; + #undef message + + default: + ; + } +} + +void +VideoWindow::xineEventListener( void *p, const xine_event_t* xineEvent ) +{ + if( !p ) + return; + + #define engine static_cast<VideoWindow*>(p) + + switch( xineEvent->type ) { + case XINE_EVENT_UI_NUM_BUTTONS: debug() << "XINE_EVENT_UI_NUM_BUTTONS\n"; break; + case XINE_EVENT_MRL_REFERENCE: { + //FIXME this is not the right way, it will have bugs + debug() << "XINE_EVENT_MRL_REFERENCE\n"; + engine->m_url = QString::fromUtf8( ((xine_mrl_reference_data_t*)xineEvent->data)->mrl ); + QTimer::singleShot( 0, engine, SLOT(play()) ); + break; + } + case XINE_EVENT_DROPPED_FRAMES: debug() << "XINE_EVENT_DROPPED_FRAMES\n"; break; + + case XINE_EVENT_UI_PLAYBACK_FINISHED: + case XINE_EVENT_FRAME_FORMAT_CHANGE: + case XINE_EVENT_UI_CHANNELS_CHANGED: + { + QCustomEvent *ce; + ce = new QCustomEvent( 2000 + xineEvent->type ); + ce->setData( const_cast<xine_event_t*>(xineEvent) ); + QApplication::postEvent( engine, ce ); + break; + } + + case XINE_EVENT_UI_SET_TITLE: + QApplication::postEvent( engine, new QCustomEvent( + QEvent::Type(3002), + new QString( QString::fromUtf8( static_cast<xine_ui_data_t*>(xineEvent->data)->str ) ) ) ); + break; + + case XINE_EVENT_PROGRESS: + { + xine_progress_data_t* pd = (xine_progress_data_t*)xineEvent->data; + + QString + msg = "%1 %2%"; + msg = msg.arg( QString::fromUtf8( pd->description ) ) + .arg( KGlobal::locale()->formatNumber( pd->percent, 0 ) ); + + QApplication::postEvent( engine, new QCustomEvent( QEvent::Type(3000), new QString( msg ) ) ); + break; + } + case XINE_EVENT_UI_MESSAGE: + { + debug() << "message received from xine\n"; + + xine_ui_message_data_t *data = (xine_ui_message_data_t *)xineEvent->data; + QString message; + + switch( data->type ) { + case XINE_MSG_NO_ERROR: + { + //series of \0 separated strings, terminated with a \0\0 + char str[2000]; + char *p = str; + for( char *msg = data->messages; !(*msg == '\0' && *(msg+1) == '\0'); ++msg, ++p ) + *p = *msg == '\0' ? '\n' : *msg; + *p = '\0'; + + debug() << str << endl; + + break; + } + + case XINE_MSG_ENCRYPTED_SOURCE: + message = i18n("The source is encrypted and can not be decrypted."); goto param; + case XINE_MSG_UNKNOWN_HOST: + message = i18n("The host is unknown for the URL: <i>%1</i>"); goto param; + case XINE_MSG_UNKNOWN_DEVICE: + message = i18n("The device name you specified seems invalid."); goto param; + case XINE_MSG_NETWORK_UNREACHABLE: + message = i18n("The network appears unreachable."); goto param; + case XINE_MSG_AUDIO_OUT_UNAVAILABLE: + message = i18n("Audio output unavailable; the device is busy."); goto param; + case XINE_MSG_CONNECTION_REFUSED: + message = i18n("The connection was refused for the URL: <i>%1</i>"); goto param; + case XINE_MSG_FILE_NOT_FOUND: + message = i18n("xine could not find the URL: <i>%1</i>"); goto param; + case XINE_MSG_PERMISSION_ERROR: + message = i18n("Access was denied for the URL: <i>%1</i>"); goto param; + case XINE_MSG_READ_ERROR: + message = i18n("The source cannot be read for the URL: <i>%1</i>"); goto param; + case XINE_MSG_LIBRARY_LOAD_ERROR: + message = i18n("A problem occurred while loading a library or decoder."); goto param; + + case XINE_MSG_GENERAL_WARNING: + case XINE_MSG_SECURITY: + default: + + if(data->explanation) + { + message += "<b>"; + message += QString::fromUtf8( (char*) data + data->explanation ); + message += "</b>"; + } + else break; //if no explanation then why bother! + + //FALL THROUGH + + param: + + message.prepend( "<p>" ); + message += "<p>"; + + if(data->parameters) + { + message += "xine says: <i>"; + message += QString::fromUtf8( (char*) data + data->parameters); + message += "</i>"; + } + else message += i18n("Sorry, no additional information is available."); + + QApplication::postEvent( engine, new QCustomEvent(QEvent::Type(3001), new QString(message)) ); + } + + } //case + } //switch + + #undef engine +} + +void +VideoWindow::toggleDVDMenu() +{ + xine_event_t e; + e.type = XINE_EVENT_INPUT_MENU1; + e.data = NULL; + e.data_length = 0; + + xine_event_send( m_stream, &e ); +} + +void +VideoWindow::showOSD( const QString &message ) +{ + if( m_osd ) { + xine_osd_clear( m_osd ); + xine_osd_draw_text( m_osd, 0, 0, message.utf8(), XINE_OSD_TEXT1 ); + xine_osd_show( m_osd, 0 ); + xine_osd_hide( m_osd, xine_get_current_vpts( m_stream ) + 180000 ); //2 seconds + } +} + +QString +VideoWindow::fileFilter() const +{ + char *supportedExtensions = xine_get_file_extensions( m_xine ); + + QString filter( "*." ); + filter.append( supportedExtensions ); + filter.remove( "txt" ); + filter.remove( "png" ); + filter.replace( ' ', " *." ); + + std::free( supportedExtensions ); + + return filter; +} + +} //namespace Codeine diff --git a/src/app/xineEngine.h b/src/app/xineEngine.h new file mode 100644 index 0000000..781bd72 --- /dev/null +++ b/src/app/xineEngine.h @@ -0,0 +1,159 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_VIDEOWINDOW_H +#define CODEINE_VIDEOWINDOW_H + +#include "codeine.h" +#include <qtimer.h> +#include <qwidget.h> +#include <kurl.h> +#include <vector> + +typedef struct xine_s xine_t; +typedef struct xine_stream_s xine_stream_t; +typedef struct xine_video_port_s xine_video_port_t; +typedef struct xine_audio_port_s xine_audio_port_t; +typedef struct xine_event_queue_s xine_event_queue_t; +typedef struct xine_post_s xine_post_t; +typedef struct xine_osd_s xine_osd_t; + +namespace Engine { + typedef std::vector<int16_t> Scope; +} + + +namespace Codeine +{ + /** Functions declared here are defined in: + * xineEngine.cpp + * videoWindow.cpp + */ + class VideoWindow : public QWidget + { + Q_OBJECT + + enum PosTimeLength { Pos, Time, Length }; + + static VideoWindow *s_instance; + + VideoWindow( const VideoWindow& ); //disable + VideoWindow &operator=( const VideoWindow& ); //disable + + friend class TheStream; + friend VideoWindow* const engine(); + friend VideoWindow* const videoWindow(); + + public: + VideoWindow( QWidget *parent ); + ~VideoWindow(); + + bool init(); + void exit(); + + bool load( const KURL &url ); + bool play( uint = 0 ); + + uint position() const { return posTimeLength( Pos ); } + uint time() const { return posTimeLength( Time ); } + uint length() const { return posTimeLength( Length ); } + + uint volume() const; + + const Engine::Scope &scope(); + Engine::State state() const; + + operator xine_t*() const { return m_xine; } + operator xine_stream_t*() const { return m_stream; } + + public slots: + void pause(); + void record(); + void seek( uint ); + void stop(); + + ///special slot, see implementation to facilitate understanding + void setStreamParameter( int ); + + signals: + void stateChanged( Engine::State ); + void statusMessage( const QString& ); + void titleChanged( const QString& ); + void channelsChanged( const QStringList& ); + + private: + #ifdef HAVE_XINE_H + static void xineEventListener( void*, const xine_event_t* ); + #endif + + uint posTimeLength( PosTimeLength ) const; + void showErrorMessage(); + + virtual void customEvent( QCustomEvent* ); + virtual void timerEvent( QTimerEvent* ); + + void eject(); + + void announceStateChange() { emit stateChanged( state() ); } + + xine_osd_t *m_osd; + xine_stream_t *m_stream; + xine_event_queue_t *m_eventQueue; + xine_video_port_t *m_videoPort; + xine_audio_port_t *m_audioPort; + xine_post_t *m_scope; + xine_t *m_xine; + + int64_t m_current_vpts; + + KURL m_url; + + public: + QString fileFilter() const; + + public slots: + void toggleDVDMenu(); + void showOSD( const QString& ); + + /// Stuff to do with video and the video window/widget + private: + static void destSizeCallBack( void*, int, int, double, int*, int*, double* ); + static void frameOutputCallBack( void*, int, int, double, int*, int*, int*, int*, double*, int*, int* ); + + void initVideo(); + void cleanUpVideo(); + + public: + static const uint CURSOR_HIDE_TIMEOUT = 2000; + + virtual QSize sizeHint() const; + virtual QSize minimumSizeHint() const; + + void *x11Visual() const; + void becomePreferredSize(); + QImage captureFrame() const; + + enum { ExposeEvent = 3000 }; + + public slots: + void resetZoom(); + + private slots: + void hideCursor(); + + private: + virtual void contextMenuEvent( QContextMenuEvent* ); + virtual bool event( QEvent* ); + virtual bool x11Event( XEvent* ); + + double m_displayRatio; + QTimer m_timer; + }; + + //global function for general use by Codeine + //videoWindow() is const for Xlib-thread-safety reasons + inline VideoWindow* const videoWindow() { return VideoWindow::s_instance; } + inline VideoWindow* const engine() { return VideoWindow::s_instance; } +} + +#endif diff --git a/src/app/xineScope.c b/src/app/xineScope.c new file mode 100644 index 0000000..740d574 --- /dev/null +++ b/src/app/xineScope.c @@ -0,0 +1,148 @@ +/* Author: Max Howell <[email protected]>, (C) 2004
+ Copyright: See COPYING file that comes with this distribution */
+
+/* gcc doesn't like inline for me */
+#define inline
+/* need access to port_ticket */
+#define XINE_ENGINE_INTERNAL
+
+#include "xineScope.h"
+#include <xine/post.h>
+#include <xine/xine_internal.h>
+
+
+static MyNode theList;
+static metronom_t theMetronom;
+static int myChannels = 0;
+
+MyNode* const myList = &theList;
+metronom_t* const myMetronom = &theMetronom;
+
+
+/* defined in xineEngine.cpp */
+extern void _debug( const char * );
+
+
+/*************************
+* post plugin functions *
+*************************/
+
+static int
+scope_port_open( xine_audio_port_t *port_gen, xine_stream_t *stream, uint32_t bits, uint32_t rate, int mode )
+{
+ _debug( "scope_port_open()\n" );
+
+ #define port ((post_audio_port_t*)port_gen)
+
+ _x_post_rewire( (post_plugin_t*)port->post );
+ _x_post_inc_usage( port );
+
+ port->stream = stream;
+ port->bits = bits;
+ port->rate = rate;
+ port->mode = mode;
+
+ myChannels = _x_ao_mode2channels( mode );
+
+ return port->original_port->open( port->original_port, stream, bits, rate, mode );
+}
+
+static void
+scope_port_close( xine_audio_port_t *port_gen, xine_stream_t *stream )
+{
+ _debug( "scope_port_close()\n" );
+
+ port->stream = NULL;
+ port->original_port->close( port->original_port, stream );
+
+ _x_post_dec_usage( port );
+}
+
+static void
+scope_port_put_buffer( xine_audio_port_t *port_gen, audio_buffer_t *buf, xine_stream_t *stream )
+{
+ MyNode *new_node;
+ const int num_samples = buf->num_frames * myChannels;
+
+ /* we are too simple to handle 8bit */
+ /* what does it mean when stream == NULL? */
+ if( port->bits == 8 ) {
+ port->original_port->put_buffer( port->original_port, buf, stream ); return; }
+
+ /* I keep my own metronom because xine wouldn't for some reason */
+ memcpy( myMetronom, stream->metronom, sizeof(metronom_t) );
+
+ new_node = malloc( sizeof(MyNode) );
+ new_node->vpts = myMetronom->got_audio_samples( myMetronom, buf->vpts, buf->num_frames );
+ new_node->num_frames = buf->num_frames;
+ new_node->mem = malloc( num_samples * 2 );
+ memcpy( new_node->mem, buf->mem, num_samples * 2 );
+
+ {
+ int64_t
+ K = myMetronom->pts_per_smpls; /*smpls = 1<<16 samples*/
+ K *= num_samples;
+ K /= (1<<16);
+ K += new_node->vpts;
+
+ new_node->vpts_end = K;
+ }
+
+ /* pass data to original port */
+ port->original_port->put_buffer( port->original_port, buf, stream );
+
+ /* finally we should append the current buffer to the list
+ * NOTE this is thread-safe due to the way we handle the list in the GUI thread */
+ new_node->next = myList->next;
+ myList->next = new_node;
+
+ #undef port
+}
+
+static void
+scope_dispose( post_plugin_t *this )
+{
+ free( this );
+}
+
+
+/************************
+* plugin init function *
+************************/
+
+xine_post_t*
+scope_plugin_new( xine_t *xine, xine_audio_port_t *audio_target )
+{
+ if( audio_target == NULL )
+ return NULL;
+
+ post_plugin_t *post_plugin = xine_xmalloc( sizeof(post_plugin_t) );
+
+ {
+ post_plugin_t *this = post_plugin;
+ post_in_t *input;
+ post_out_t *output;
+ post_audio_port_t *port;
+
+ _x_post_init( this, 1, 0 );
+
+ port = _x_post_intercept_audio_port( this, audio_target, &input, &output );
+ port->new_port.open = scope_port_open;
+ port->new_port.close = scope_port_close;
+ port->new_port.put_buffer = scope_port_put_buffer;
+
+ this->xine_post.audio_input[0] = &port->new_port;
+ this->xine_post.type = PLUGIN_POST;
+
+ this->dispose = scope_dispose;
+ }
+
+ /* code is straight from xine_init_post()
+ can't use that function as it only dlopens the plugins
+ and our plugin is statically linked in */
+
+ post_plugin->running_ticket = xine->port_ticket;
+ post_plugin->xine = xine;
+
+ return &post_plugin->xine_post;
+}
diff --git a/src/app/xineScope.h b/src/app/xineScope.h new file mode 100644 index 0000000..f2dae75 --- /dev/null +++ b/src/app/xineScope.h @@ -0,0 +1,38 @@ +/* Author: Max Howell <[email protected]>, (C) 2004
+ Copyright: See COPYING file that comes with this distribution
+
+ This has to be a c file or for some reason it won't link! (GCC 3.4.1)
+*/
+
+#ifndef XINESCOPE_H
+#define XINESCOPE_H
+
+/* need access to some stuff for scope time stamping */
+#define METRONOM_INTERNAL
+
+#include <sys/types.h>
+#include <xine/metronom.h>
+
+typedef struct my_node_s MyNode;
+
+struct my_node_s
+{
+ MyNode *next;
+ int16_t *mem;
+ int num_frames;
+ int64_t vpts;
+ int64_t vpts_end;
+};
+
+extern metronom_t* const myMetronom;
+extern MyNode* const myList;
+
+#ifdef __cplusplus
+extern "C"
+{
+ xine_post_t*
+ scope_plugin_new( xine_t*, xine_audio_port_t* );
+}
+#endif
+
+#endif
diff --git a/src/codeine.h b/src/codeine.h new file mode 100644 index 0000000..24e0659 --- /dev/null +++ b/src/codeine.h @@ -0,0 +1,43 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_H +#define CODEINE_H + +// try to keep this file light. It gets included by +// practically every implementation and many headers + +namespace Engine +{ + enum State + { + Uninitialised = 0, + Empty = 1, + Loaded = 2, + Playing = 4, + Paused = 8, + TrackEnded = 16 + }; +} + +class QWidget; + +namespace Analyzer +{ + static const int SCOPE_SIZE_EXP = 9; + static const int SCOPE_SIZE = 1 << SCOPE_SIZE_EXP; +} + +namespace Codeine +{ + QWidget *mainWindow(); //defined in mainWindow.cpp +} + +/// used by mainWindow.h and xineEngine.h +int main( int, char** ); + +#define APP_VERSION "1.0.1" +#define APP_NAME "codeine" +#define PRETTY_NAME "Codeine" + +#endif diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 0000000..a6b9baa --- /dev/null +++ b/src/debug.h @@ -0,0 +1,263 @@ +// Author: Max Howell <[email protected]>, (C) 2003-5 +// Copyright: See COPYING file that comes with this distribution +// + +#ifndef CODEINE_DEBUG_H +#define CODEINE_DEBUG_H + +#include <kdebug.h> +#include <qcstring.h> +#include <qvariant.h> +#include <sys/time.h> + +class QApplication; ///@see Debug::Indent +extern QApplication *qApp; + + +/** + * @namespace Debug + * @short kdebug with indentation functionality and convenience macros + * @author Max Howell <[email protected]> + * + * Usage: + * + * #define DEBUG_PREFIX "Blah" + * #include "debug.h" + * + * void function() + * { + * Debug::Block myBlock( __PRETTY_FUNCTION__ ); + * + * debug() << "output1" << endl; + * debug() << "output2" << endl; + * } + * + * Will output: + * + * app: BEGIN: void function() + * app: [Blah] output1 + * app: [Blah] output2 + * app: END: void function(): Took 0.1s + * + * @see Block + * @see CrashHelper + * @see ListStream + */ + + +namespace Debug +{ + inline QCString &indent() + { + static QCString indent; + return indent; + #if 0 + static timeval *stamp = 0; + + if( stamp == 0 ) { + stamp = new timeval; + return "[00:00] "; } + + timeval now; + gettimeofday( &now, 0 ); + now.tv_sec -= stamp->tv_sec; + + QString time( "[%1:%2]" ); + + return time.arg( now.tv_sec / 60, 2 ).arg( now.tv_sec % 60, 2 ).latin1() + indent; + #endif + } + + #ifdef NDEBUG + static inline kndbgstream debug() { return kndbgstream(); } + static inline kndbgstream warning() { return kndbgstream(); } + static inline kndbgstream error() { return kndbgstream(); } + static inline kndbgstream fatal() { return kndbgstream(); } + + static inline void debug1( QVariant v ) {} + + typedef kndbgstream Stream; + #else + #ifndef DEBUG_PREFIX + #define AMK_PREFIX "" + #else + #define AMK_PREFIX "[" DEBUG_PREFIX "] " + #endif + + //from kdebug.h + enum DebugLevels { + KDEBUG_INFO = 0, + KDEBUG_WARN = 1, + KDEBUG_ERROR = 2, + KDEBUG_FATAL = 3 + }; + + static inline kdbgstream debug() { return kdbgstream( indent(), 0, KDEBUG_INFO ) << AMK_PREFIX; } + static inline kdbgstream warning() { return kdbgstream( indent(), 0, KDEBUG_WARN ) << AMK_PREFIX << "[WARNING!] "; } + static inline kdbgstream error() { return kdbgstream( indent(), 0, KDEBUG_ERROR ) << AMK_PREFIX << "[ERROR!] "; } + static inline kdbgstream fatal() { return kdbgstream( indent(), 0, KDEBUG_FATAL ) << AMK_PREFIX; } + + /// convenience function + static inline void debug1( QVariant v ) { kdbgstream( indent(), 0, KDEBUG_INFO ) << v << endl; } + + typedef kdbgstream Stream; + + #undef AMK_PREFIX + #endif + + typedef kndbgstream DummyStream; +} + +using Debug::debug; +using Debug::debug1; + +/// Standard function announcer +#define DEBUG_FUNC_INFO kdDebug() << Debug::indent() << k_funcinfo << endl; + +/// Announce a line +#define DEBUG_LINE_INFO kdDebug() << Debug::indent() << k_funcinfo << "Line: " << __LINE__ << endl; + +/// Convenience macro for making a standard Debug::Block +#define DEBUG_BLOCK Debug::Block uniquelyNamedStackAllocatedStandardBlock( __PRETTY_FUNCTION__ ); + +#define DEBUG_INDENT Debug::indent() += " "; +#define DEBUG_UNINDENT { QCString &s = Debug::indent(); s.truncate( s.length() - 2 ); } + +/// Use this to remind yourself to finish the implementation of a function +#define DEBUG_NOTIMPLEMENTED warning() << "NOT-IMPLEMENTED: " << __PRETTY_FUNCTION__ << endl; + +/// Use this to alert other developers to stop using a function +#define DEBUG_DEPRECATED warning() << "DEPRECATED: " << __PRETTY_FUNCTION__ << endl; + + +namespace Debug +{ + /** + * @class Debug::Block + * @short Use this to label sections of your code + * + * Usage: + * + * void function() + * { + * Debug::Block myBlock( "section" ); + * + * debug() << "output1" << endl; + * debug() << "output2" << endl; + * } + * + * Will output: + * + * app: BEGIN: section + * app: [prefix] output1 + * app: [prefix] output2 + * app: END: section - Took 0.1s + * + */ + + class Block + { + timeval m_start; + const char *m_label; + + public: + Block( const char *label ) + : m_label( label ) + { + gettimeofday( &m_start, 0 ); + + kdDebug() << indent() << "BEGIN: " << label << "\n"; + DEBUG_INDENT + } + + ~Block() + { + timeval end; + gettimeofday( &end, 0 ); + + end.tv_sec -= m_start.tv_sec; + if( end.tv_usec < m_start.tv_usec) { + // Manually carry a one from the seconds field. + end.tv_usec += 1000000; + end.tv_sec--; + } + end.tv_usec -= m_start.tv_usec; + + double duration = double(end.tv_sec) + (double(end.tv_usec) / 1000000.0); + + DEBUG_UNINDENT + kdDebug() << indent() << "END__: " << m_label + << " - Took " << QString::number( duration, 'g', 3 ) << "s\n"; + } + }; + + + /** + * @name Debug::stamp() + * @short To facilitate crash/freeze bugs, by making it easy to mark code that has been processed + * + * Usage: + * + * { + * Debug::stamp(); + * function1(); + * Debug::stamp(); + * function2(); + * Debug::stamp(); + * } + * + * Will output (assuming the crash occurs in function2() + * + * app: Stamp: 1 + * app: Stamp: 2 + * + */ + + inline void stamp() + { + static int n = 0; + debug() << "| Stamp: " << ++n << endl; + } +} + + +namespace Debug +{ + /** + * @class Debug::List + * @short You can pass anything to this and it will output it as a list + * + * debug() << (Debug::List() << anInt << aString << aQStringList << aDouble) << endl; + */ + + typedef QValueList<QVariant> List; +} + + +#include <kmessagebox.h> + +namespace Codeine +{ + //FIXME this function is inlined, so this may cause linkage problems for some people.. + extern class VideoWindow* const videoWindow(); + + namespace MessageBox + { + static inline void error( const QString &message ) + { + KMessageBox::error( (QWidget*)videoWindow(), message ); + } + + static inline void sorry( const QString &message ) + { + KMessageBox::error( (QWidget*)videoWindow(), message ); + } + + static inline void information( const QString &message, const QString &title ) + { + KMessageBox::information( (QWidget*)videoWindow(), message, title ); + } + } +} + +#endif diff --git a/src/mxcl.library.cpp b/src/mxcl.library.cpp new file mode 100644 index 0000000..597a548 --- /dev/null +++ b/src/mxcl.library.cpp @@ -0,0 +1,19 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include "mxcl.library.h" +#include <qapplication.h> +#include <kcursor.h> + +namespace mxcl +{ + WaitCursor::WaitCursor() + { + QApplication::setOverrideCursor( KCursor::waitCursor() ); + } + + WaitCursor::~WaitCursor() + { + QApplication::restoreOverrideCursor(); + } +} diff --git a/src/mxcl.library.h b/src/mxcl.library.h new file mode 100644 index 0000000..feb71ef --- /dev/null +++ b/src/mxcl.library.h @@ -0,0 +1,28 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef MXCL_LIBRARY_H +#define MXCL_LIBRARY_H + + +namespace mxcl +{ + /// Allocate on stack, wait cursor will be shown during existance + struct WaitCursor + { + WaitCursor(); + ~WaitCursor(); + }; +} + + +/// almost always negates the need to #include <klocale.h> in implementations +#include <qstring.h> +QString i18n( const char *text ); + + +/// very useful for QStringLists +#define foreach( x ) \ + for( QStringList::ConstIterator it = x.constBegin(), end = x.constEnd(); it != end; ++it ) + +#endif diff --git a/src/part/SConscript b/src/part/SConscript new file mode 100644 index 0000000..c2bce67 --- /dev/null +++ b/src/part/SConscript @@ -0,0 +1,12 @@ +# Copyright 2005 Max Howell <[email protected]> + +Import( "*" ) +myenv=env.Copy() + +## Additional paths for compiling the source files +## Always add '../' (top-level directory) because moc makes code that needs it +KDEaddpaths( ['./', '../', '../../'], myenv ) + +KDEaddlibs( ['qt-mt', 'kdecore', 'kdeui', 'kparts', 'xine'], myenv ) + +KDEshlib( "libcodeine", Split( "part.cpp xineEngine.cpp videoWindow.cpp toolbar.cpp ../mxcl.library.cpp" ), myenv ) diff --git a/src/part/part.cpp b/src/part/part.cpp new file mode 100644 index 0000000..20d1577 --- /dev/null +++ b/src/part/part.cpp @@ -0,0 +1,83 @@ +// Author: Max Howell <[email protected]>, (C) 2005
+// Copyright: See COPYING file that comes with this distribution
+
+#include "codeine.h"
+#include "debug.h"
+#include <kaboutdata.h>
+#include <kparts/genericfactory.h>
+#include "part.h"
+#include <qtimer.h>
+#include "toolbar.h"
+#include "videoWindow.h"
+
+#include <kaction.h>
+#include <qslider.h>
+
+namespace Codeine
+{
+ typedef KParts::GenericFactory<Codeine::Part> Factory;
+}
+
+
+K_EXPORT_COMPONENT_FACTORY( libcodeine, Codeine::Factory )
+
+
+namespace Codeine
+{
+ Part::Part( QWidget *parentWidget, const char *widgetName, QObject *parent, const char *name, const QStringList& )
+ : ReadOnlyPart( parent, name )
+ , m_statusBarExtension( new KParts::StatusBarExtension( this ) )
+ {
+ setInstance( Factory::instance() );
+ setWidget( new VideoWindow( parentWidget, widgetName ) );
+
+ if( !videoWindow()->init() )
+ //FIXME this will terminate the host, eg Konqueror
+ Debug::fatal() << "Couldn't init xine!\n";
+
+ KAction *play = new KToggleAction( i18n("Play"), "player_play", Qt::Key_Space, videoWindow(), SLOT(togglePlay()), actionCollection(), "play" );
+ KAction *mute = new KToggleAction( i18n("Mute"), "player_mute", Qt::Key_M, videoWindow(), SLOT(toggleMute()), actionCollection(), "mute" );
+ KToolBar *toolBar = new MouseOverToolBar( widget() );
+ play->plug( toolBar );
+ mute->plug( toolBar );
+ m_slider = new QSlider( Qt::Horizontal, toolBar, "slider" );
+ m_slider->setMaxValue( 65535 );
+ toolBar->setStretchableWidget( m_slider );
+ toolBar->addSeparator(); //FIXME ugly
+
+ QObject *o = (QObject*)statusBar();
+ connect( videoWindow(), SIGNAL(statusMessage( const QString& )), o, SLOT(message( const QString& )) );
+ connect( videoWindow(), SIGNAL(titleChanged( const QString& )), o, SLOT(message( const QString& )) ); //FIXME
+ }
+
+ bool
+ Part::openURL( const KURL &url )
+ {
+ //FIXME nasty, we'd rather not do this way
+ killTimers();
+ startTimer( 100 );
+
+ return videoWindow()->play( m_url = url );
+ }
+
+ bool
+ Part::closeURL()
+ {
+ m_url = KURL();
+ videoWindow()->eject();
+ return true;
+ }
+
+ KAboutData*
+ Part::createAboutData()
+ {
+ // generic factory expects this on the heap
+ return new KAboutData( APP_NAME, PRETTY_NAME, APP_VERSION );
+ }
+
+ void
+ Part::timerEvent( QTimerEvent* )
+ {
+ m_slider->setValue( videoWindow()->position() );
+ }
+}
diff --git a/src/part/part.h b/src/part/part.h new file mode 100644 index 0000000..30467ac --- /dev/null +++ b/src/part/part.h @@ -0,0 +1,38 @@ +// Author: Max Howell <[email protected]>, (C) 2005
+// Copyright: See COPYING file that comes with this distribution
+
+#ifndef CODEINE_PART_H
+#define CODEINE_PART_H
+
+#include <kparts/statusbarextension.h>
+#include <kparts/part.h>
+#include <kurl.h>
+
+class KAboutData;
+class QSlider;
+
+
+namespace Codeine
+{
+ class Part : public KParts::ReadOnlyPart
+ {
+ public:
+ Part( QWidget*, const char*, QObject*, const char*, const QStringList& );
+
+ virtual bool openFile() { return false; } //pure virtual in base class
+ virtual bool openURL( const KURL& );
+ virtual bool closeURL();
+
+ static KAboutData *createAboutData();
+
+ private:
+ KParts::StatusBarExtension *m_statusBarExtension;
+ QSlider *m_slider;
+
+ KStatusBar *statusBar() { return m_statusBarExtension->statusBar(); }
+
+ virtual void timerEvent( QTimerEvent* );
+ };
+}
+
+#endif
diff --git a/src/part/toolbar.cpp b/src/part/toolbar.cpp new file mode 100644 index 0000000..8939074 --- /dev/null +++ b/src/part/toolbar.cpp @@ -0,0 +1,44 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#include <kpushbutton.h> +#include <qapplication.h> +#include <qevent.h> +#include "toolbar.h" + + +MouseOverToolBar::MouseOverToolBar( QWidget *parent ) + : KToolBar( parent ) +{ + parent->installEventFilter( this ); + move( 0, 0 ); //TODO necessary? + hide(); + + setPalette( QApplication::palette() ); //videoWindow palette has a black background +} + +bool +MouseOverToolBar::eventFilter( QObject *o, QEvent *e ) +{ + Q_ASSERT( o == parent() ); + + switch( e->type() ) + { + case QEvent::Resize: + resize( static_cast<QResizeEvent*>(e)->size().width(), sizeHint().height() ); + break; + + case QEvent::Enter: + show(); + break; + + case QEvent::Leave: + hide(); + break; + + default: + ; + } + + return false; +}
\ No newline at end of file diff --git a/src/part/toolbar.h b/src/part/toolbar.h new file mode 100644 index 0000000..cd189d4 --- /dev/null +++ b/src/part/toolbar.h @@ -0,0 +1,18 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_TOOLBAR_H +#define CODEINE_TOOLBAR_H + +#include <ktoolbar.h> + + +class MouseOverToolBar : public KToolBar +{ + virtual bool eventFilter( QObject*, QEvent* ); + +public: + MouseOverToolBar( QWidget *parent ); +}; + +#endif diff --git a/src/part/videoWindow.cpp b/src/part/videoWindow.cpp new file mode 100644 index 0000000..d798b36 --- /dev/null +++ b/src/part/videoWindow.cpp @@ -0,0 +1,192 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "videoWindow" + +#include <cstdlib> +#include "debug.h" +#include <qapplication.h> //sendEvent() +#include <qcursor.h> +#include <qevent.h> +#include "videoWindow.h" +#include <X11/Xlib.h> //TODO this breaks compile for lots of people due to excessive macro content +#include <xine.h> //x11_visual_t + + +namespace Codeine { + + +VideoWindow *VideoWindow::s_instance = 0; + + +namespace X +{ + Display *d; + int s, w; +} + + +VideoWindow::VideoWindow( QWidget *parent, const char *name ) + : QWidget( parent, name ) + , m_osd( 0 ) + , m_stream( 0 ) + , m_eventQueue( 0 ) + , m_videoPort( 0 ) + , m_audioPort( 0 ) + , m_xine( 0 ) + , m_displayRatio( 1 ) +{ + s_instance = this; + + // with this Konqueror would crash on exit + // without this we may be unstable! + //XInitThreads(); + + show(); + + setWFlags( Qt::WNoAutoErase ); + setMouseTracking( true ); + setAcceptDrops( true ); + setUpdatesEnabled( false ); //to stop Qt drawing over us + setPaletteBackgroundColor( Qt::black ); + + X::d = XOpenDisplay( std::getenv("DISPLAY") ); + X::s = DefaultScreen( X::d ); + X::w = winId(); + + XLockDisplay( X::d ); + XSelectInput( X::d, X::w, ExposureMask ); + + { + using X::d; using X::s; + + //these are Xlib macros + double w = DisplayWidth( d, s ) * 1000 / DisplayWidthMM( d, s ); + double h = DisplayHeight( d, s ) * 1000 / DisplayHeightMM( d, s ); + + m_displayRatio = w / h; + } + + XUnlockDisplay( X::d ); + + connect( &m_timer, SIGNAL(timeout()), SLOT(hideCursor()) ); +} + +VideoWindow::~VideoWindow() +{ + DEBUG_BLOCK + + if( m_osd ) xine_osd_free( m_osd ); + if( m_stream ) xine_close( m_stream ); + if( m_eventQueue ) xine_event_dispose_queue( m_eventQueue ); + if( m_stream ) xine_dispose( m_stream ); + if( m_videoPort ) xine_close_video_driver( m_xine, m_videoPort ); + if( m_audioPort ) xine_close_audio_driver( m_xine, m_audioPort ); + if( m_xine ) xine_exit( m_xine ); + + XCloseDisplay( X::d ); +} + +void* +VideoWindow::x11Visual() const +{ + x11_visual_t* visual = new x11_visual_t; + + visual->display = X::d; + visual->screen = X::s; + visual->d = X::w; + visual->dest_size_cb = &VideoWindow::destSizeCallBack; + visual->frame_output_cb = &VideoWindow::frameOutputCallBack; + visual->user_data = (void*)this; + + return visual; +} + +void +VideoWindow::destSizeCallBack( + void* p, int /*video_width*/, int /*video_height*/, + double /*video_aspect*/, int* dest_width, + int* dest_height, double* dest_aspect ) +{ + if( !p ) + return; + + #define vw static_cast<VideoWindow*>(p) + + *dest_width = vw->width(); + *dest_height = vw->height(); + *dest_aspect = vw->m_displayRatio; +} + +void +VideoWindow::frameOutputCallBack( + void* p, int video_width, int video_height, double video_aspect, + int* dest_x, int* dest_y, int* dest_width, int* dest_height, + double* dest_aspect, int* win_x, int* win_y ) +{ + if( !p ) + return; + + *dest_x = 0; + *dest_y = 0 ; + *dest_width = vw->width(); + *dest_height = vw->height(); + *win_x = vw->x(); + *win_y = vw->y(); + *dest_aspect = vw->m_displayRatio; + + // correct size with video aspect + // TODO what's this about? + if( video_aspect >= vw->m_displayRatio ) + video_width = int( double(video_width * video_aspect / vw->m_displayRatio + 0.5) ); + else + video_height = int( double(video_height * vw->m_displayRatio / video_aspect) + 0.5 ); + + #undef vw +} + +bool +VideoWindow::event( QEvent *e ) +{ + switch( e->type() ) + { + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + unsetCursor(); + m_timer.start( CURSOR_HIDE_TIMEOUT, true ); + break; + + case QEvent::Close: + case QEvent::Hide: + xine_stop( m_stream ); + break; + + case QEvent::Leave: + m_timer.stop(); + break; + + default: + ; + } + + return QWidget::event( e ); +} + +bool +VideoWindow::x11Event( XEvent *e ) +{ + if( e->type == Expose && e->xexpose.count == 0 ) { + xine_gui_send_vo_data( m_stream, XINE_GUI_SEND_EXPOSE_EVENT, e ); + return true; + } + + return false; +} + +void +VideoWindow::hideCursor() +{ + setCursor( Qt::BlankCursor ); +} + +} //namespace Codeine diff --git a/src/part/videoWindow.h b/src/part/videoWindow.h new file mode 100644 index 0000000..7db72ab --- /dev/null +++ b/src/part/videoWindow.h @@ -0,0 +1,94 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#ifndef CODEINE_VIDEO_WINDOW_H +#define CODEINE_VIDEO_WINDOW_H + +#include "../codeine.h" +#include <qtimer.h> +#include <qwidget.h> +#include <kurl.h> + +typedef struct xine_s xine_t; +typedef struct xine_stream_s xine_stream_t; +typedef struct xine_video_port_s xine_video_port_t; +typedef struct xine_audio_port_s xine_audio_port_t; +typedef struct xine_event_queue_s xine_event_queue_t; +typedef struct xine_post_s xine_post_t; +typedef struct xine_osd_s xine_osd_t; + + +namespace Codeine +{ + class VideoWindow : public QWidget + { + Q_OBJECT + + static VideoWindow *s_instance; + static const uint CURSOR_HIDE_TIMEOUT = 2000; + + friend VideoWindow* const videoWindow(); + + public: + VideoWindow( QWidget *parent, const char *name ); + ~VideoWindow(); + + bool init(); + + bool play( KURL ); + void eject(); + + int position(); + + signals: + void statusMessage( const QString& ); + void titleChanged( const QString& ); + + private: + /// @see xineEngine.cpp + #ifdef HAVE_XINE_H + static void xineEventListener( void*, const xine_event_t* ); + #endif + + void showErrorMessage(); //TODO don't use this, just show delayed message + + virtual void customEvent( QCustomEvent* ); + virtual bool x11Event( XEvent* ); + virtual bool event( QEvent* ); + + xine_osd_t *m_osd; + xine_stream_t *m_stream; + xine_event_queue_t *m_eventQueue; + xine_video_port_t *m_videoPort; + xine_audio_port_t *m_audioPort; + xine_t *m_xine; + + KURL m_url; + + private: + void *x11Visual() const; + + static void destSizeCallBack( void*, int, int, double, int*, int*, double* ); + static void frameOutputCallBack( void*, int, int, double, int*, int*, int*, int*, double*, int*, int* ); + + double m_displayRatio; + QTimer m_timer; + + public slots: + void togglePlay(); + void toggleMute(); + + private slots: + void hideCursor(); + + private: + /// prevent compiler generated functions + VideoWindow( const VideoWindow& ); + VideoWindow &operator=( const VideoWindow& ); + bool operator==( const VideoWindow& ); + }; + + inline VideoWindow* const videoWindow() { return VideoWindow::s_instance; } +} + +#endif diff --git a/src/part/xineEngine.cpp b/src/part/xineEngine.cpp new file mode 100644 index 0000000..2365f83 --- /dev/null +++ b/src/part/xineEngine.cpp @@ -0,0 +1,345 @@ +// (C) 2005 Max Howell ([email protected]) +// See COPYING file for licensing information + +#define CODEINE_DEBUG_PREFIX "engine" + +#include "debug.h" +#include <kglobalsettings.h> +#include <klocale.h> +#include "mxcl.library.h" +#include <qapplication.h> //::sendEvent() +#include <qdatetime.h> //::play() +#include <qdir.h> //QDir::homeDir() +#include <xine.h> +#include "videoWindow.h" + + +namespace Codeine { + + +bool +VideoWindow::init() +{ + mxcl::WaitCursor allocateOnStack; + + debug() << "xine_new()\n"; + m_xine = xine_new(); + if( !m_xine ) + return false; + + debug() << "xine_config_load()\n"; + xine_config_load( m_xine, QFile::encodeName( QDir::homeDirPath() + "/.xine/config" ) ); + + debug() << "xine_init()\n"; + xine_init( m_xine ); + + debug() << "xine_open_video_driver()\n"; + m_videoPort = xine_open_video_driver( m_xine, "auto", XINE_VISUAL_TYPE_X11, x11Visual() ); + + debug() << "xine_open_audio_driver()\n"; + m_audioPort = xine_open_audio_driver( m_xine, "auto", NULL ); + + debug() << "xine_stream_new()\n"; + m_stream = xine_stream_new( m_xine, m_audioPort, m_videoPort ); + if( !m_stream ) + return false; + + if( !m_audioPort ) + MessageBox::error( i18n("xine was unable to initialize any audio-drivers.") ); + if( !m_videoPort ) + MessageBox::error( i18n("xine was unable to initialize any video-drivers.") ); + + debug() << "xine_osd_new()\n"; + m_osd = xine_osd_new( m_stream, 10, 10, 1000, 18 * 6 + 10 ); + if( m_osd ) { + xine_osd_set_font( m_osd, "sans", 18 ); + xine_osd_set_text_palette( m_osd, XINE_TEXTPALETTE_WHITE_BLACK_TRANSPARENT, XINE_OSD_TEXT1 ); + } + + debug() << "xine_event_create_listener_thread()\n"; + xine_event_create_listener_thread( + m_eventQueue = xine_event_new_queue( m_stream ), + &VideoWindow::xineEventListener, (void*)this ); + + { /// set save directory + xine_cfg_entry_t config; + + if( xine_config_lookup_entry( m_xine, "misc.save_dir", &config ) ) { + const QCString dir = KGlobalSettings::desktopPath().local8Bit(); + config.str_value = qstrdup( dir ); + xine_config_update_entry( m_xine, &config ); + } + } + + return true; +} + +bool +VideoWindow::play( KURL url ) +{ + DEBUG_BLOCK + + m_url = url; + + mxcl::WaitCursor allocateOnStack; + + //TODO make sensible + if( url.protocol() == "http" ) { + /// automatically save http streams to Desktop folder + + const QString fileName = url.filename(); + + QString + u = url.url(); + u += "#save:"; + u += url.host(); + u += " ["; + u += QDate::currentDate().toString(); + u += ']'; + u += fileName.mid( fileName.findRev( '.' ) + 1 ).lower(); + + url = u; + } + + debug() << "About to load..\n"; + if( xine_open( m_stream, url.url().local8Bit() ) ) + { + debug() << "About to play..\n"; + if( xine_play( m_stream, 0, 0 ) ) + return true; + } + + showErrorMessage(); + return false; +} + +void +VideoWindow::togglePlay() +{ + if( xine_get_param( m_stream, XINE_PARAM_SPEED ) ) { + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_PAUSE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_CLOSE_DEVICE, 1); + } + else + xine_set_param( m_stream, XINE_PARAM_SPEED, XINE_SPEED_NORMAL ); +} + +void +VideoWindow::toggleMute() +{ + bool const muted = xine_get_param( m_stream, XINE_PARAM_AUDIO_MUTE ); + xine_set_param( m_stream, XINE_PARAM_AUDIO_MUTE, !muted ); +} + +void +VideoWindow::eject() +{ + m_url = KURL(); + + xine_stop( m_stream ); +} + +int +VideoWindow::position() +{ + int pos = 0, time = 0, length = 0; + xine_get_pos_length( m_stream, &pos, &time, &length ); + + return pos; +} + +void +VideoWindow::showErrorMessage() +{ + const QString filename = m_url.fileName(); + + debug() << "xine_get_error()\n"; + + // NOTE these error messages are somewhat customised + // relative to the main application + // This is because when embedded in some other application + // the error messages have no context, so we must say that we are a video player! + + switch( xine_get_error( m_stream ) ) + { + case XINE_ERROR_NO_INPUT_PLUGIN: + MessageBox::sorry( i18n("The Codeine video player could not find an input plugin for '%1'.").arg( filename ) ); + break; + case XINE_ERROR_NO_DEMUX_PLUGIN: + MessageBox::sorry( i18n("The Codeine video player could not find a demux plugin for '%1'.").arg( filename ) ); + break; + case XINE_ERROR_DEMUX_FAILED: + MessageBox::sorry( i18n("The Codeine video player failed to demux '%1'; please check your xine installation.").arg( filename ) ); + break; + case XINE_ERROR_INPUT_FAILED: + case XINE_ERROR_MALFORMED_MRL: + case XINE_ERROR_NONE: + MessageBox::sorry( i18n("The Codeine video player reports an internal error; please check your xine installation.") ); + break; + } +} + +void +VideoWindow::customEvent( QCustomEvent *e ) +{ + switch( e->type() - 2000 ) { + case XINE_EVENT_UI_PLAYBACK_FINISHED: +//FIXME emit stateChanged( Engine::TrackEnded ); + break; + + case 1000: + #define message static_cast<QString*>(e->data()) + emit statusMessage( *message ); + delete message; + break; + + case 1001: + MessageBox::sorry( (*message).arg( "FIXME" ) ); //FIXME + delete message; + break; + + case 1002: + emit titleChanged( *message ); + delete message; + break; + #undef message + + default: + ; + } +} + +void +VideoWindow::xineEventListener( void *p, const xine_event_t* xineEvent ) +{ + if( !p ) + return; + + #define engine static_cast<VideoWindow*>(p) + + switch( xineEvent->type ) { + case XINE_EVENT_MRL_REFERENCE: + { + mxcl::WaitCursor allocateOnStack; + const char *mrl = ((xine_mrl_reference_data_t*)xineEvent->data)->mrl; + + debug() << "XINE_EVENT_MRL_REFERENCE: " << mrl << endl; + + if( xine_open( engine->m_stream, mrl ) ) + xine_play( engine->m_stream, 0, 0 ); + + break; + } + + case XINE_EVENT_UI_NUM_BUTTONS: debug() << "XINE_EVENT_UI_NUM_BUTTONS\n"; break; + case XINE_EVENT_DROPPED_FRAMES: debug() << "XINE_EVENT_DROPPED_FRAMES\n"; break; + + case XINE_EVENT_UI_PLAYBACK_FINISHED: + case XINE_EVENT_FRAME_FORMAT_CHANGE: + case XINE_EVENT_UI_CHANNELS_CHANGED: + { + QCustomEvent *ce; + ce = new QCustomEvent( 2000 + xineEvent->type ); + ce->setData( const_cast<xine_event_t*>(xineEvent) ); + QApplication::postEvent( engine, ce ); + break; + } + + case XINE_EVENT_UI_SET_TITLE: + QApplication::postEvent( engine, new QCustomEvent( + QEvent::Type(3002), + new QString( QString::fromUtf8( static_cast<xine_ui_data_t*>(xineEvent->data)->str ) ) ) ); + break; + + case XINE_EVENT_PROGRESS: + { + xine_progress_data_t* pd = (xine_progress_data_t*)xineEvent->data; + + QString + msg = "%1 %2%"; + msg = msg.arg( QString::fromUtf8( pd->description ) ) + .arg( KGlobal::locale()->formatNumber( pd->percent, 0 ) ); + + QApplication::postEvent( engine, new QCustomEvent( QEvent::Type(3000), new QString( msg ) ) ); + break; + } + case XINE_EVENT_UI_MESSAGE: + { + debug() << "Message received from xine\n"; + + xine_ui_message_data_t *data = (xine_ui_message_data_t *)xineEvent->data; + QString message; + + switch( data->type ) { + case XINE_MSG_NO_ERROR: + { + //series of \0 separated strings, terminated with a \0\0 + char str[2000]; + char *p = str; + for( char *msg = data->messages; !(*msg == '\0' && *(msg+1) == '\0'); ++msg, ++p ) + *p = *msg == '\0' ? '\n' : *msg; + *p = '\0'; + + debug() << str << endl; + + break; + } + + case XINE_MSG_ENCRYPTED_SOURCE: + message = i18n("The source is encrypted and can not be decrypted."); goto param; + case XINE_MSG_UNKNOWN_HOST: + message = i18n("The host is unknown for the URL: <i>%1</i>"); goto param; + case XINE_MSG_UNKNOWN_DEVICE: + message = i18n("The device name you specified seems invalid."); goto param; + case XINE_MSG_NETWORK_UNREACHABLE: + message = i18n("The network appears unreachable."); goto param; + case XINE_MSG_AUDIO_OUT_UNAVAILABLE: + message = i18n("Audio output unavailable; the device is busy."); goto param; + case XINE_MSG_CONNECTION_REFUSED: + message = i18n("The connection was refused for the URL: <i>%1</i>"); goto param; + case XINE_MSG_FILE_NOT_FOUND: + message = i18n("xine could not find the URL: <i>%1</i>"); goto param; + case XINE_MSG_PERMISSION_ERROR: + message = i18n("Access was denied for the URL: <i>%1</i>"); goto param; + case XINE_MSG_READ_ERROR: + message = i18n("The source cannot be read for the URL: <i>%1</i>"); goto param; + case XINE_MSG_LIBRARY_LOAD_ERROR: + message = i18n("A problem occurred while loading a library or decoder."); goto param; + + case XINE_MSG_GENERAL_WARNING: + case XINE_MSG_SECURITY: + default: + + if(data->explanation) + { + message += "<b>"; + message += QString::fromUtf8( (char*) data + data->explanation ); + message += "</b>"; + } + else break; //if no explanation then why bother! + + //FALL THROUGH + + param: + + message.prepend( "<p>" ); + message += "<p>"; + + if(data->parameters) + { + message += "xine says: <i>"; + message += QString::fromUtf8( (char*) data + data->parameters); + message += "</i>"; + } + else message += i18n("Sorry, no additional information is available."); + + QApplication::postEvent( engine, new QCustomEvent(QEvent::Type(3001), new QString(message)) ); + } + + } //case + } //switch + + #undef engine +} + +} //namespace Codeine |