kithe 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.
|