diff --git a/docs/src/reading.md b/docs/src/reading.md index 7157772c..35781c05 100644 --- a/docs/src/reading.md +++ b/docs/src/reading.md @@ -89,6 +89,11 @@ Total available frame count is available via `counttotalframes(f)` VideoIO.counttotalframes ``` +The video framerate can be read via `framerate(f)` +```@docs +VideoIO.framerate +``` + !!! note H264 videos encoded with `crf>0` have been observed to have 4-fewer frames available for reading. @@ -174,7 +179,7 @@ julia> opts["video_size"] = "640x480" julia> opencamera(VideoIO.DEFAULT_CAMERA_DEVICE[], VideoIO.DEFAULT_CAMERA_FORMAT[], opts) VideoReader(...) ``` - + Or more simply, change the default. For example: ```julia julia> VideoIO.DEFAULT_CAMERA_OPTIONS["video_size"] = "640x480" diff --git a/src/avio.jl b/src/avio.jl index d063a056..b7c6fbb9 100644 --- a/src/avio.jl +++ b/src/avio.jl @@ -405,10 +405,17 @@ Like containers, elementary streams also can store timestamps, 1/time_base is th For some codecs, the time base is closer to the field rate than the frame rate. Most notably, H.264 and MPEG-2 specify time_base as half of frame duration if no telecine is used ... -Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2. +Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2. =# -framerate(f::VideoReader) = - f.codec_context.time_base.den // f.codec_context.time_base.num // f.codec_context.ticks_per_frame +""" + framerate(f::VideoReader) + +Read the framerate of a VideoReader object. +""" +function framerate(f::VideoReader) + stream = get_stream(f) + return stream.time_base.den // stream.time_base.num // f.codec_context.ticks_per_frame +end height(f::VideoReader) = f.codec_context.height width(f::VideoReader) = f.codec_context.width diff --git a/src/testvideos.jl b/src/testvideos.jl index 0cae5217..cb2bd8aa 100644 --- a/src/testvideos.jl +++ b/src/testvideos.jl @@ -17,25 +17,10 @@ mutable struct VideoFile{compression} source::AbstractString download_url::AbstractString numframes::Int + framerate::Rational testframe::Int summarysize::Int - fps::Union{Nothing,Rational} - - VideoFile{compression}( - name::AbstractString, - description::AbstractString, - license::AbstractString, - credit::AbstractString, - source::AbstractString, - download_url::AbstractString, - numframes::Int, - testframe::Int, - summarysize::Int, - fps::Rational, - ) where {compression} = - new(name, description, license, credit, source, download_url, numframes, testframe, summarysize, fps) - VideoFile{compression}( name::AbstractString, description::AbstractString, @@ -44,10 +29,11 @@ mutable struct VideoFile{compression} source::AbstractString, download_url::AbstractString, numframes::Int, + framerate::Rational, testframe::Int, summarysize::Int, ) where {compression} = - new(name, description, license, credit, source, download_url, numframes, testframe, summarysize, nothing) + new(name, description, license, credit, source, download_url, numframes, framerate, testframe, summarysize) end show(io::IO, v::VideoFile) = print( @@ -61,15 +47,13 @@ VideoFile: source: $(v.source) download_url: $(v.download_url) numframes: $(v.numframes) + framerate: $(v.framerate) summarysize: $(v.summarysize) """, ) -VideoFile(name, description, license, credit, source, download_url, numframes, testframe, summarysize) = - VideoFile{:raw}(name, description, license, credit, source, download_url, numframes, testframe, summarysize) - -VideoFile(name, description, license, credit, source, download_url, numframes, testframe, summarysize, fps) = - VideoFile{:raw}(name, description, license, credit, source, download_url, numframes, testframe, summarysize, fps) +VideoFile(name, description, license, credit, source, download_url, numframes, framerate, testframe, summarysize) = + VideoFile{:raw}(name, description, license, credit, source, download_url, numframes, framerate, testframe, summarysize) # Standard test videos const videofiles = Dict( @@ -81,6 +65,7 @@ const videofiles = Dict( "https://downloadnatureclip.blogspot.com/p/download-links.html", "https://archive.org/download/LadybirdOpeningWingsCCBYNatureClip/Ladybird%20opening%20wings%20CC-BY%20NatureClip.mp4", 397, + 30000//1001, 13, 3216, ), @@ -92,6 +77,7 @@ const videofiles = Dict( "https://commons.wikimedia.org/wiki/File:Annie_Oakley_shooting_glass_balls,_1894.ogg", "https://upload.wikimedia.org/wikipedia/commons/8/87/Annie_Oakley_shooting_glass_balls%2C_1894.ogv", 726, + 30000//1001, 2, 167311096, ), @@ -103,6 +89,7 @@ const videofiles = Dict( "https://commons.wikimedia.org/wiki/File:2010-10-10-Lune.ogv", "https://upload.wikimedia.org/wikipedia/commons/e/ef/2010-10-10-Lune.ogv", 1213, + 25//1, 1, 9744, ), @@ -114,6 +101,7 @@ const videofiles = Dict( "https://www.eso.org/public/videos/eso1004a/", "https://upload.wikimedia.org/wikipedia/commons/1/13/Artist%E2%80%99s_impression_of_the_black_hole_inside_NGC_300_X-1_%28ESO_1004c%29.webm", 597, + 25//1, 1, 4816, ), @@ -125,10 +113,10 @@ const videofiles = Dict( "https://peach.blender.org/", "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4", 300, - 2, - 207376840, # Can be also 30000/1001 30 // 1, + 2, + 207376840, ), ) diff --git a/test/reading.jl b/test/reading.jl index cd84cc5f..f7fcb32d 100644 --- a/test/reading.jl +++ b/test/reading.jl @@ -11,6 +11,8 @@ try time_seconds = VideoIO.gettime(v) @test time_seconds == 0 + @test VideoIO.framerate(v) == testvid.framerate + @test get_fps(testvid_path) == testvid.framerate # ffprobe sanity check width, height = VideoIO.out_frame_size(v) @test VideoIO.width(v) == width @test VideoIO.height(v) == height @@ -21,8 +23,15 @@ trimmed_comparison_frame = comparison_frame end + fps1 = VideoIO.framerate(v) + # Find the first non-trivial image first_img = read(v) + + # fps should be the same before and after first read + fps2 = VideoIO.framerate(v) + @test fps1 == fps2 + first_time = VideoIO.gettime(v) seekstart(v) img = read(v) @@ -30,15 +39,6 @@ @test img == first_img @test size(img) == VideoIO.out_frame_size(v)[[2, 1]] - - # First read(v) then framerate(v) - # https://github.com/JuliaIO/VideoIO.jl/issues/349 - if !isnothing(testvid.fps) - @test isapprox(VideoIO.framerate(v), testvid.fps, rtol=0.01) - else - @test VideoIO.framerate(v) != 0 - end - # no scaling currently @test VideoIO.out_frame_size(v) == VideoIO.raw_frame_size(v) @test VideoIO.raw_pixel_format(v) == 0 # true for current test videos diff --git a/test/utils.jl b/test/utils.jl index 222aab5d..585030f8 100644 --- a/test/utils.jl +++ b/test/utils.jl @@ -173,3 +173,26 @@ macro memory_profile() end end end + +""" + get_fps(file [, streamno]) + +Query the container `file` for the frame per second(fps) of the video stream +`streamno` if applicable, instead returning `nothing`. +""" +function get_fps(file::AbstractString, streamno::Integer = 0) + streamno >= 0 || throw(ArgumentError("streamno must be non-negative")) + fps_strs = FFMPEG.exe( + `-v 0 -of compact=p=0 -select_streams v:$(streamno) -show_entries stream=r_frame_rate $file`, + command = FFMPEG.ffprobe, + collect = true, + ) + fps = split(fps_strs[1], '=')[2] + if occursin("No such file or directory", fps) + error("Could not find file $file") + elseif occursin("N/A", fps) + return nothing + end + + return reduce(//, parse.(Int, split(fps,'/')) ) +end diff --git a/test/writing.jl b/test/writing.jl index 4a7139ef..28e0bfb4 100644 --- a/test/writing.jl +++ b/test/writing.jl @@ -25,6 +25,7 @@ end VideoIO.save( tempvidpath, img_stack; + framerate = 24, codec_name = codec_name, encoder_private_options = encoder_private_options, encoder_options = encoder_options, @@ -36,6 +37,7 @@ end try notempty = !eof(f) @test notempty + @test VideoIO.framerate(f) == 24 if notempty img = read(f) test_img = scanline_arg ? parent(img) : img @@ -179,6 +181,30 @@ end end end +@testset "Encoding video with integer frame rates" begin + n = 100 + for fr in 20:30 + target_dur = n / fr + @testset "Encoding with frame rate $(fr)" begin + imgstack = map(x -> rand(UInt8, 100, 100), 1:n) + encoder_options = (color_range = 2, crf = 0, preset = "medium") + VideoIO.save(tempvidpath, imgstack, framerate = fr, encoder_options = encoder_options) + @test stat(tempvidpath).size > 100 + measured_dur_str = VideoIO.FFMPEG.exe( + `-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $(tempvidpath)`, + command = VideoIO.FFMPEG.ffprobe, + collect = true, + ) + @test parse(Float64, measured_dur_str[1]) ≈ target_dur rtol = 0.01 + @testset "Reading framerate" begin + f = VideoIO.openvideo(tempvidpath) + @test VideoIO.framerate(f) == fr + @test get_fps(tempvidpath) == fr # ffprobe sanity check + end + end + end +end + @testset "Encoding video with rational frame rates" begin n = 100 fr = 59 // 2 # 29.5 @@ -194,6 +220,11 @@ end collect = true, ) @test parse(Float64, measured_dur_str[1]) == target_dur + @testset "Reading framerate" begin + f = VideoIO.openvideo(tempvidpath) + @test VideoIO.framerate(f) == fr + @test get_fps(tempvidpath) == fr # ffprobe sanity check + end end end @@ -212,5 +243,10 @@ end collect = true, ) @test parse(Float64, measured_dur_str[1]) == target_dur + @testset "Reading framerate" begin + f = VideoIO.openvideo(tempvidpath) + @test VideoIO.framerate(f) == fr + @test get_fps(tempvidpath) == fr # ffprobe sanity check + end end end