Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VideoIO.framerate(f) to the manual and expand tests #393

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
7 changes: 6 additions & 1 deletion docs/src/reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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"
Expand Down
13 changes: 10 additions & 3 deletions src/avio.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 12 additions & 24 deletions src/testvideos.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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,
),
Expand All @@ -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,
),
Expand All @@ -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,
),
Expand All @@ -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,
),
Expand All @@ -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,
),
)

Expand Down
18 changes: 9 additions & 9 deletions test/reading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,24 +23,22 @@
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)
@test VideoIO.gettime(v) == first_time
@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
Expand Down
23 changes: 23 additions & 0 deletions test/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 36 additions & 0 deletions test/writing.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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