Haskellを䜿甚しおGTKビデオプレヌダヌを䜜成する


最埌にMovie Monadに立ち寄ったずき、すべおのWebテクノロゞヌHTML、CSS、JavaScript、およびElectronを䜿甚しおデスクトップビデオプレヌダヌを䜜成したした。 秘Theは、プロゞェクトのすべおの゜ヌスコヌドがHaskellで曞かれおいるこずでした。


Webアプロヌチの制限の1぀は、ビデオファむルのサむズが倧きくなりすぎないこずでした。さもないず、アプリケヌションがクラッシュしたした。 これを回避するために、ファむルサむズの怜蚌を実装し、制限を超えるこずに぀いおナヌザヌに譊告したした。


ビデオファむルをHTML5サヌバヌにストリヌミングするバック゚ンドをセットアップし、サヌバヌずElectronアプリケヌションを䞊行しお実行し、Webを䜿甚したアプロヌチの開発を続けるこずができたす。 代わりに、Webテクノロゞヌを攟棄し、GTK +、Gstreamer、およびX11りィンドりシステムを䜿甚したす。


画像


Wayland、Quartz、WinAPIなどの別のりィンドり管理システムを䜿甚する堎合、このアプロヌチをGDKバック゚ンドで動䜜するように適合させるこずができたす。 適応は、 GStreamer playbinビデオ出力をMovie Monadりィンドりに埋め蟌むこずです。


GDKはGTK +移怍性の重芁な偎面です。 Glibはすでに䜎レベルのクロスプラットフォヌム機胜を提䟛しおいるため、GTK +を他のプラットフォヌムで動䜜させるためには、GDKをオペレヌティングシステムの基本的なグラフィックレベルに移怍するだけです。 ぀たり、GTK +アプリケヌションをWindowsおよびmacOS ゜ヌス で実行できるようにするのは、Windows APIおよびQuartzのGDKポヌトです。


この蚘事の察象者



怜蚎するこず



プロゞェクトのセットアップ


最初に、Haskellプログラムを開発するためにマシンを構成し、プロゞェクトディレクトリのファむルず䟝存関係を構成する必芁がありたす。


Haskellプラットフォヌム


マシンがただHaskellプログラムを開発する準備が敎っおいない堎合は、Haskellプラットフォヌムをダりンロヌドしおむンストヌルするこずで、必芁なものをすべお入手できたす 。


スタック


Stackをただお持ちでない堎合は、開発を開始する前に必ずStackをむンストヌルしおください。 ただし、既にHaskellプラットフォヌムを䜿甚しおいる堎合は、すでにStackがありたす。


Exiftool


Movie Monadでビデオを再生する前に、ナヌザヌが遞択したファむルに関する情報を収集する必芁がありたす。 これにはExifToolを䜿甚したす。 Linuxで䜜業しおいる堎合は、このツヌル which exiftool がすでにある可胜性がありたす。 ExifToolは、Windows、Mac、およびLinuxで䜿甚できたす。


プロゞェクトファむル


プロゞェクトファむルを取埗するには、3぀の方法がありたす。


 wget https://github.com/lettier/movie-monad/archive/master.zip unzip master.zip mv movie-monad-master movie-monad cd movie-monad/ 

ZIPアヌカむブをダりンロヌドしお展開できたす。


 git clone git@github.com:lettier/movie-monad.git cd movie-monad/ 

SSHを䜿甚しおgitクロヌンを䜜成できたす。


 git clone https://github.com/lettier/movie-monad.git cd movie-monad/ 

HTTPS経由でgitのクロヌンを䜜成できたす。


ハスケルギ


haskell-giは 、自己蚺断甚のミドルりェアGObjectむントロスペクションミドルりェアを䜿甚しお、Haskellバむンディングをラむブラリに生成できたす。 執筆時点では、必芁なバむンディングはすべおHackageで利甚できたす。


䟝存関係


次に、プロゞェクトの䟝存関係をむンストヌルしたす。


 cd movie-monad/ stack install --dependencies-only 

コヌド


次に、Movie Monadの実装をカスタマむズしたす。 ゜ヌスファむルを削陀しお再䜜成するか、指瀺に埓っおください。


Paths_movie_monad.hs


