kithe 2.3.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/characterization/kithe/ffprobe_characterization.rb +179 -0
- data/app/models/kithe/asset.rb +2 -1
- data/lib/kithe/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f3d3c39a3bdccd9fc97f8f5e16c94cf12d4926e7ca50cc5e3343406ad3044d3
|
4
|
+
data.tar.gz: 2bd6fc9c175d4ec213e58460fbb95b5c40b49ca1ae5808e77df26932cf60474a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55c9e6eb14e975a46927fa01b4c7a8a67ca1b804ae7662f2e34e7878ee0a3247b944395edf93e311f7c39b640392ddd5b74a3df77381ba5f761e78f883e48dfa
|
7
|
+
data.tar.gz: 730f6f1d20d45d7973d2c340e15889d6f2b56295f70eec8703a1e1b666ba078357b1d1ec2e34fa14ca5575ff17032f52340ffba3070e2b16e3da35012c5e8d9e
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'tty/command'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Kithe
|
5
|
+
# Characterizes Audio or Video files using `ffprobe`, a tool that comes with `ffmpeg`.
|
6
|
+
#
|
7
|
+
# You can pass in a local File object (with a pathname), a local String pathname, or
|
8
|
+
# a remote URL. (Remote URLs will be passed directy to ffprobe, which can efficiently
|
9
|
+
# fetch just the bytes it needs)
|
10
|
+
#
|
11
|
+
# You can get back normalized A/V metadata:
|
12
|
+
#
|
13
|
+
# metadata = FfprobeCharacterization.new(url).normalized_metadata
|
14
|
+
#
|
15
|
+
# Normalized metadata is a *flat* hash of typed JSON-able values. It uses
|
16
|
+
# keys based on what the ActiveEncode gem seems to use, but adds some extras
|
17
|
+
# and makes a few tweaks. See the #normalized_metadata method source for
|
18
|
+
# keys supplied.
|
19
|
+
#
|
20
|
+
# Or the complete FFprobe response as JSON. (We try to use ffprobe options that
|
21
|
+
# are exhausitive as to what is returned, including ffprobe version(s))
|
22
|
+
#
|
23
|
+
# ffprobe_results = FfprobeCharacterization.new(url).ffprobe_hash
|
24
|
+
#
|
25
|
+
class FfprobeCharacterization
|
26
|
+
class_attribute :ffprobe_command, default: "ffprobe"
|
27
|
+
class_attribute :ffprobe_timeout, default: 10
|
28
|
+
|
29
|
+
attr_reader :input_arg
|
30
|
+
|
31
|
+
# @param input [String,File] local File OR local filepath as String, OR remote URL as string
|
32
|
+
# If you have a remote url, just passing hte remote url is way more performant than
|
33
|
+
# downloading it yourself locally -- ffprobe will just fetch the bytes it needs.
|
34
|
+
def initialize(input)
|
35
|
+
if input.respond_to?(:path)
|
36
|
+
input = input.path
|
37
|
+
end
|
38
|
+
@input_arg = input
|
39
|
+
end
|
40
|
+
|
41
|
+
# a helper for creating a block for shrine uploader, you can always use
|
42
|
+
# FFprobeCharecterization.new directly too!
|
43
|
+
#
|
44
|
+
# * Does not run on "cache" action, only on promotion (or manual execution).
|
45
|
+
#
|
46
|
+
# * Will run only on items with "audio/" or "video/" content-type.
|
47
|
+
#
|
48
|
+
# * By default only on main original, not derivatives, although
|
49
|
+
# you can pass `run_on_derivatives: true` if desired.
|
50
|
+
#
|
51
|
+
# Will use ffprobe with direct URL if possible based on source_io (ffprobe
|
52
|
+
# can very efficiently access only bytes needed from URL), otherwise will
|
53
|
+
# download local temp copy if necessary.
|
54
|
+
#
|
55
|
+
# class AssetUploader < Kithe::AssetUploader
|
56
|
+
# add_metadata do |source_io, **context|
|
57
|
+
# Kithe::FfprobeCharacterization.characterize_from_uploader(source_io, context)
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# #...
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
def self.characterize_from_uploader(source_io, add_metadata_context, run_on_derivatives: false)
|
64
|
+
# only for A/V please
|
65
|
+
return {} unless add_metadata_context.dig(:metadata, "mime_type")&.start_with?(%r{\A(audio|video)/})
|
66
|
+
|
67
|
+
# don't run on cache, only on promotion or manual trigger
|
68
|
+
return {} unless add_metadata_context[:action] != :cache
|
69
|
+
|
70
|
+
# don't run on derivatives unless option given
|
71
|
+
return {} unless add_metadata_context[:derivative].nil? || run_on_derivatives
|
72
|
+
|
73
|
+
# ffprobe can use a URL and very efficiently only retrieve what bytes it needs...
|
74
|
+
if source_io.respond_to?(:url) && source_io.url.start_with?(/\Ahttps?:/)
|
75
|
+
Kithe::FfprobeCharacterization.new(source_io.url).normalized_metadata
|
76
|
+
else
|
77
|
+
# if not already a file, will download, possibly slow, but gets us to go.
|
78
|
+
Shrine.with_file(source_io) do |file|
|
79
|
+
Kithe::FfprobeCharacterization.new(file.path).normalized_metadata
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# ffprobe args come from this suggestion:
|
85
|
+
#
|
86
|
+
# https://gist.github.com/nrk/2286511?permalink_comment_id=2593200#gistcomment-2593200
|
87
|
+
#
|
88
|
+
# We also add in various current version tags! If we're going to record all ffprobe
|
89
|
+
# output, we'll want that too!
|
90
|
+
def ffprobe_options
|
91
|
+
[
|
92
|
+
"-hide_banner",
|
93
|
+
"-loglevel", "fatal",
|
94
|
+
"-show_error", "-show_format", "-show_streams", "-show_programs",
|
95
|
+
"-show_chapters", "-show_private_data", "-show_versions",
|
96
|
+
"-print_format", "json",
|
97
|
+
]
|
98
|
+
end
|
99
|
+
|
100
|
+
# ffprobe output parsed as JSON...
|
101
|
+
def ffprobe_hash
|
102
|
+
@ffprobe_hash ||= JSON.parse(ffprobe_stdout).merge(
|
103
|
+
"ffprobe_options_used" => ffprobe_options.join(" ")
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns a FLAT JSON-able hash of normalized a/v metadata.
|
108
|
+
#
|
109
|
+
# Tries to standardize to what ActiveEncode uses, with some changes and additions.
|
110
|
+
# https://github.com/samvera-labs/active_encode/blob/42f5ed5427a39e56093a5e82123918c4b2619a47/lib/active_encode/technical_metadata.rb
|
111
|
+
#
|
112
|
+
# A video file or other container can have more than one audio or video stream in it, although
|
113
|
+
# this is somewhat unusual for our domain. For the stream-specific audio_ and video_ metadata
|
114
|
+
# returned, we just choose the *first* returned audio or video stream (which may be more or
|
115
|
+
# less arbitrary)
|
116
|
+
#
|
117
|
+
# See also #ffprobe_hash for complete ffprobe results
|
118
|
+
def normalized_metadata
|
119
|
+
# overall audio_sample_rate are null, audio codec is wrong
|
120
|
+
@normalized_metadata ||= {
|
121
|
+
"width" => first_video_stream_json&.dig("width"),
|
122
|
+
"height" => first_video_stream_json&.dig("height"),
|
123
|
+
"frame_rate" => video_frame_rate_as_float, # frames per second
|
124
|
+
"duration_seconds" => ffprobe_hash&.dig("format", "duration")&.to_f&.round(3),
|
125
|
+
"audio_codec" => first_audio_stream_json&.dig("codec_name"),
|
126
|
+
"video_codec" => first_video_stream_json&.dig("codec_name"),
|
127
|
+
"audio_bitrate" => first_audio_stream_json&.dig("bit_rate")&.to_i, # in bps
|
128
|
+
"video_bitrate" => first_video_stream_json&.dig("bit_rate")&.to_i, # in bps
|
129
|
+
# extra ones not ActiveEncode
|
130
|
+
"bitrate" => ffprobe_hash.dig("format", "bit_rate")&.to_i, # overall bitrate of whole file in bps
|
131
|
+
"audio_sample_rate" => first_audio_stream_json&.dig("sample_rate")&.to_i, # in Hz
|
132
|
+
"audio_channels" => first_audio_stream_json&.dig("channels")&.to_i, # usually 1 or 2 (for stereo)
|
133
|
+
"audio_channel_layout" => first_audio_stream_json&.dig("channel_layout"), # stereo or mono or (dolby) 2.1, or something else.
|
134
|
+
}.compact
|
135
|
+
end
|
136
|
+
|
137
|
+
# just the ffprobe version please. This is also available
|
138
|
+
# in ffprobe_hash
|
139
|
+
def ffprobe_version
|
140
|
+
ffprobe_hash.dig("program_version", "version")
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def ffprobe_stdout
|
146
|
+
@ffprobe_output ||= TTY::Command.new(printer: :null).run(
|
147
|
+
ffprobe_command,
|
148
|
+
*ffprobe_options,
|
149
|
+
input_arg,
|
150
|
+
timeout: ffprobe_timeout).out
|
151
|
+
end
|
152
|
+
|
153
|
+
def first_video_stream_json
|
154
|
+
@first_video_stream_json ||= ffprobe_hash["streams"].find { |stream| stream["codec_type"] == "video" }
|
155
|
+
end
|
156
|
+
|
157
|
+
def first_audio_stream_json
|
158
|
+
@first_audio_stream_json ||= ffprobe_hash["streams"].find { |stream| stream["codec_type"] == "audio" }
|
159
|
+
end
|
160
|
+
|
161
|
+
# There are a few different values we could choose here. We're going to choose
|
162
|
+
# `avg_frame_rate` == total duration / number of frames,
|
163
|
+
# vs (not chosen) `r_frame_rate ` "the lowest framerate with which all timestamps can be represented accurately (it is the least common multiple of all framerates in the stream)"
|
164
|
+
#
|
165
|
+
# (note this sometimes gets us not what we expected, like it gets us 29.78 fps instead of 29.97)
|
166
|
+
#
|
167
|
+
# Then we have to change it from numerator/denomominator to float truncated to two decimal places,
|
168
|
+
# which we let ruby rational do for us.
|
169
|
+
def video_frame_rate_as_float
|
170
|
+
avg_frame_rate = first_video_stream_json&.dig("avg_frame_rate")
|
171
|
+
|
172
|
+
return nil unless avg_frame_rate
|
173
|
+
|
174
|
+
return nil if avg_frame_rate.split("/")[1] == "0" # sometimes it returns '0/0', don't know why.
|
175
|
+
|
176
|
+
Rational(avg_frame_rate).to_f.round(2)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/app/models/kithe/asset.rb
CHANGED
@@ -27,7 +27,8 @@ class Kithe::Asset < Kithe::Model
|
|
27
27
|
to: :file, allow_nil: true
|
28
28
|
delegate :stored?, to: :file_attacher
|
29
29
|
delegate :set_promotion_directives, :promotion_directives, to: :file_attacher
|
30
|
-
|
30
|
+
# delegate "metadata" as #file_metadata
|
31
|
+
delegate :metadata, to: :file, prefix: true, allow_nil: true
|
31
32
|
|
32
33
|
# will be sent to file_attacher.set_promotion_directives, provided by our
|
33
34
|
# kithe_promotion_hooks shrine plugin.
|
data/lib/kithe/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kithe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Rochkind
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -345,6 +345,7 @@ files:
|
|
345
345
|
- README.md
|
346
346
|
- Rakefile
|
347
347
|
- app/assets/config/kithe_manifest.js
|
348
|
+
- app/characterization/kithe/ffprobe_characterization.rb
|
348
349
|
- app/derivative_transformers/kithe/ffmpeg_transformer.rb
|
349
350
|
- app/derivative_transformers/kithe/vips_cli_image_to_jpeg.rb
|
350
351
|
- app/helpers/kithe/form_helper.rb
|
@@ -424,7 +425,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
424
425
|
- !ruby/object:Gem::Version
|
425
426
|
version: '0'
|
426
427
|
requirements: []
|
427
|
-
rubygems_version: 3.
|
428
|
+
rubygems_version: 3.2.32
|
428
429
|
signing_key:
|
429
430
|
specification_version: 4
|
430
431
|
summary: Shareable tools/components for building a digital collections app in Rails.
|