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
(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.