haskellffmpegglossjuicy-pixels

How to find mp4 metadata with ffmpeg-light in haskell?


I'm using ffmpeg-light, JuicyPixels and gloss to display a video with Haskell. I want to find the metadata of videos I'm playing automatically, but I have not yet found a way to do so.

I would like to access metadata like the resolution and the framerate of the video.

Can you help me?

EDIT:

I have tried your solution @CRDrost, but the video is now playing at 2x normal speed. I assume the function imageReaderTime is giving the wrong timestamps.

EDIT 2:

The abnormal playing speed is a bug in the ffmpeg-light library. I've opened an issue at the github repository.

My updated code:

import Graphics.Gloss
import Codec.FFmpeg
import Codec.FFmpeg.Juicy
import Codec.Picture
import Control.Applicative
import Data.Maybe
import Graphics.Gloss.Juicy
import Control.Monad
-- import System.IO.Unsafe (unsafePerformIO)-- for debugging purposes

resolution :: (Int,Int)
resolution = (640, 360)

frameCount :: Int
frameCount = 100

main :: IO ()
main = do
    initFFmpeg
    (getFrame, cleanup) <- imageReaderTime "big_buck_bunny.mp4"
    frames <- replicateM frameCount $ nextFrame getFrame
    cleanup
    animate (InWindow "Nice Window" resolution (10,10)) white (frameAt frames)

nextFrame :: IO (Maybe (Image PixelRGB8, Double)) -> IO (Picture, Float)
nextFrame getFrame = mapSnd realToFrac . mapFst fromImageRGB8 . fromJust <$> getFrame

frameAt :: [(Picture, Float)] -> Float -> Picture
frameAt list time = fst . head . dropWhile ((< time) . snd) $ list

mapFst :: (a -> c) -> (a, b) -> (c, b)
mapFst f (a, b) = (f a, b) -- applies f to first element of a 2-tuple

mapSnd :: (b -> c) -> (a, b) -> (a, c)
mapSnd f (a, b) = (a, f b) -- applies f to the second element of a 2-tuple

Solution

  • (a) I think void cleanup is redundant and just cleanup works, but I like you am not 100% sure what that IO () value does precisely.

    I don't see a direct way to read FPS, but imageReaderTime produces timestamps in seconds, which would give you a good indicator. To propagate the timestamp you'll need to modify:

    nextFrame :: IO (Maybe (Image PixelRGB8, Double)) -> IO (Double, Picture)
    nextFrame getFrame = fmap fromImageRGB8 . swap . fromJust <$> getFrame
    

    Then you would say:

    stampedFrames <- replicateM frameCount $ nextFrame getFrame
    let (tstamps, frames) = unzip stampedFrames
    let approx_fps = fromIntegral (length tstamps) / (maximum tstamps - minimum tstamps)
    

    Finally you can pass approx_fps as a parameter to frameAt, which will have to use Double rather than Float or else some type-coercing function.

    However, for what you're doing, what might be better is to have something like:

    frameAt :: [(Double, Picture)] -> Double -> Picture
    frameAt list time = snd . head . dropWhile ((< time) . fst) $ list
    

    This takes the list, drops all elements whose first element (timestamp) is less than the requested time, and then returns the second element (picture) of the first pair that occurs after that. No FPS guessing is needed.