Skip to content

Commit aadd066

Browse files
Add VideoIO.framerate(f) to the manual and expand tests (#393)
Co-authored-by: AbelHo <[email protected]>
1 parent 1e0dca4 commit aadd066

File tree

6 files changed

+88
-31
lines changed

6 files changed

+88
-31
lines changed

docs/src/reading.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ Total available frame count is available via `counttotalframes(f)`
8989
VideoIO.counttotalframes
9090
```
9191

92+
The video framerate can be read via `framerate(f)`
93+
```@docs
94+
VideoIO.framerate
95+
```
96+
9297
!!! note H264 videos encoded with `crf>0` have been observed to have 4-fewer frames
9398
available for reading.
9499

src/avio.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,11 @@ For some codecs, the time base is closer to the field rate than the frame rate.
429429
Most notably, H.264 and MPEG-2 specify time_base as half of frame duration if no telecine is used ...
430430
Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
431431
=#
432+
"""
433+
framerate(f::VideoReader)
434+
435+
Read the framerate of a VideoReader object.
436+
"""
432437
function framerate(f::VideoReader)
433438
# Try codec_context time_base first
434439
# NOTE: In modern FFmpeg (4+), codec_context.time_base is primarily used for encoding

src/testvideos.jl

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,10 @@ mutable struct VideoFile{compression}
3131
source::AbstractString
3232
download_url::AbstractString
3333
numframes::Int
34+
framerate::Rational
3435
testframe::Int
3536
summarysize::Int
3637

37-
fps::Union{Nothing,Rational}
38-
39-
VideoFile{compression}(
40-
name::AbstractString,
41-
description::AbstractString,
42-
license::AbstractString,
43-
credit::AbstractString,
44-
source::AbstractString,
45-
download_url::AbstractString,
46-
numframes::Int,
47-
testframe::Int,
48-
summarysize::Int,
49-
fps::Rational,
50-
) where {compression} =
51-
new(name, description, license, credit, source, download_url, numframes, testframe, summarysize, fps)
52-
5338
VideoFile{compression}(
5439
name::AbstractString,
5540
description::AbstractString,
@@ -58,10 +43,11 @@ mutable struct VideoFile{compression}
5843
source::AbstractString,
5944
download_url::AbstractString,
6045
numframes::Int,
46+
framerate::Rational,
6147
testframe::Int,
6248
summarysize::Int,
6349
) where {compression} =
64-
new(name, description, license, credit, source, download_url, numframes, testframe, summarysize, nothing)
50+
new(name, description, license, credit, source, download_url, numframes, framerate, testframe, summarysize)
6551
end
6652

6753
show(io::IO, v::VideoFile) = print(
@@ -75,15 +61,13 @@ VideoFile:
7561
source: $(v.source)
7662
download_url: $(v.download_url)
7763
numframes: $(v.numframes)
64+
framerate: $(v.framerate)
7865
summarysize: $(v.summarysize)
7966
""",
8067
)
8168

82-
VideoFile(name, description, license, credit, source, download_url, numframes, testframe, summarysize) =
83-
VideoFile{:raw}(name, description, license, credit, source, download_url, numframes, testframe, summarysize)
84-
85-
VideoFile(name, description, license, credit, source, download_url, numframes, testframe, summarysize, fps) =
86-
VideoFile{:raw}(name, description, license, credit, source, download_url, numframes, testframe, summarysize, fps)
69+
VideoFile(name, description, license, credit, source, download_url, numframes, framerate, testframe, summarysize) =
70+
VideoFile{:raw}(name, description, license, credit, source, download_url, numframes, framerate, testframe, summarysize)
8771

8872
# Standard test videos
8973
const videofiles = Dict(
@@ -95,6 +79,7 @@ const videofiles = Dict(
9579
"https://downloadnatureclip.blogspot.com/p/download-links.html",
9680
"https://archive.org/download/LadybirdOpeningWingsCCBYNatureClip/Ladybird%20opening%20wings%20CC-BY%20NatureClip.mp4",
9781
397,
82+
30000//1001,
9883
13,
9984
3216,
10085
),
@@ -106,6 +91,7 @@ const videofiles = Dict(
10691
"https://commons.wikimedia.org/wiki/File:Annie_Oakley_shooting_glass_balls,_1894.ogg",
10792
"https://upload.wikimedia.org/wikipedia/commons/8/87/Annie_Oakley_shooting_glass_balls%2C_1894.ogv",
10893
726,
94+
30000//1001,
10995
2,
11096
167311096,
11197
),
@@ -117,6 +103,7 @@ const videofiles = Dict(
117103
"https://commons.wikimedia.org/wiki/File:2010-10-10-Lune.ogv",
118104
"https://upload.wikimedia.org/wikipedia/commons/e/ef/2010-10-10-Lune.ogv",
119105
1213,
106+
25//1,
120107
1,
121108
9744,
122109
),
@@ -128,6 +115,7 @@ const videofiles = Dict(
128115
"https://www.eso.org/public/videos/eso1004a/",
129116
"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",
130117
597,
118+
25//1,
131119
1,
132120
4816,
133121
),

test/reading.jl

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
try
1212
time_seconds = VideoIO.gettime(v)
1313
@test time_seconds == 0
14+
@test VideoIO.framerate(v) == testvid.framerate
15+
@test get_fps(testvid_path) == testvid.framerate # ffprobe sanity check
1416
width, height = VideoIO.out_frame_size(v)
1517
@test VideoIO.width(v) == width
1618
@test VideoIO.height(v) == height
@@ -21,24 +23,22 @@
2123
trimmed_comparison_frame = comparison_frame
2224
end
2325

26+
fps1 = VideoIO.framerate(v)
27+
2428
# Find the first non-trivial image
2529
first_img = read(v)
30+
31+
# fps should be the same before and after first read
32+
fps2 = VideoIO.framerate(v)
33+
@test fps1 == fps2
34+
2635
first_time = VideoIO.gettime(v)
2736
seekstart(v)
2837
img = read(v)
2938
@test VideoIO.gettime(v) == first_time
3039
@test img == first_img
3140
@test size(img) == VideoIO.out_frame_size(v)[[2, 1]]
3241

33-
34-
# First read(v) then framerate(v)
35-
# https://github.com/JuliaIO/VideoIO.jl/issues/349
36-
if !isnothing(testvid.fps)
37-
@test isapprox(VideoIO.framerate(v), testvid.fps, rtol=0.01)
38-
else
39-
@test VideoIO.framerate(v) != 0
40-
end
41-
4242
# no scaling currently
4343
@test VideoIO.out_frame_size(v) == VideoIO.raw_frame_size(v)
4444
@test VideoIO.raw_pixel_format(v) == 0 # true for current test videos

test/utils.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,26 @@ macro memory_profile()
169169
end
170170
end
171171
end
172+
173+
"""
174+
get_fps(file [, streamno])
175+
176+
Query the container `file` for the frame per second(fps) of the video stream
177+
`streamno` if applicable, instead returning `nothing`.
178+
"""
179+
function get_fps(file::AbstractString, streamno::Integer = 0)
180+
streamno >= 0 || throw(ArgumentError("streamno must be non-negative"))
181+
fps_strs = FFMPEG.exe(
182+
`-v 0 -of compact=p=0 -select_streams v:$(streamno) -show_entries stream=r_frame_rate $file`,
183+
command = FFMPEG.ffprobe,
184+
collect = true,
185+
)
186+
fps = split(fps_strs[1], '=')[2]
187+
if occursin("No such file or directory", fps)
188+
error("Could not find file $file")
189+
elseif occursin("N/A", fps)
190+
return nothing
191+
end
192+
193+
return reduce(//, parse.(Int, split(fps,'/')) )
194+
end

test/writing.jl

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ end
2525
VideoIO.save(
2626
tempvidpath,
2727
img_stack;
28+
framerate = 24,
2829
codec_name = codec_name,
2930
encoder_private_options = encoder_private_options,
3031
encoder_options = encoder_options,
@@ -36,6 +37,7 @@ end
3637
try
3738
notempty = !eof(f)
3839
@test notempty
40+
@test VideoIO.framerate(f) == 24
3941
if notempty
4042
img = read(f)
4143
test_img = scanline_arg ? parent(img) : img
@@ -184,6 +186,30 @@ end
184186
end
185187
end
186188

189+
@testset "Encoding video with integer frame rates" begin
190+
n = 100
191+
for fr in 20:30
192+
target_dur = n / fr
193+
@testset "Encoding with frame rate $(fr)" begin
194+
imgstack = map(x -> rand(UInt8, 100, 100), 1:n)
195+
encoder_options = (color_range = 2, crf = 0, preset = "medium")
196+
VideoIO.save(tempvidpath, imgstack, framerate = fr, encoder_options = encoder_options)
197+
@test stat(tempvidpath).size > 100
198+
measured_dur_str = VideoIO.FFMPEG.exe(
199+
`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $(tempvidpath)`,
200+
command = VideoIO.FFMPEG.ffprobe,
201+
collect = true,
202+
)
203+
@test parse(Float64, measured_dur_str[1]) target_dur rtol = 0.01
204+
@testset "Reading framerate" begin
205+
f = VideoIO.openvideo(tempvidpath)
206+
@test VideoIO.framerate(f) == fr
207+
@test get_fps(tempvidpath) == fr # ffprobe sanity check
208+
end
209+
end
210+
end
211+
end
212+
187213
@testset "Encoding video with rational frame rates" begin
188214
n = 100
189215
fr = 59 // 2 # 29.5
@@ -199,6 +225,11 @@ end
199225
collect = true,
200226
)
201227
@test parse(Float64, measured_dur_str[1]) == target_dur
228+
@testset "Reading framerate" begin
229+
f = VideoIO.openvideo(tempvidpath)
230+
@test VideoIO.framerate(f) == fr
231+
@test get_fps(tempvidpath) == fr # ffprobe sanity check
232+
end
202233
end
203234
end
204235

@@ -217,5 +248,10 @@ end
217248
collect = true,
218249
)
219250
@test parse(Float64, measured_dur_str[1]) == target_dur
251+
@testset "Reading framerate" begin
252+
f = VideoIO.openvideo(tempvidpath)
253+
@test VideoIO.framerate(f) == fr
254+
@test get_fps(tempvidpath) == fr # ffprobe sanity check
255+
end
220256
end
221257
end

0 commit comments

Comments
 (0)