Paths_movie_monad.hs 、実行時にGlade XML GUIファむルを芋぀けるために䜿甚されたす。 開発䞭なので、ダミヌモゞュヌル movie-monad/src/dev/Paths_movie_monad.hs を䜿甚しおmovie-monad/src/data/gui.glade movie-monad/src/dev/Paths_movie_monad.hs movie-monad/src/data/gui.gladeを怜玢しmovie-monad/src/data/gui.glade 。 プロゞェクトをビルド/むンストヌルするず、実際のPaths_movie_monadモゞュヌルが自動的に生成されたす。 getDataFileName関数が提䟛されたす。 出力デヌタに、 data-dir (movie-monad/src/) data-filesコピヌたたはむンストヌルされる絶察パスの圢匏でプレフィックスを割り圓おdata-dir (movie-monad/src/) data-files 。


 {-# LANGUAGE OverloadedStrings #-} module Paths_movie_monad where dataDir :: String dataDir = "./src/" getDataFileName :: FilePath -> IO FilePath getDataFileName a = do putStrLn "You are using a fake Paths_movie_monad." return (dataDir ++ "/" ++ a) 

ダミヌモゞュヌルPaths_movie_monad


 {-# LANGUAGE CPP #-} {-# OPTIONS_GHC -fno-warn-missing-import-lists #-} {-# OPTIONS_GHC -fno-warn-implicit-prelude #-} module Paths_movie_monad ( version, getBinDir, getLibDir, getDynLibDir, getDataDir, getLibexecDir, getDataFileName, getSysconfDir ) where import qualified Control.Exception as Exception import Data.Version (Version(..)) import System.Environment (getEnv) import Prelude #if defined(VERSION_base) #if MIN_VERSION_base(4,0,0) catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a #else catchIO :: IO a -> (Exception.Exception -> IO a) -> IO a #endif #else catchIO :: IO a -> (Exception.IOException -> IO a) -> IO a #endif catchIO = Exception.catch version :: Version version = Version [0,0,0,0] [] bindir, libdir, dynlibdir, datadir, libexecdir, sysconfdir :: FilePath bindir = "/home/<snip>/.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/bin" libdir = "/home/<snip>/.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/lib/x86_64-linux-ghc-8.0.2/movie-monad-0.0.0.0" dynlibdir = "/home/<snip>/.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/lib/x86_64-linux-ghc-8.0.2" datadir = "/home/<snip>/.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/share/x86_64-linux-ghc-8.0.2/movie-monad-0.0.0.0" libexecdir = "/home/<snip>/.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/libexec" sysconfdir = "/home/<snip>/.stack-work/install/x86_64-linux-nopie/lts-9.1/8.0.2/etc" getBinDir, getLibDir, getDynLibDir, getDataDir, getLibexecDir, getSysconfDir :: IO FilePath getBinDir = catchIO (getEnv "movie_monad_bindir") (\_ -> return bindir) getLibDir = catchIO (getEnv "movie_monad_libdir") (\_ -> return libdir) getDynLibDir = catchIO (getEnv "movie_monad_dynlibdir") (\_ -> return dynlibdir) getDataDir = catchIO (getEnv "movie_monad_datadir") (\_ -> return datadir) getLibexecDir = catchIO (getEnv "movie_monad_libexecdir") (\_ -> return libexecdir) getSysconfDir = catchIO (getEnv "movie_monad_sysconfdir") (\_ -> return sysconfdir) getDataFileName :: FilePath -> IO FilePath getDataFileName name = do dir <- getDataDir return (dir ++ "/" ++ name) 

自動生成モゞュヌルPaths_movie_monad 。


Main.hs


Main.hsは、Movie Monadの゚ントリポむントです。 このファむルでは、異なるりィゞェットを䜿甚しおりィンドりを構成し、GStreamerを接続し、ナヌザヌが終了するずりィンドりを砎壊したす。


プラグマ


オヌバヌロヌドされた文字列ずレキシカルスコヌプの型倉数が必芁であるこずをコンパむラヌGHCに䌝える必芁がありたす。


OverloadedStringsを䜿甚するず、 String/[Char]たたはTextが必芁な堎所で、 String/[Char]列リテラル "Literal" を䜿甚できたす。 ScopedTypeVariablesを䜿甚するず、ExifToolが呌び出されたずきにむンタヌセプトするために枡されるラムダ関数のパラメヌタヌパタヌンで型シグネチャを䜿甚できたす。


 {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} 

茞入品


 module Main where import Prelude import Foreign.C.Types import System.Process import System.Exit import Control.Monad import Control.Exception import Text.Read import Data.IORef import Data.Maybe import Data.Int import Data.Text import Data.GI.Base import Data.GI.Base.Signals import Data.GI.Base.Properties import GI.GLib import GI.GObject import qualified GI.Gtk import GI.Gst import GI.GstVideo import GI.Gdk import GI.GdkX11 import Paths_movie_monad 

Cバむンディングを䜿甚するため、この蚀語に既に存圚する型を䜿甚する必芁がありたす。 むンポヌトの倧郚分は、haskell-giによっお生成されたバむンディングです。


IsVideoOverlay


GStreamerビデオgi-gstvideo  gi-gstvideo には、タむプむンタヌフェむス IsVideoOverlayクラスが含たれおいたす。 GStreamerバむンディング gi-gst には芁玠タむプが含たれたす。 playbin関数でplaybin芁玠を䜿甚するには、 playbin型型むンスタンス IsVideoOverlayむンスタンスを宣蚀する必芁がありたす。 たた、C偎では、 playbinはVideoOverlayむンタヌフェむスを実装したす。


 newtype GstElement = GstElement GI.Gst.Element instance GI.GstVideo.IsVideoOverlay GstElement 

haskell-giバむンディングの倖郚でむンスタンスを宣蚀する際に、倱われた孀立したむンスタンスの出珟を避けるために、 GI.Gst.Elementを新しい型newtypeでラップするこずに泚意しおください。


メむン


Mainは私たちの最倧の機胜です。 その䞭で、すべおのGUIりィゞェットを初期化し、特定のむベントに基づいおコヌルバックプロシヌゞャを定矩したす。


 main :: IO () main = do 

GI初期化


  _ <- GI.Gst.init Nothing _ <- GI.Gtk.init Nothing 

ここで、GStreamerずGTK +を初期化したした。


GUIりィゞェットの構築


  gladeFile <- getDataFileName "data/gui.glade" builder <- GI.Gtk.builderNewFromFile (pack gladeFile) window <- builderGetObject GI.Gtk.Window builder "window" fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button" drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area" seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale" onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch" volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button" desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box" fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button" errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog" aboutButton <- builderGetObject GI.Gtk.Button builder "about-button" aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog" 

すでに述べたように、すべおのGUIりィゞェットを蚘述するdata/gui.glade XMLファむルぞの絶察パスを取埗したす。 次に、このファむルからコンストラクタヌを䜜成し、りィゞェットを取埗したす。 Gladeを䜿甚しおいなかった堎合は、手動で䜜成する必芁があり、かなり面倒です。


プレむビン


  playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer") 

ここでは、 playbin GStreamerパむプラむンを䜜成したす。 さたざたなニヌズを解決するように蚭蚈されおおり、独自のコンベアを䜜成する時間を節玄できたす。 この芁玠をMultimediaPlayer呌びたす。


GStreamer出力の埋め蟌み


GTK +ずGStreamerを連​​携させるには、GStreamerにビデオの正確な出力先を指瀺する必芁がありたす。 これを行わないず、 playbinを䜿甚するため、GStreamerは独自のりィンドりを䜜成したす。


  _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton -- ... onDrawingAreaRealize :: GI.Gtk.Widget -> GI.Gst.Element -> GI.Gtk.Button -> GI.Gtk.WidgetRealizeCallback onDrawingAreaRealize drawingArea playbin fullscreenButton = do gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow xid <- GI.GdkX11.x11WindowGetXid x11Window let xid' = fromIntegral xid :: CUIntPtr GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid' GI.Gtk.widgetHide fullscreenButton 

drawingAreaりィゞェットのdrawingAreaがdrawingAreaたずきのコヌルバックのセットアップが衚瀺されたす。 GStreamerがビデオを衚瀺するのはこのりィゞェットです。 レンダヌ゚リアりィゞェットの芪GDKりィンドりを取埗したす。 次に、りィンドりハンドラヌ、たたはGTK +りィンドりのX11システムのXIDを取埗したす。 文字列CUIntPtrはIDをCULongからCULongに倉換したす。これはCUIntPtrに必芁です。 正しい型を受け取っplaybin 、 xid'ハンドラヌの助けを借りお、りィンドりにplaybinの出力を描画できるこずをplaybinせたす。


Gladeのバグにより、プログラムでフルスクリヌンりィゞェットを非衚瀺にしたす。Gladeの衚瀺ボックスをオフにしおも、りィゞェットは非衚瀺にならないためです。


Xシステムではなく他のシステムを䜿甚しおいる堎合は、ここでMovie Monadをりィンドりシステムで動䜜するように調敎する必芁がありたす。


ファむル遞択


  _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $ onFileChooserButtonFileSet playbin fileChooserButton volumeButton isWindowFullScreenRef desiredVideoWidthComboBox onOffSwitch fullscreenButton drawingArea window errorMessageDialog -- ... onFileChooserButtonFileSet :: GI.Gst.Element -> GI.Gtk.FileChooserButton -> GI.Gtk.VolumeButton -> IORef Bool -> GI.Gtk.ComboBoxText -> GI.Gtk.Switch -> GI.Gtk.Button -> GI.Gtk.Widget -> GI.Gtk.Window -> GI.Gtk.MessageDialog -> GI.Gtk.FileChooserButtonFileSetCallback onFileChooserButtonFileSet playbin fileChooserButton volumeButton isWindowFullScreenRef desiredVideoWidthComboBox onOffSwitch fullscreenButton drawingArea window errorMessageDialog = do _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton setPlaybinUriAndVolume playbin filename volumeButton isWindowFullScreen <- readIORef isWindowFullScreenRef desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox maybeWindowSize <- getWindowSize desiredVideoWidth filename case maybeWindowSize of Nothing -> do _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused GI.Gtk.windowUnfullscreen window GI.Gtk.switchSetActive onOffSwitch False GI.Gtk.widgetHide fullscreenButton GI.Gtk.widgetShow desiredVideoWidthComboBox resetWindowSize desiredVideoWidth fileChooserButton drawingArea window _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog) void $ GI.Gtk.dialogRun errorMessageDialog Just (width, height) -> do _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying GI.Gtk.switchSetActive onOffSwitch True GI.Gtk.widgetShow fullscreenButton unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window 

ビデオ再生セッションを開始するには、ナヌザヌはビデオファむルを遞択できる必芁がありたす。 ファむルを遞択した埌、すべおが正垞に機胜するために必芁ないく぀かのアクションを実行する必芁がありたす。



䞀時停止しお再生


  _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin) -- ... onSwitchStateSet :: GI.Gst.Element -> Bool -> IO Bool onSwitchStateSet playbin switchOn = do if switchOn then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused return switchOn 

