Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions 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
5 changes: 5 additions & 0 deletions src/avio.jl
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,11 @@ 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.
=#
"""
framerate(f::VideoReader)

Read the framerate of a VideoReader object.
"""
function framerate(f::VideoReader)
# Try codec_context time_base first
# NOTE: In modern FFmpeg (4+), codec_context.time_base is primarily used for encoding
Expand Down
32 changes: 10 additions & 22 deletions src/testvideos.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,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 @@ -58,10 +43,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 @@ -75,15 +61,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 @@ -95,6 +79,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 @@ -106,6 +91,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 @@ -117,6 +103,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 @@ -128,6 +115,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 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 @@ -169,3 +169,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 @@ -184,6 +186,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 @@ -199,6 +225,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 @@ -217,5 +248,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