-
Notifications
You must be signed in to change notification settings - Fork 21.4k
/
video_analyzer.rb
157 lines (132 loc) · 4.04 KB
/
video_analyzer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# frozen_string_literal: true
module ActiveStorage
# = Active Storage Video \Analyzer
#
# Extracts the following from a video blob:
#
# * Width (pixels)
# * Height (pixels)
# * Duration (seconds)
# * Angle (degrees)
# * Display aspect ratio
# * Audio (true if file has an audio channel, false if not)
# * Video (true if file has an video channel, false if not)
#
# Example:
#
# ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3], audio: true, video: true }
#
# When a video's angle is 90, -90, 270 or -270 degrees, its width and height are automatically swapped for convenience.
#
# This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by \Rails.
class Analyzer::VideoAnalyzer < Analyzer
def self.accept?(blob)
blob.video?
end
def metadata
{ width: width, height: height, duration: duration, angle: angle, display_aspect_ratio: display_aspect_ratio, audio: audio?, video: video? }.compact
end
private
def width
if rotated?
computed_height || encoded_height
else
encoded_width
end
end
def height
if rotated?
encoded_width
else
computed_height || encoded_height
end
end
def duration
duration = video_stream["duration"] || container["duration"]
Float(duration) if duration
end
def angle
if tags["rotate"]
Integer(tags["rotate"])
elsif display_matrix && display_matrix["rotation"]
Integer(display_matrix["rotation"])
end
end
def display_matrix
side_data.detect { |data| data["side_data_type"] == "Display Matrix" }
end
def display_aspect_ratio
if descriptor = video_stream["display_aspect_ratio"]
if terms = descriptor.split(":", 2)
numerator = Integer(terms[0])
denominator = Integer(terms[1])
[numerator, denominator] unless numerator == 0
end
end
end
def rotated?
angle == 90 || angle == 270 || angle == -90 || angle == -270
end
def audio?
audio_stream.present?
end
def video?
video_stream.present?
end
def computed_height
if encoded_width && display_height_scale
encoded_width * display_height_scale
end
end
def encoded_width
@encoded_width ||= Float(video_stream["width"]) if video_stream["width"]
end
def encoded_height
@encoded_height ||= Float(video_stream["height"]) if video_stream["height"]
end
def display_height_scale
@display_height_scale ||= Float(display_aspect_ratio.last) / display_aspect_ratio.first if display_aspect_ratio
end
def tags
@tags ||= video_stream["tags"] || {}
end
def side_data
@side_data ||= video_stream["side_data_list"] || {}
end
def video_stream
@video_stream ||= streams.detect { |stream| stream["codec_type"] == "video" } || {}
end
def audio_stream
@audio_stream ||= streams.detect { |stream| stream["codec_type"] == "audio" } || {}
end
def streams
probe["streams"] || []
end
def container
probe["format"] || {}
end
def probe
@probe ||= download_blob_to_tempfile { |file| probe_from(file) }
end
def probe_from(file)
instrument(File.basename(ffprobe_path)) do
IO.popen([ ffprobe_path,
"-print_format", "json",
"-show_streams",
"-show_format",
"-v", "error",
file.path
]) do |output|
JSON.parse(output.read)
end
end
rescue Errno::ENOENT
logger.info "Skipping video analysis because ffprobe isn't installed"
{}
end
def ffprobe_path
ActiveStorage.paths[:ffprobe] || "ffprobe"
end
end
end