すべおがシンプルです。 スむッチが「オン」の䜍眮にある堎合、 playbin芁玠をplaybin状態に蚭定したす。 それ以倖の堎合は、䞀時停止状態を䞎えたす。


音量蚭定


  _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin) -- ... onScaleButtonValueChanged :: GI.Gst.Element -> Double -> IO () onScaleButtonValueChanged playbin volume = void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume 

りィゞェットの音量レベルが倉曎されるず、その倀をGStreamerに枡しお、再生音量を調敎できるようにしたす。


ビデオナビゲヌション


  seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale) -- ... onRangeValueChanged :: GI.Gst.Element -> GI.Gtk.Scale -> IO () onRangeValueChanged playbin seekScale = do (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime when couldQueryDuration $ do percentage' <- GI.Gtk.rangeGetValue seekScale let percentage = percentage' / 100.0 let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64 void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position 

Movie Monadには、スラむダヌを前埌に移動しおビデオフレヌムを移動できる再生バヌがありたす。


0〜100のスケヌルは、ビデオファむルの合蚈時間を衚したす。 たずえば、スラむダヌを50に移動するず、開始ず終了の䞭間にあるタむムスタンプに移動したす。 スケヌルをれロからビデオの長さたで調敎できたすが、説明した方法はより䞀般的です。


このコヌルバックでは、埌で必芁になるため、シグナルID seekScaleHandlerId を䜿甚するこずに泚意しおください。


再生バヌの曎新


  _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId) -- ... updateSeekScale :: GI.Gst.Element -> GI.Gtk.Scale -> Data.GI.Base.Signals.SignalHandlerId -> IO Bool updateSeekScale playbin seekScale seekScaleHandlerId = do (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime let percentage = if couldQueryDuration && couldQueryPosition && duration > 0 then 100.0 * (fromIntegral position / fromIntegral duration :: Double) else 0.0 GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId GI.Gtk.rangeSetValue seekScale percentage GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId return True 

スケヌルずビデオ再生プロセスを同期するには、GTK +ずGStreamerの間でメッセヌゞを転送する必芁がありたす。 毎秒、珟圚の再生䜍眮を芁求し、それに応じおスケヌルを曎新したす。 そのため、ファむルのどの郚分がすでに衚瀺されおいるかをナヌザヌに瀺し、スラむダヌは垞に実際の再生䜍眮に察応したす。


以前に構成されたコヌルバックを開始しないために、再生バヌを曎新するずきにonRangeValueChangedシグナルonRangeValueChangedを無効にしたす。 onRangeValueChanged onRangeValueChangedは、 ナヌザヌがスラむダヌの䜍眮を倉曎した堎合にのみ実行する必芁がありたす 。


ビデオのサむズを倉曎する


  _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $ onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window -- ... onComboBoxChanged :: GI.Gtk.FileChooserButton -> GI.Gtk.ComboBoxText -> GI.Gtk.Widget -> GI.Gtk.Window -> IO () onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window = do filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton let filename = fromMaybe "" filename' desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox maybeWindowSize <- getWindowSize desiredVideoWidth filename case maybeWindowSize of Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window 

このりィゞェットを䜿甚するず、ナヌザヌは目的のビデオ幅を遞択できたす。 高さは、ビデオファむルのアスペクト比に基づいお自動的に遞択されたす。


党画面モヌド


  _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window) -- ... onFullscreenButtonRelease :: IORef Bool -> GI.Gtk.ComboBoxText -> GI.Gtk.FileChooserButton -> GI.Gtk.Window -> GI.Gdk.EventButton -> IO Bool onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window _ = do isWindowFullScreen <- readIORef isWindowFullScreenRef if isWindowFullScreen then do GI.Gtk.widgetShow desiredVideoWidthComboBox GI.Gtk.widgetShow fileChooserButton void $ GI.Gtk.windowUnfullscreen window else do GI.Gtk.widgetHide desiredVideoWidthComboBox GI.Gtk.widgetHide fileChooserButton void $ GI.Gtk.windowFullscreen window return True 

