ffmpeg-light 0.8.2 → 0.9.0
raw patch · 8 files changed
+178/−51 lines, 8 filesdep +time
Dependencies added: time
Files
- CHANGELOG.md +22/−24
- demo/Main.hs +33/−2
- ffmpeg-light.cabal +3/−3
- src/Codec/FFmpeg.hs +2/−1
- src/Codec/FFmpeg/Decode.hs +52/−16
- src/Codec/FFmpeg/Juicy.hs +4/−4
- src/Codec/FFmpeg/Probe.hsc +1/−1
- src/Codec/FFmpeg/Types.hsc +61/−0
CHANGELOG.md view
@@ -1,15 +1,22 @@-0.8.2----+# 0.9.0 +* Add support for camera input (Thomas M. DuBuisson)+ * Try it: build the demo executable (`cabal configure -fBuildDemo`)+ and run `cabal run demo -- cam` to record 10s of video from a+ connected camera to an output file `camera.mov`.++* Extract frame time stamps from the video stream rather than the+ codec context (hat tip to Jaro Reinders)++# 0.8.2+ * Added probe features -0.8.1----+# 0.8.1 * Update raster demo to use new JuicyPixels-3.2 API -0.8----+# 0.8 * Update to transformers-0.4.1 and mtl-2.2.1 * Changed decode-related types to accomodate deprecation of the@@ -30,8 +37,7 @@ * Rasterific bump * Rasterific exports its own linear algebra types as of 0.3 -0.7.1----+# 0.7.1 * Bumped transformers dependency @@ -43,26 +49,22 @@ * Added BGRA pixel format -0.7----+# 0.7 * Simplified top-level API to focus on JuicyPixels-based interface -0.6----+# 0.6 * Cleaned the API of detritus. Use the image* functions. -0.5----+# 0.5 * Juiced the Encode and Decode APIs. Using `imageWriter` and `imageReader` provides a degree of pixel format polymorphism based on JuicyPixels pixel types. -0.4----+# 0.4 * Fixed corrupted output of palettized animated GIFs. @@ -79,20 +81,17 @@ * See the `demo/Raster.hs` for examples. -0.3.1----+# 0.3.1 * Automatically palettize RGB24 to RGB8 for GIF output. * Add a Rasterific demo program that records an animation. -0.3----+# 0.3 * Support for GIF encoding (and other palletized formats). -0.2----+# 0.2 * Separate `Scaler` module and friendly `libswscaler` interface. @@ -100,7 +99,6 @@ * Added demo program. -0.1----+# 0.1 * Basic h264 encoding and decoding.
demo/Main.hs view
@@ -2,12 +2,14 @@ import Codec.Picture import Control.Applicative import Control.Monad (replicateM_)+import qualified Data.Time.Clock as C import qualified Data.Vector.Storable as V import System.Environment+import Control.Monad (unless) -- The example used in the README firstFrame :: IO (Maybe DynamicImage)-firstFrame = do (getFrame, cleanup) <- imageReader "myVideo.mov"+firstFrame = do (getFrame, cleanup) <- imageReader (File "myVideo.mov") (fmap ImageRGB8 <$> getFrame) <* cleanup -- | Generate a video that pulses from light to dark.@@ -37,7 +39,7 @@ testDecode :: FilePath -> IO () testDecode vidFile = do initFFmpeg- (getFrame, cleanup) <- imageReaderTime vidFile+ (getFrame, cleanup) <- imageReaderTime (File vidFile) frame1 <- getFrame case frame1 of Just (avf,ts) -> do putStrLn $ "Frame at "++show ts@@ -52,10 +54,39 @@ cleanup putStrLn "All done!" +-- | @loopFor timeSpan action@ repeats @action@ until at least @timeSpan@+-- seconds have elapsed.+loopFor :: Double -> IO () -> IO ()+loopFor time m =+ do start <- C.getCurrentTime+ let go = do m+ now <- C.getCurrentTime+ unless (realToFrac (C.diffUTCTime now start) >= time) go+ go++testCamera :: IO ()+testCamera =+ do initFFmpeg+ (getFrame, cleanup) <- imageReader (Camera "0:0")+ frame1 <- getFrame+ case frame1 of+ img@(Just (Image w h _)) ->+ do let [w',h'] = map fromIntegral [w,h]+ writeFrame <- imageWriter (defaultParams w' h') "camera.mov"+ writeFrame (img :: Maybe (Image PixelRGB8))+ let go = getFrame >>= writeFrame+ loopFor 10 go+ writeFrame Nothing+ _ -> putStrLn "Couldn't read the first frame from the camera"+ cleanup+ main :: IO () main = do args <- getArgs case args of [] -> testEncode+ [s]+ | s `elem` ["--help", "-help", "-h"] -> error usage + | s == "cam" -> testCamera [vidFile] -> testDecode vidFile _ -> error usage where usage =
ffmpeg-light.cabal view
@@ -1,5 +1,5 @@ name: ffmpeg-light-version: 0.8.2+version: 0.9.0 synopsis: Minimal bindings to the FFmpeg library. description: Stream frames from an encoded video, or stream frames to@@ -62,7 +62,7 @@ transformers >= 0.4.1 && < 0.5, mtl >= 2.2.1 && < 2.3, JuicyPixels >= 3.1 && < 3.3- pkgconfig-depends: libavutil, libavformat, libavcodec, libswscale+ pkgconfig-depends: libavutil, libavformat, libavcodec, libswscale, libavdevice hs-source-dirs: src include-dirs: src default-language: Haskell2010@@ -73,7 +73,7 @@ buildable: False build-depends: base < 5, vector, mtl, transformers, JuicyPixels if flag(BuildDemo)- build-depends: ffmpeg-light+ build-depends: ffmpeg-light, time hs-source-dirs: demo main-is: Main.hs default-language: Haskell2010
src/Codec/FFmpeg.hs view
@@ -18,11 +18,12 @@ import Codec.FFmpeg.Types foreign import ccall "av_register_all" av_register_all :: IO ()+foreign import ccall "avdevice_register_all" avdevice_register_all :: IO () -- foreign import ccall "avcodec_register_all" avcodec_register_all :: IO () -- | Initialize FFmpeg by registering all known codecs. This /must/ -- be called before using other FFmpeg functions. initFFmpeg :: IO ()-initFFmpeg = av_register_all+initFFmpeg = av_register_all >> avdevice_register_all
src/Codec/FFmpeg/Decode.hs view
@@ -25,7 +25,7 @@ foreign import ccall "avformat_open_input" avformat_open_input :: Ptr AVFormatContext -> CString -> Ptr ()- -> Ptr (Ptr ()) -> IO CInt+ -> Ptr AVDictionary -> IO CInt foreign import ccall "avformat_find_stream_info" avformat_find_stream_info :: AVFormatContext -> Ptr () -> IO CInt@@ -58,11 +58,46 @@ foreign import ccall "avformat_close_input" close_input :: Ptr AVFormatContext -> IO () +foreign import ccall "av_dict_set"+ av_dict_set :: Ptr AVDictionary -> CString -> CString -> CInt -> IO CInt++dictSet :: Ptr AVDictionary -> String -> String -> IO ()+dictSet d k v = do+ r <- withCString k $ \k' -> withCString v $ \v' ->+ av_dict_set d k' v' 0+ when (r < 0)+ (error $ "av_dict_set failed("++show r++"): "++k++" => "++v)+ -- * FFmpeg Decoding Interface +-- | Open the first video input device enumerated by FFMPEG.+openCamera :: (MonadIO m, MonadError String m) => String -> m AVFormatContext+openCamera cam =+ wrapIOError . alloca $ \ctx ->+ withCString cam $ \cstr ->+ do avPtr <- mallocAVFormatContext+ setupCamera avPtr cam+ poke ctx avPtr+ r <- alloca $ \dict -> do+ dictSet dict "framerate" "30"+ avformat_open_input ctx cstr nullPtr dict+ when (r /= 0) (fail $ "ffmpeg failed opening file: " ++ show r)+ peek ctx+ where+ setupCamera :: AVFormatContext -> String -> IO ()+ setupCamera avfc c = do+ setCamera avfc+ setFilename avfc c++openInput :: (MonadIO m, MonadError String m) => InputSource -> m AVFormatContext+openInput ipt =+ case ipt of+ File fileName -> openFile fileName+ Camera cam -> openCamera cam+ -- | Open an input media file.-openInput :: (MonadIO m, MonadError String m) => String -> m AVFormatContext-openInput filename = +openFile :: (MonadIO m, MonadError String m) => String -> m AVFormatContext+openFile filename = wrapIOError . alloca $ \ctx -> withCString filename $ \cstr -> do poke (castPtr ctx) nullPtr@@ -94,7 +129,8 @@ -- found, returns the index of the stream in the container, and its -- associated 'AVCodecContext' and 'AVCodec'. findVideoStream :: (MonadIO m, MonadError String m)- => AVFormatContext -> m (CInt, AVCodecContext, AVCodec)+ => AVFormatContext+ -> m (CInt, AVCodecContext, AVCodec, AVStream) findVideoStream fmt = do wrapIOError . alloca $ \codec -> do poke codec (AVCodec nullPtr)@@ -104,7 +140,7 @@ streams <- getStreams fmt vidStream <- peek (advancePtr streams (fromIntegral i)) ctx <- getCodecContext vidStream- return (i, ctx, cod)+ return (i, ctx, cod, vidStream) -- | Find a registered decoder with a codec ID matching that found in -- the given 'AVCodecContext'.@@ -132,11 +168,11 @@ -- | Read frames of the given 'AVPixelFormat' from a video stream. frameReader :: (MonadIO m, MonadError String m)- => AVPixelFormat -> FilePath -> m (IO (Maybe AVFrame), IO ())-frameReader dstFmt fileName =- do inputContext <- openInput fileName+ => AVPixelFormat -> InputSource -> m (IO (Maybe AVFrame), IO ())+frameReader dstFmt ipt =+ do inputContext <- openInput ipt checkStreams inputContext- (vidStreamIndex, ctx, cod) <- findVideoStream inputContext+ (vidStreamIndex, ctx, cod, _vidStream) <- findVideoStream inputContext _ <- openCodec ctx cod prepareReader inputContext vidStreamIndex dstFmt ctx @@ -144,22 +180,22 @@ -- -- > frameReaderT = fmap (first MaybeT) . frameReader frameReaderT :: (Functor m, MonadIO m, MonadError String m)- => FilePath -> m (MaybeT IO AVFrame, IO ())+ => InputSource -> m (MaybeT IO AVFrame, IO ()) frameReaderT = fmap (first MaybeT) . frameReader avPixFmtRgb24 -- | Read time stamped frames of the given 'AVPixelFormat' from a -- video stream. Time is given in seconds from the start of the -- stream. frameReaderTime :: (MonadIO m, MonadError String m)- => AVPixelFormat -> FilePath+ => AVPixelFormat -> InputSource -> m (IO (Maybe (AVFrame, Double)), IO ())-frameReaderTime dstFmt fileName =- do inputContext <- openInput fileName+frameReaderTime dstFmt src =+ do inputContext <- openInput src checkStreams inputContext- (vidStreamIndex, ctx, cod) <- findVideoStream inputContext+ (vidStreamIndex, ctx, cod, vidStream) <- findVideoStream inputContext _ <- openCodec ctx cod (reader, cleanup) <- prepareReader inputContext vidStreamIndex dstFmt ctx- AVRational num den <- liftIO $ getTimeBase ctx+ AVRational num den <- liftIO $ getTimeBase vidStream let (numl, dend) = (fromIntegral num, fromIntegral den) frameTime' frame = do n <- getPts frame@@ -176,7 +212,7 @@ -- -- > frameReaderT = fmap (first MaybeT) . frameReader frameReaderTimeT :: (Functor m, MonadIO m, MonadError String m)- => FilePath -> m (MaybeT IO (AVFrame, Double), IO ())+ => InputSource -> m (MaybeT IO (AVFrame, Double), IO ()) frameReaderTimeT = fmap (first MaybeT) . frameReaderTime avPixFmtRgb24 -- | Construct an action that gets the next available frame, and an
src/Codec/FFmpeg/Juicy.hs view
@@ -104,7 +104,7 @@ imageReaderT :: forall m p. (Functor m, MonadIO m, MonadError String m, JuicyPixelFormat p)- => FilePath -> m (IO (Maybe (Image p)), IO ())+ => InputSource -> m (IO (Maybe (Image p)), IO ()) imageReaderT = fmap (first (runMaybeT . aux toJuicyImage)) . frameReader (juicyPixelFormat ([] :: [p])) where aux g x = MaybeT x >>= MaybeT . g@@ -112,7 +112,7 @@ -- | Read frames from a video stream. Errors are thrown as -- 'IOException's. imageReader :: JuicyPixelFormat p- => FilePath -> IO (IO (Maybe (Image p)), IO ())+ => InputSource -> IO (IO (Maybe (Image p)), IO ()) imageReader = (>>= either error return) . runExceptT . imageReaderT -- | Read time stamped frames from a video stream. Time is given in@@ -120,7 +120,7 @@ imageReaderTimeT :: forall m p. (Functor m, MonadIO m, MonadError String m, JuicyPixelFormat p)- => FilePath -> m (IO (Maybe (Image p, Double)), IO ())+ => InputSource -> m (IO (Maybe (Image p, Double)), IO ()) imageReaderTimeT = fmap (first (runMaybeT . aux toJuicyImage)) . frameReaderTime (juicyPixelFormat ([] :: [p])) where aux g x = do (f,t) <- MaybeT x@@ -131,7 +131,7 @@ -- seconds from the start of the stream. Errors are thrown as -- 'IOException's. imageReaderTime :: JuicyPixelFormat p- => FilePath -> IO (IO (Maybe (Image p, Double)), IO ())+ => InputSource -> IO (IO (Maybe (Image p, Double)), IO ()) imageReaderTime = (>>= either error return) . runExceptT . imageReaderTimeT -- | Open a target file for writing a video stream. When the returned
src/Codec/FFmpeg/Probe.hsc view
@@ -49,7 +49,7 @@ withAvFile :: (MonadMask m, MonadIO m) => String -> AvFormat m a -> m a withAvFile fn f = do- ectx <- runEitherT $ openInput fn+ ectx <- runEitherT $ openFile fn case ectx of Left e -> liftIO $ fail e Right ctx -> finally
src/Codec/FFmpeg/Types.hsc view
@@ -3,6 +3,7 @@ module Codec.FFmpeg.Types where import Codec.FFmpeg.Enums import Control.Applicative+import Control.Monad (zipWithM_,when) import Foreign.C.String (CString) import Foreign.C.Types import Foreign.Ptr@@ -24,12 +25,45 @@ #mkField Streams, (Ptr AVStream) #mkField OutputFormat, AVOutputFormat #mkField IOContext, AVIOContext+#mkField InputFormat, AVInputFormat #hasField AVFormatContext, NumStreams, nb_streams #hasField AVFormatContext, Streams, streams #hasField AVFormatContext, OutputFormat, oformat+#hasField AVFormatContext, InputFormat, iformat #hasField AVFormatContext, IOContext, pb +setFilename :: AVFormatContext -> String -> IO ()+setFilename ctx fn =+ do let ptr = getPtr ctx+ dst = (#ptr AVFormatContext, filename) ptr+ bytes = map (fromIntegral . fromEnum) fn+ zipWithM_ (pokeElemOff dst) bytes [(0 :: CInt) ..]+++foreign import ccall "av_input_video_device_next"+ av_input_video_device_next :: AVInputFormat -> IO AVInputFormat++setCamera :: AVFormatContext -> IO ()+setCamera ctx = do+ ipt <- getCameraAVInputFormat (AVInputFormat nullPtr)+ setInputFormat ctx ipt+ where+ -- Currently straight-line, but we can filter each 'nxt' based on+ -- predicates, such as device ('avfoundtion', 'v4l2' etc) in the+ -- future, if needed.+ getCameraAVInputFormat :: AVInputFormat -> IO AVInputFormat+ getCameraAVInputFormat p = do+ nxt <- av_input_video_device_next p+ when (nullPtr == getPtr nxt) (error "No video input device found.")+ return nxt++foreign import ccall "avformat_alloc_context"+ avformat_alloc_context :: IO (Ptr ())++mallocAVFormatContext :: IO AVFormatContext+mallocAVFormatContext = AVFormatContext <$> avformat_alloc_context+ newtype AVCodecContext = AVCodecContext (Ptr ()) deriving (Storable, HasPtr) #mkField BitRate, CInt@@ -98,6 +132,27 @@ #hasField AVOutputFormat, FormatFlags, flags #hasField AVOutputFormat, VideoCodecID, video_codec +newtype AVInputFormat = AVInputFormat (Ptr ()) deriving (Storable, HasPtr)+newtype AVClass = AVClass (Ptr ()) deriving (Storable, HasPtr)+#mkField AVClass, AVClass+#hasField AVInputFormat, AVClass, priv_class++getAVCategory :: AVInputFormat -> IO Category+getAVCategory aif =+ do c <- getAVClass aif+ if nullPtr == getPtr c+ then return (Category (-1))+ else Category <$> peek ((#ptr AVClass, category) $ castPtr $ getPtr c)++newtype Category = Category CInt deriving (Eq,Ord,Show,Read,Enum)+#enum Category, Category, AV_CLASS_CATEGORY_NA, AV_CLASS_CATEGORY_INPUT,\+ AV_CLASS_CATEGORY_OUTPUT, AV_CLASS_CATEGORY_MUXER, AV_CLASS_CATEGORY_DEMUXER,\+ AV_CLASS_CATEGORY_ENCODER, AV_CLASS_CATEGORY_DECODER, AV_CLASS_CATEGORY_FILTER,\+ AV_CLASS_CATEGORY_BITSTREAM_FILTER, AV_CLASS_CATEGORY_SWSCALER, AV_CLASS_CATEGORY_SWRESAMPLER,\+ AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT, AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT, AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,\+ AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT, AV_CLASS_CATEGORY_DEVICE_OUTPUT, AV_CLASS_CATEGORY_DEVICE_INPUT,\+ AV_CLASS_CATEGORY_NB+ newtype AVIOContext = AVIOContext (Ptr ()) deriving (Storable, HasPtr) newtype AVPacket = AVPacket (Ptr ()) deriving (Storable, HasPtr)@@ -159,3 +214,9 @@ poke ptr (AVFrac v n d) = do (#poke AVFrac, val) ptr v (#poke AVFrac, num) ptr n (#poke AVFrac, den) ptr d++-- | The input source can be a file or a camera. When using 'Camera',+-- frequently in the form @Camera "0:0"@, the first input video device+-- enumerated by libavdevice is selected.+data InputSource = File FilePath | Camera String+ deriving (Eq, Ord, Show, Read)