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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f1252c0becbda9f754e23af2333aaae66cf40cbfd6bacdad0f97425cd317de6
4
- data.tar.gz: 8460f704869d27d5a2011f746e88b9016fde465d1cbb023c883fe600b0510c1d
3
+ metadata.gz: 6f3d3c39a3bdccd9fc97f8f5e16c94cf12d4926e7ca50cc5e3343406ad3044d3
4
+ data.tar.gz: 2bd6fc9c175d4ec213e58460fbb95b5c40b49ca1ae5808e77df26932cf60474a
5
5
  SHA512:
6
- metadata.gz: '0890b072c3bfb9901f4c910fcaf0f288dd72dc8c8164202998dfabf2db4b3f7a09dfeaa8df6c4c9cd242b0c98ec084c48f5214d4a2c8f4991ee6d08643495857'
7
- data.tar.gz: e7c2bbe6f3f32ce67c646289f038a06d51f371fdb87634657296546c84fe3cb52a216b0a3c6648669f73e7d61f51b56a3afb0d28584fc85950dbf0936d6f5eda
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Kithe
2
- VERSION = '2.3.0'
2
+ VERSION = '2.4.0'
3
3
  end
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.3.0
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: 2021-12-02 00:00:00.000000000 Z
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.1.6
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.