ナヌザヌが党画面モヌドりィゞェットのボタンを離すず、りィンドりの党画面モヌドの状態が切り替わり、ファむル遞択パネルずビデオ幅遞択りィゞェットが非衚瀺になりたす。 党画面モヌドを終了するず、パネルずりィゞェットが埩元されたす。


ビデオがない堎合、フルスクリヌンりィゞェットは衚瀺されないこずに泚意しおください。


  _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef) -- ... onWidgetWindowStateEvent :: IORef Bool -> GI.Gdk.EventWindowState -> IO Bool onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates writeIORef isWindowFullScreenRef isWindowFullScreen return True 

りィンドりのフルスクリヌン状態を制埡するには、りィンドりの状態が倉わるたびに開始するようにコヌルバックを構成する必芁がありたす。 さたざたなコヌルバックは、りィンドりのフルスクリヌン状態に関する情報に䟝存しおいたす。 IORefを補助ずしお䜿甚し、そこから各関数が読み取り、コヌルバックが曞き蟌たれたす。 このIORefは可倉および汎甚リンクです。 理想的には、フルスクリヌンモヌドのずきにりィンドりを正確に芁求する必芁がありたすが、このためのAPIはありたせん。 したがっお、可倉リンクを䜿甚したす。


メむンスレッドで1぀のラむタヌずヒヌプのシグナルコヌルバックを䜿甚するこずにより、䞀般的な可倉状態のトラップを回避できたす。 実行スレッドの安党性が心配な堎合は、代わりにMVar 、 TVarたたはatomicModifyIORef䜿甚できたす。


