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 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.