プログラムに぀いお


  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog) -- ... onAboutButtonRelease :: GI.Gtk.AboutDialog -> GI.Gdk.EventButton -> IO Bool onAboutButtonRelease aboutDialog _ = do _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog) _ <- GI.Gtk.dialogRun aboutDialog return True 

問題の最埌のりィゞェットは、Aboutダむアログです。 ここでは、ダむアログボックスをメむンりィンドりに衚瀺される[About]ボタンに関連付けたす。


りィンドりを閉じる


  _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin) -- ... onWindowDestroy :: GI.Gst.Element -> IO () onWindowDestroy playbin = do _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull _ <- GI.Gst.objectUnref playbin GI.Gtk.mainQuit 

ナヌザヌがりィンドりを閉じるず、 playbinパむプラむンが砎棄され、メむンのGTKルヌプが終了したす。


打ち䞊げ


  GI.Gtk.widgetShowAll window GI.Gtk.main 

最埌に、メむンりィンドりを衚瀺たたは描画し、メむンのGTK +サむクルを開始したす。 mainQuitたでブロックしたす。


完党なMain.hsファむル


以䞋は、 movie-monad/src/Main.hs 。 main関連するさたざたなヘルパヌ関数は衚瀺されおいたせん。


 {- Movie Monad (C) 2017 David lettier lettier.com -} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Main where import Prelude import Foreign.C.Types import System.Process import System.Exit import Control.Monad import Control.Exception import Text.Read import Data.IORef import Data.Maybe import Data.Int import Data.Text import Data.GI.Base import Data.GI.Base.Signals import Data.GI.Base.Properties import GI.GLib import GI.GObject import qualified GI.Gtk import GI.Gst import GI.GstVideo import GI.Gdk import GI.GdkX11 import Paths_movie_monad -- Declare Element a type instance of IsVideoOverlay via a newtype wrapper -- Our GStreamer element is playbin -- Playbin implements the GStreamer VideoOverlay interface newtype GstElement = GstElement GI.Gst.Element instance GI.GstVideo.IsVideoOverlay GstElement main :: IO () main = do _ <- GI.Gst.init Nothing _ <- GI.Gtk.init Nothing gladeFile <- getDataFileName "data/gui.glade" builder <- GI.Gtk.builderNewFromFile (pack gladeFile) window <- builderGetObject GI.Gtk.Window builder "window" fileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "file-chooser-button" drawingArea <- builderGetObject GI.Gtk.Widget builder "drawing-area" seekScale <- builderGetObject GI.Gtk.Scale builder "seek-scale" onOffSwitch <- builderGetObject GI.Gtk.Switch builder "on-off-switch" volumeButton <- builderGetObject GI.Gtk.VolumeButton builder "volume-button" desiredVideoWidthComboBox <- builderGetObject GI.Gtk.ComboBoxText builder "desired-video-width-combo-box" fullscreenButton <- builderGetObject GI.Gtk.Button builder "fullscreen-button" errorMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "error-message-dialog" aboutButton <- builderGetObject GI.Gtk.Button builder "about-button" aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog" playbin <- fromJust <$> GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayer") isWindowFullScreenRef <- newIORef False _ <- GI.Gtk.onWidgetRealize drawingArea $ onDrawingAreaRealize drawingArea playbin fullscreenButton _ <- GI.Gtk.onFileChooserButtonFileSet fileChooserButton $ onFileChooserButtonFileSet playbin fileChooserButton volumeButton isWindowFullScreenRef desiredVideoWidthComboBox onOffSwitch fullscreenButton drawingArea window errorMessageDialog _ <- GI.Gtk.onSwitchStateSet onOffSwitch (onSwitchStateSet playbin) _ <- GI.Gtk.onScaleButtonValueChanged volumeButton (onScaleButtonValueChanged playbin) seekScaleHandlerId <- GI.Gtk.onRangeValueChanged seekScale (onRangeValueChanged playbin seekScale) _ <- GI.GLib.timeoutAddSeconds GI.GLib.PRIORITY_DEFAULT 1 (updateSeekScale playbin seekScale seekScaleHandlerId) _ <- GI.Gtk.onComboBoxChanged desiredVideoWidthComboBox $ onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window _ <- GI.Gtk.onWidgetButtonReleaseEvent fullscreenButton (onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window) _ <- GI.Gtk.onWidgetWindowStateEvent window (onWidgetWindowStateEvent isWindowFullScreenRef) _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton (onAboutButtonRelease aboutDialog) _ <- GI.Gtk.onWidgetDestroy window (onWindowDestroy playbin) GI.Gtk.widgetShowAll window GI.Gtk.main builderGetObject :: (GI.GObject.GObject b, GI.Gtk.IsBuilder a) => (Data.GI.Base.ManagedPtr b -> b) -> a -> Prelude.String -> IO b builderGetObject objectTypeClass builder objectId = fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>= GI.Gtk.unsafeCastTo objectTypeClass onDrawingAreaRealize :: GI.Gtk.Widget -> GI.Gst.Element -> GI.Gtk.Button -> GI.Gtk.WidgetRealizeCallback onDrawingAreaRealize drawingArea playbin fullscreenButton = do gdkWindow <- fromJust <$> GI.Gtk.widgetGetWindow drawingArea x11Window <- GI.Gtk.unsafeCastTo GI.GdkX11.X11Window gdkWindow xid <- GI.GdkX11.x11WindowGetXid x11Window let xid' = fromIntegral xid :: CUIntPtr GI.GstVideo.videoOverlaySetWindowHandle (GstElement playbin) xid' GI.Gtk.widgetHide fullscreenButton onFileChooserButtonFileSet :: GI.Gst.Element -> GI.Gtk.FileChooserButton -> GI.Gtk.VolumeButton -> IORef Bool -> GI.Gtk.ComboBoxText -> GI.Gtk.Switch -> GI.Gtk.Button -> GI.Gtk.Widget -> GI.Gtk.Window -> GI.Gtk.MessageDialog -> GI.Gtk.FileChooserButtonFileSetCallback onFileChooserButtonFileSet playbin fileChooserButton volumeButton isWindowFullScreenRef desiredVideoWidthComboBox onOffSwitch fullscreenButton drawingArea window errorMessageDialog = do _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull filename <- fromJust <$> GI.Gtk.fileChooserGetFilename fileChooserButton setPlaybinUriAndVolume playbin filename volumeButton isWindowFullScreen <- readIORef isWindowFullScreenRef desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox maybeWindowSize <- getWindowSize desiredVideoWidth filename case maybeWindowSize of Nothing -> do _ <- GI.Gst.elementSetState playbin GI.Gst.StatePaused GI.Gtk.windowUnfullscreen window GI.Gtk.switchSetActive onOffSwitch False GI.Gtk.widgetHide fullscreenButton GI.Gtk.widgetShow desiredVideoWidthComboBox resetWindowSize desiredVideoWidth fileChooserButton drawingArea window _ <- GI.Gtk.onDialogResponse errorMessageDialog (\ _ -> GI.Gtk.widgetHide errorMessageDialog) void $ GI.Gtk.dialogRun errorMessageDialog Just (width, height) -> do _ <- GI.Gst.elementSetState playbin GI.Gst.StatePlaying GI.Gtk.switchSetActive onOffSwitch True GI.Gtk.widgetShow fullscreenButton unless isWindowFullScreen $ setWindowSize width height fileChooserButton drawingArea window onSwitchStateSet :: GI.Gst.Element -> Bool -> IO Bool onSwitchStateSet playbin switchOn = do if switchOn then void $ GI.Gst.elementSetState playbin GI.Gst.StatePlaying else void $ GI.Gst.elementSetState playbin GI.Gst.StatePaused return switchOn onScaleButtonValueChanged :: GI.Gst.Element -> Double -> IO () onScaleButtonValueChanged playbin volume = void $ Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume onRangeValueChanged :: GI.Gst.Element -> GI.Gtk.Scale -> IO () onRangeValueChanged playbin seekScale = do (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime when couldQueryDuration $ do percentage' <- GI.Gtk.rangeGetValue seekScale let percentage = percentage' / 100.0 let position = fromIntegral (round ((fromIntegral duration :: Double) * percentage) :: Int) :: Int64 void $ GI.Gst.elementSeekSimple playbin GI.Gst.FormatTime [ GI.Gst.SeekFlagsFlush ] position updateSeekScale :: GI.Gst.Element -> GI.Gtk.Scale -> Data.GI.Base.Signals.SignalHandlerId -> IO Bool updateSeekScale playbin seekScale seekScaleHandlerId = do (couldQueryDuration, duration) <- GI.Gst.elementQueryDuration playbin GI.Gst.FormatTime (couldQueryPosition, position) <- GI.Gst.elementQueryPosition playbin GI.Gst.FormatTime let percentage = if couldQueryDuration && couldQueryPosition && duration > 0 then 100.0 * (fromIntegral position / fromIntegral duration :: Double) else 0.0 GI.GObject.signalHandlerBlock seekScale seekScaleHandlerId GI.Gtk.rangeSetValue seekScale percentage GI.GObject.signalHandlerUnblock seekScale seekScaleHandlerId return True onComboBoxChanged :: GI.Gtk.FileChooserButton -> GI.Gtk.ComboBoxText -> GI.Gtk.Widget -> GI.Gtk.Window -> IO () onComboBoxChanged fileChooserButton desiredVideoWidthComboBox drawingArea window = do filename' <- GI.Gtk.fileChooserGetFilename fileChooserButton let filename = fromMaybe "" filename' desiredVideoWidth <- getDesiredVideoWidth desiredVideoWidthComboBox maybeWindowSize <- getWindowSize desiredVideoWidth filename case maybeWindowSize of Nothing -> resetWindowSize desiredVideoWidth fileChooserButton drawingArea window Just (width, height) -> setWindowSize width height fileChooserButton drawingArea window onFullscreenButtonRelease :: IORef Bool -> GI.Gtk.ComboBoxText -> GI.Gtk.FileChooserButton -> GI.Gtk.Window -> GI.Gdk.EventButton -> IO Bool onFullscreenButtonRelease isWindowFullScreenRef desiredVideoWidthComboBox fileChooserButton window _ = do isWindowFullScreen <- readIORef isWindowFullScreenRef if isWindowFullScreen then do GI.Gtk.widgetShow desiredVideoWidthComboBox GI.Gtk.widgetShow fileChooserButton void $ GI.Gtk.windowUnfullscreen window else do GI.Gtk.widgetHide desiredVideoWidthComboBox GI.Gtk.widgetHide fileChooserButton void $ GI.Gtk.windowFullscreen window return True onWidgetWindowStateEvent :: IORef Bool -> GI.Gdk.EventWindowState -> IO Bool onWidgetWindowStateEvent isWindowFullScreenRef eventWindowState = do windowStates <- GI.Gdk.getEventWindowStateNewWindowState eventWindowState let isWindowFullScreen = Prelude.foldl (\ acc x -> acc || GI.Gdk.WindowStateFullscreen == x) False windowStates writeIORef isWindowFullScreenRef isWindowFullScreen return True onAboutButtonRelease :: GI.Gtk.AboutDialog -> GI.Gdk.EventButton -> IO Bool onAboutButtonRelease aboutDialog _ = do _ <- GI.Gtk.onDialogResponse aboutDialog (\ _ -> GI.Gtk.widgetHide aboutDialog) _ <- GI.Gtk.dialogRun aboutDialog return True onWindowDestroy :: GI.Gst.Element -> IO () onWindowDestroy playbin = do _ <- GI.Gst.elementSetState playbin GI.Gst.StateNull _ <- GI.Gst.objectUnref playbin GI.Gtk.mainQuit setPlaybinUriAndVolume :: GI.Gst.Element -> Prelude.String -> GI.Gtk.VolumeButton -> IO () setPlaybinUriAndVolume playbin filename volumeButton = do let uri = "file://" ++ filename volume <- GI.Gtk.scaleButtonGetValue volumeButton Data.GI.Base.Properties.setObjectPropertyDouble playbin "volume" volume Data.GI.Base.Properties.setObjectPropertyString playbin "uri" (Just $ pack uri) getVideoInfo :: Prelude.String -> Prelude.String -> IO (Maybe Prelude.String) getVideoInfo flag filename = do (code, out, _) <- catch ( readProcessWithExitCode "exiftool" [flag, "-s", "-S", filename] "" ) (\ (_ :: Control.Exception.IOException) -> return (ExitFailure 1, "", "")) if code == System.Exit.ExitSuccess then return (Just out) else return Nothing isVideo :: Prelude.String -> IO Bool isVideo filename = do maybeOut <- getVideoInfo "-MIMEType" filename case maybeOut of Nothing -> return False Just out -> return ("video" `isInfixOf` pack out) getWindowSize :: Int -> Prelude.String -> IO (Maybe (Int32, Int32)) getWindowSize desiredVideoWidth filename = isVideo filename >>= getWidthHeightString >>= splitWidthHeightString >>= widthHeightToDouble >>= ratio >>= windowSize where getWidthHeightString :: Bool -> IO (Maybe Prelude.String) getWidthHeightString False = return Nothing getWidthHeightString True = getVideoInfo "-ImageSize" filename splitWidthHeightString :: Maybe Prelude.String -> IO (Maybe [Text]) splitWidthHeightString Nothing = return Nothing splitWidthHeightString (Just string) = return (Just (Data.Text.splitOn "x" (pack string))) widthHeightToDouble :: Maybe [Text] -> IO (Maybe Double, Maybe Double) widthHeightToDouble (Just (x:y:_)) = return (readMaybe (unpack x) :: Maybe Double, readMaybe (unpack y) :: Maybe Double) widthHeightToDouble _ = return (Nothing, Nothing) ratio :: (Maybe Double, Maybe Double) -> IO (Maybe Double) ratio (Just width, Just height) = if width <= 0.0 then return Nothing else return (Just (height / width)) ratio _ = return Nothing windowSize :: Maybe Double -> IO (Maybe (Int32, Int32)) windowSize Nothing = return Nothing windowSize (Just ratio') = return (Just (fromIntegral desiredVideoWidth :: Int32, round ((fromIntegral desiredVideoWidth :: Double) * ratio') :: Int32)) getDesiredVideoWidth :: GI.Gtk.ComboBoxText -> IO Int getDesiredVideoWidth = fmap (\ x -> read (Data.Text.unpack x) :: Int) . GI.Gtk.comboBoxTextGetActiveText setWindowSize :: Int32 -> Int32 -> GI.Gtk.FileChooserButton -> GI.Gtk.Widget -> GI.Gtk.Window -> IO () setWindowSize width height fileChooserButton drawingArea window = do GI.Gtk.setWidgetWidthRequest fileChooserButton width GI.Gtk.setWidgetWidthRequest drawingArea width GI.Gtk.setWidgetHeightRequest drawingArea height GI.Gtk.setWidgetWidthRequest window width GI.Gtk.setWidgetHeightRequest window height GI.Gtk.windowResize window width (if height <= 0 then 1 else height) resetWindowSize :: (Integral a) => a -> GI.Gtk.FileChooserButton -> GI.Gtk.Widget -> GI.Gtk.Window -> IO () resetWindowSize width' fileChooserButton drawingArea window = do let width = fromIntegral width' :: Int32 GI.Gtk.widgetQueueDraw drawingArea setWindowSize width 0 fileChooserButton drawingArea window 

Movie Monad


, Movie Monad .


 cd movie-monad/ stack clean stack install stack exec -- movie-monad # Or just `movie-monad` if `stack path | grep local-bin-path` is in your `echo $PATH` 

, Movie Monad .


おわりに


Movie Monad , GTK+ GStreamer. , Electron-. Movie Monad .


GTK+ . , GTK+ ~50 , Electron — ~300 (500%- ).


, GTK+ . , Electron - . haskell-gi .


, GTK+ Haskell, Gifcurry . .



Source: https://habr.com/ru/post/J338176/


All Articles