cloudinary 1.17.0 → 1.20.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.
@@ -0,0 +1,43 @@
1
+ module Cloudinary
2
+ module Config
3
+ include BaseConfig
4
+
5
+ ENV_URL = "CLOUDINARY_URL"
6
+ SCHEME = "cloudinary"
7
+
8
+ def load_config_from_env
9
+ if ENV["CLOUDINARY_CLOUD_NAME"]
10
+ config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
11
+ config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
12
+ config_keys.each do |full_key|
13
+ conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
14
+ conf_val = ENV[full_key]
15
+ conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
16
+ update(conf_key => conf_val)
17
+ end
18
+ elsif ENV[ENV_URL]
19
+ load_from_url(ENV[ENV_URL])
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def env_url
26
+ ENV_URL
27
+ end
28
+
29
+ def expected_scheme
30
+ SCHEME
31
+ end
32
+
33
+ def config_from_parsed_url(parsed_url)
34
+ {
35
+ "cloud_name" => parsed_url.host,
36
+ "api_key" => parsed_url.user,
37
+ "api_secret" => parsed_url.password,
38
+ "private_cdn" => !parsed_url.path.blank?,
39
+ "secure_distribution" => parsed_url.path[1..-1]
40
+ }
41
+ end
42
+ end
43
+ end
@@ -438,7 +438,7 @@ begin
438
438
  # @return [::SassC::Script::Value::String]
439
439
  def cloudinary_url(public_id, sass_options = {})
440
440
  options = {}
441
- sass_options.to_h.each { |k, v| options[k.value] = v.value }
441
+ sass_options.to_h.each { |k, v| options[k.value.to_sym] = v.value }
442
442
  url = Cloudinary::Utils.cloudinary_url(public_id.value, {:type => :asset}.merge(options))
443
443
  ::SassC::Script::Value::String.new("url(#{url})")
444
444
  end
@@ -42,6 +42,7 @@ class Cloudinary::Uploader
42
42
  :faces => Cloudinary::Utils.as_safe_bool(options[:faces]),
43
43
  :folder => options[:folder],
44
44
  :format => options[:format],
45
+ :filename_override => options[:filename_override],
45
46
  :headers => build_custom_headers(options[:headers]),
46
47
  :image_metadata => Cloudinary::Utils.as_safe_bool(options[:image_metadata]),
47
48
  :invalidate => Cloudinary::Utils.as_safe_bool(options[:invalidate]),
@@ -65,7 +66,8 @@ class Cloudinary::Uploader
65
66
  :unique_filename => Cloudinary::Utils.as_safe_bool(options[:unique_filename]),
66
67
  :upload_preset => options[:upload_preset],
67
68
  :use_filename => Cloudinary::Utils.as_safe_bool(options[:use_filename]),
68
- :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis])
69
+ :accessibility_analysis => Cloudinary::Utils.as_safe_bool(options[:accessibility_analysis]),
70
+ :metadata => Cloudinary::Utils.encode_context(options[:metadata])
69
71
  }
70
72
  params
71
73
  end
@@ -272,6 +274,29 @@ class Cloudinary::Uploader
272
274
  return self.call_tags_api(nil, "remove_all", public_ids, options)
273
275
  end
274
276
 
277
+ # Populates metadata fields with the given values. Existing values will be overwritten.
278
+ #
279
+ # Any metadata-value pairs given are merged with any existing metadata-value pairs
280
+ # (an empty value for an existing metadata field clears the value).
281
+ #
282
+ # @param [Hash] metadata A list of custom metadata fields (by external_id) and the values to assign to each of them.
283
+ # @param [Array] public_ids An array of Public IDs of assets uploaded to Cloudinary.
284
+ # @param [Hash] options
285
+ # @option options [String] :resource_type The type of file. Default: image. Valid values: image, raw, video.
286
+ # @option options [String] :type The storage type. Default: upload. Valid values: upload, private, authenticated
287
+ # @return mixed a list of public IDs that were updated
288
+ # @raise [Cloudinary::Api:Error]
289
+ def self.update_metadata(metadata, public_ids, options = {})
290
+ self.call_api("metadata", options) do
291
+ {
292
+ timestamp: (options[:timestamp] || Time.now.to_i),
293
+ type: options[:type],
294
+ public_ids: Cloudinary::Utils.build_array(public_ids),
295
+ metadata: Cloudinary::Utils.encode_context(metadata)
296
+ }
297
+ end
298
+ end
299
+
275
300
  private
276
301
 
277
302
  def self.call_tags_api(tag, command, public_ids = [], options = {})
@@ -317,11 +342,13 @@ class Cloudinary::Uploader
317
342
  non_signable ||= []
318
343
 
319
344
  unless options[:unsigned]
320
- api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
321
- api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
322
- params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret)
323
- params[:api_key] = api_key
345
+ api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
346
+ api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
347
+ signature_algorithm = options[:signature_algorithm]
348
+ params[:signature] = Cloudinary::Utils.api_sign_request(params.reject { |k, v| non_signable.include?(k) }, api_secret, signature_algorithm)
349
+ params[:api_key] = api_key
324
350
  end
351
+ proxy = options[:api_proxy] || Cloudinary.config.api_proxy
325
352
  timeout = options.fetch(:timeout) { Cloudinary.config.to_h.fetch(:timeout, 60) }
326
353
 
327
354
  result = nil
@@ -331,7 +358,7 @@ class Cloudinary::Uploader
331
358
  headers['Content-Range'] = options[:content_range] if options[:content_range]
332
359
  headers['X-Unique-Upload-Id'] = options[:unique_upload_id] if options[:unique_upload_id]
333
360
  headers.merge!(options[:extra_headers]) if options[:extra_headers]
334
- RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers) do
361
+ RestClient::Request.execute(:method => :post, :url => api_url, :payload => params.reject { |k, v| v.nil? || v=="" }, :timeout => timeout, :headers => headers, :proxy => proxy) do
335
362
  |response, request, tmpresult|
336
363
  raise CloudinaryException, "Server returned unexpected status code - #{response.code} - #{response.body}" unless [200, 400, 401, 403, 404, 500].include?(response.code)
337
364
  begin
@@ -32,19 +32,34 @@ class Cloudinary::Utils
32
32
 
33
33
  PREDEFINED_VARS = {
34
34
  "aspect_ratio" => "ar",
35
+ "aspectRatio" => "ar",
35
36
  "current_page" => "cp",
37
+ "currentPage" => "cp",
36
38
  "face_count" => "fc",
39
+ "faceCount" => "fc",
37
40
  "height" => "h",
38
41
  "initial_aspect_ratio" => "iar",
42
+ "initialAspectRatio" => "iar",
43
+ "trimmed_aspect_ratio" => "tar",
44
+ "trimmedAspectRatio" => "tar",
39
45
  "initial_height" => "ih",
46
+ "initialHeight" => "ih",
40
47
  "initial_width" => "iw",
48
+ "initialWidth" => "iw",
41
49
  "page_count" => "pc",
50
+ "pageCount" => "pc",
42
51
  "page_x" => "px",
52
+ "pageX" => "px",
43
53
  "page_y" => "py",
54
+ "pageY" => "py",
44
55
  "tags" => "tags",
45
56
  "initial_duration" => "idu",
57
+ "initialDuration" => "idu",
46
58
  "duration" => "du",
47
- "width" => "w"
59
+ "width" => "w",
60
+ "illustration_score" => "ils",
61
+ "illustrationScore" => "ils",
62
+ "context" => "ctx"
48
63
  }
49
64
 
50
65
  SIMPLE_TRANSFORMATION_PARAMS = {
@@ -144,6 +159,16 @@ class Cloudinary::Utils
144
159
  LONG_URL_SIGNATURE_LENGTH = 32
145
160
  SHORT_URL_SIGNATURE_LENGTH = 8
146
161
 
162
+ UPLOAD_PREFIX = 'https://api.cloudinary.com'
163
+
164
+ ALGO_SHA1 = :sha1
165
+ ALGO_SHA256 = :sha256
166
+
167
+ ALGORITHM_SIGNATURE = {
168
+ ALGO_SHA1 => Digest::SHA1,
169
+ ALGO_SHA256 => Digest::SHA256,
170
+ }
171
+
147
172
  def self.extract_config_params(options)
148
173
  options.select{|k,v| URL_KEYS.include?(k)}
149
174
  end
@@ -309,16 +334,16 @@ class Cloudinary::Utils
309
334
  "if_" + normalize_expression(if_value) unless if_value.to_s.empty?
310
335
  end
311
336
 
312
- EXP_REGEXP = Regexp.new('(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
337
+ EXP_REGEXP = Regexp.new('(\$_*[^_ ]+)|(?<!\$)('+PREDEFINED_VARS.keys.join("|")+')'+'|('+CONDITIONAL_OPERATORS.keys.reverse.map { |k| Regexp.escape(k) }.join('|')+')(?=[ _])')
313
338
  EXP_REPLACEMENT = PREDEFINED_VARS.merge(CONDITIONAL_OPERATORS)
314
339
 
315
340
  def self.normalize_expression(expression)
316
341
  if expression.nil?
317
- ''
342
+ nil
318
343
  elsif expression.is_a?( String) && expression =~ /^!.+!$/ # quoted string
319
344
  expression
320
345
  else
321
- expression.to_s.gsub(EXP_REGEXP,EXP_REPLACEMENT).gsub(/[ _]+/, "_")
346
+ expression.to_s.gsub(EXP_REGEXP) { |match| EXP_REPLACEMENT[match] || match }.gsub(/[ _]+/, "_")
322
347
  end
323
348
  end
324
349
 
@@ -433,9 +458,9 @@ class Cloudinary::Utils
433
458
  params_to_sign.map{|k,v| [k.to_s, v.is_a?(Array) ? v.join(",") : v]}.reject{|k,v| v.nil? || v == ""}.sort_by(&:first).map{|k,v| "#{k}=#{v}"}.join("&")
434
459
  end
435
460
 
436
- def self.api_sign_request(params_to_sign, api_secret)
461
+ def self.api_sign_request(params_to_sign, api_secret, signature_algorithm = nil)
437
462
  to_sign = api_string_to_sign(params_to_sign)
438
- Digest::SHA1.hexdigest("#{to_sign}#{api_secret}")
463
+ hash("#{to_sign}#{api_secret}", signature_algorithm, :hexdigest)
439
464
  end
440
465
 
441
466
  # Returns a JSON array as String.
@@ -501,6 +526,7 @@ class Cloudinary::Utils
501
526
  use_root_path = config_option_consume(options, :use_root_path)
502
527
  auth_token = config_option_consume(options, :auth_token)
503
528
  long_url_signature = config_option_consume(options, :long_url_signature)
529
+ signature_algorithm = config_option_consume(options, :signature_algorithm)
504
530
  unless auth_token == false
505
531
  auth_token = Cloudinary::AuthToken.merge_auth_token(Cloudinary.config.auth_token, auth_token)
506
532
  end
@@ -545,7 +571,10 @@ class Cloudinary::Utils
545
571
  raise(CloudinaryException, "Must supply api_secret") if (secret.nil? || secret.empty?)
546
572
  to_sign = [transformation, sign_version && version, source_to_sign].reject(&:blank?).join("/")
547
573
  to_sign = fully_unescape(to_sign)
548
- signature = compute_signature(to_sign, secret, long_url_signature)
574
+ signature_algorithm = long_url_signature ? ALGO_SHA256 : signature_algorithm
575
+ hash = hash("#{to_sign}#{secret}", signature_algorithm)
576
+ signature = Base64.urlsafe_encode64(hash)
577
+ signature = "s--#{signature[0, long_url_signature ? LONG_URL_SIGNATURE_LENGTH : SHORT_URL_SIGNATURE_LENGTH ]}--"
549
578
  end
550
579
 
551
580
  prefix = unsigned_download_url_prefix(source, cloud_name, private_cdn, cdn_subdomain, secure_cdn_subdomain, cname, secure, secure_distribution)
@@ -661,18 +690,31 @@ class Cloudinary::Utils
661
690
  prefix
662
691
  end
663
692
 
693
+ # Creates a base URL for the cloudinary api
694
+ #
695
+ # @param [Object] path Resource name
696
+ # @param [Hash] options Additional options
697
+ #
698
+ # @return [String]
699
+ def self.base_api_url(path, options = {})
700
+ cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || UPLOAD_PREFIX
701
+ cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, 'Must supply cloud_name')
702
+
703
+ [cloudinary, 'v1_1', cloud_name, path].join('/')
704
+ end
705
+
664
706
  def self.cloudinary_api_url(action = 'upload', options = {})
665
- cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || "https://api.cloudinary.com"
666
- cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise(CloudinaryException, "Must supply cloud_name")
667
- resource_type = options[:resource_type] || "image"
668
- return [cloudinary, "v1_1", cloud_name, resource_type, action].join("/")
707
+ resource_type = options[:resource_type] || 'image'
708
+
709
+ base_api_url([resource_type, action], options)
669
710
  end
670
711
 
671
712
  def self.sign_request(params, options={})
672
713
  api_key = options[:api_key] || Cloudinary.config.api_key || raise(CloudinaryException, "Must supply api_key")
673
714
  api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise(CloudinaryException, "Must supply api_secret")
715
+ signature_algorithm = options[:signature_algorithm]
674
716
  params = params.reject{|k, v| self.safe_blank?(v)}
675
- params[:signature] = Cloudinary::Utils.api_sign_request(params, api_secret)
717
+ params[:signature] = api_sign_request(params, api_secret, signature_algorithm)
676
718
  params[:api_key] = api_key
677
719
  params
678
720
  end
@@ -736,6 +778,18 @@ class Cloudinary::Utils
736
778
  download_archive_url(options.merge(:target_format => "zip"))
737
779
  end
738
780
 
781
+ # Creates and returns a URL that when invoked creates an archive of a folder.
782
+ #
783
+ # @param [Object] folder_path Full path (from the root) of the folder to download.
784
+ # @param [Hash] options Additional options.
785
+ #
786
+ # @return [String]
787
+ def self.download_folder(folder_path, options = {})
788
+ resource_type = options[:resource_type] || "all"
789
+
790
+ download_archive_url(options.merge(:resource_type => resource_type, :prefixes => folder_path))
791
+ end
792
+
739
793
  def self.signed_download_url(public_id, options = {})
740
794
  aws_private_key_path = options[:aws_private_key_path] || Cloudinary.config.aws_private_key_path
741
795
  if aws_private_key_path
@@ -1137,23 +1191,51 @@ class Cloudinary::Utils
1137
1191
  REMOTE_URL_REGEX === url
1138
1192
  end
1139
1193
 
1140
- # Computes a short or long signature based on a message and secret
1141
- # @param [String] message The string to sign
1142
- # @param [String] secret A secret that will be added to the message when signing
1143
- # @param [Boolean] long_signature Whether to create a short or long signature
1144
- # @return [String] Properly formatted signature
1145
- def self.compute_signature(message, secret, long_url_signature)
1146
- combined_message_secret = message + secret
1194
+ # The returned url should allow downloading the backedup asset based on the version and asset id
1195
+ #
1196
+ # asset and version id are returned with resource(<PUBLIC_ID1>, { versions: true })
1197
+ #
1198
+ # @param [String] asset_id Asset identifier
1199
+ # @param [String] version_id Specific version of asset to download
1200
+ # @param [Hash] options Additional options
1201
+ #
1202
+ # @return [String] An url for downloading a file
1203
+ def self.download_backedup_asset(asset_id, version_id, options = {})
1204
+ params = Cloudinary::Utils.sign_request({
1205
+ :timestamp => (options[:timestamp] || Time.now.to_i),
1206
+ :asset_id => asset_id,
1207
+ :version_id => version_id
1208
+ }, options)
1209
+
1210
+ "#{Cloudinary::Utils.base_api_url("download_backup", options)}?#{Cloudinary::Utils.hash_query_params((params))}"
1211
+ end
1147
1212
 
1148
- algo, signature_length =
1149
- if long_url_signature
1150
- [Digest::SHA256, LONG_URL_SIGNATURE_LENGTH]
1151
- else
1152
- [Digest::SHA1, SHORT_URL_SIGNATURE_LENGTH]
1153
- end
1213
+ # Format date in a format accepted by the usage API (e.g., 31-12-2020) if
1214
+ # passed value is of type Date, otherwise return the string representation of
1215
+ # the input.
1216
+ #
1217
+ # @param [Date|Object] date
1218
+ # @return [String]
1219
+ def self.to_usage_api_date_format(date)
1220
+ if date.is_a?(Date)
1221
+ date.strftime('%d-%m-%Y')
1222
+ else
1223
+ date.to_s
1224
+ end
1225
+ end
1154
1226
 
1155
- "s--#{Base64.urlsafe_encode64(algo.digest(combined_message_secret))[0, signature_length]}--"
1227
+ # Computes hash from input string using specified algorithm.
1228
+ #
1229
+ # @param [String] input String which to compute hash from
1230
+ # @param [String|nil] signature_algorithm Algorithm to use for computing hash
1231
+ # @param [Symbol] hash_method Hash method applied to a signature algorithm (:digest or :hexdigest)
1232
+ #
1233
+ # @return [String] Computed hash value
1234
+ def self.hash(input, signature_algorithm = nil, hash_method = :digest)
1235
+ signature_algorithm ||= Cloudinary.config.signature_algorithm || ALGO_SHA1
1236
+ algorithm = ALGORITHM_SIGNATURE[signature_algorithm] || raise("Unsupported algorithm '#{signature_algorithm}'")
1237
+ algorithm.public_send(hash_method, input)
1156
1238
  end
1157
1239
 
1158
- private_class_method :compute_signature
1240
+ private_class_method :hash
1159
1241
  end
@@ -1,4 +1,4 @@
1
1
  # Copyright Cloudinary
2
2
  module Cloudinary
3
- VERSION = "1.17.0"
3
+ VERSION = "1.20.0"
4
4
  end
@@ -3,12 +3,33 @@ module CloudinaryHelper
3
3
  DEFAULT_POSTER_OPTIONS = { :format => 'jpg', :resource_type => 'video' }
4
4
  DEFAULT_SOURCE_TYPES = %w(webm mp4 ogv)
5
5
  DEFAULT_VIDEO_OPTIONS = { :resource_type => 'video' }
6
+ DEFAULT_SOURCES = [
7
+ {
8
+ :type => "mp4",
9
+ :codecs => "hev1",
10
+ :transformations => { :video_codec => "h265" }
11
+ },
12
+ {
13
+ :type => "webm",
14
+ :codecs => "vp9",
15
+ :transformations => { :video_codec => "vp9" }
16
+ },
17
+ {
18
+ :type => "mp4",
19
+ :transformations => { :video_codec => "auto" }
20
+ },
21
+ {
22
+ :type => "webm",
23
+ :transformations => { :video_codec => "auto" }
24
+ }
25
+ ]
6
26
 
7
27
  # Creates an HTML video tag for the provided +source+
8
28
  #
9
29
  # ==== Options
10
30
  # * <tt>:source_types</tt> - Specify which source type the tag should include. defaults to webm, mp4 and ogv.
11
31
  # * <tt>:source_transformation</tt> - specific transformations to use for a specific source type.
32
+ # * <tt>:sources</tt> - list of sources (overrides :source_types when present)
12
33
  # * <tt>:poster</tt> - override default thumbnail:
13
34
  # * url: provide an ad hoc url
14
35
  # * options: with specific poster transformations and/or Cloudinary +:public_id+
@@ -20,6 +41,17 @@ module CloudinaryHelper
20
41
  # cl_video_tag("mymovie.webm", :source_types => [:webm, :mp4], :poster => {:effect => 'sepia'}) do
21
42
  # content_tag( :span, "Cannot present video!")
22
43
  # end
44
+ # cl_video_tag("mymovie", :sources => [
45
+ # {
46
+ # :type => "mp4",
47
+ # :codecs => "hev1",
48
+ # :transformations => { :video_codec => "h265" }
49
+ # },
50
+ # {
51
+ # :type => "webm",
52
+ # :transformations => { :video_codec => "auto" }
53
+ # }
54
+ # ])
23
55
  def cl_video_tag(source, options = {}, &block)
24
56
  source = strip_known_ext(source)
25
57
  video_attributes = [:autoplay,:controls,:loop,:muted,:poster, :preload]
@@ -48,30 +80,12 @@ module CloudinaryHelper
48
80
  video_options[:poster] = cl_video_thumbnail_path(source, options)
49
81
  end
50
82
 
51
- source_transformation = options.delete(:source_transformation) || {}
52
- source_types = Array(options.delete(:source_types))
53
- fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
83
+ fallback = (capture(&block) if block_given?) || options.delete(:fallback_content)
54
84
 
55
- if source_types.size > 1
56
- cloudinary_tag(source, options) do |_source, tag_options|
57
- content_tag('video', tag_options.merge(video_options)) do
58
- source_tags = source_types.map do |type|
59
- transformation = source_transformation[type.to_sym] || {}
60
- cloudinary_tag("#{source}.#{type}", options.merge(transformation)) do |url, _tag_options|
61
- mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
62
- tag("source", :src => url, :type => mime_type)
63
- end
64
- end
65
- source_tags.push(fallback.html_safe) unless fallback.blank?
66
- safe_join(source_tags)
67
- end
68
- end
85
+ if options[:sources]
86
+ video_tag_from_sources(source, options, video_options, fallback)
69
87
  else
70
- transformation = source_transformation[source_types.first.to_sym] || {}
71
- video_options[:src] = cl_video_path("#{source}.#{source_types.first.to_sym}", transformation.merge(options))
72
- cloudinary_tag(source, options) do |_source, tag_options|
73
- content_tag('video', fallback, tag_options.merge(video_options))
74
- end
88
+ video_tag_from_source_types(source, options, video_options, fallback)
75
89
  end
76
90
  end
77
91
 
@@ -96,6 +110,66 @@ module CloudinaryHelper
96
110
  name.sub(/\.(#{DEFAULT_SOURCE_TYPES.join("|")})$/, '')
97
111
  end
98
112
 
113
+ private
114
+
115
+ def video_tag_from_source_types(source_name, options, video_options, fallback)
116
+ source_transformation = options.delete(:source_transformation) || {}
117
+ source_types = Array(options.delete(:source_types))
118
+
119
+ if source_types.size > 1
120
+ sources = source_types.map do |type|
121
+ {
122
+ :type => type,
123
+ :transformations => source_transformation[type.to_sym] || {}
124
+ }
125
+ end
126
+
127
+ generate_tag_from_sources(:source_name => source_name,
128
+ :sources => sources,
129
+ :options => options,
130
+ :video_options => video_options,
131
+ :fallback => fallback)
132
+ else
133
+ transformation = source_transformation[source_types.first.to_sym] || {}
134
+ video_options[:src] = cl_video_path("#{source_name}.#{source_types.first.to_sym}", transformation.merge(options))
135
+ cloudinary_tag(source_name, options) do |_source, tag_options|
136
+ content_tag('video', fallback, tag_options.merge(video_options))
137
+ end
138
+ end
139
+ end
140
+
141
+ def video_tag_from_sources(source_name, options, video_options, fallback)
142
+ sources = options.delete(:sources)
143
+
144
+ generate_tag_from_sources(:source_name => source_name,
145
+ :sources => sources,
146
+ :options => options,
147
+ :video_options => video_options,
148
+ :fallback => fallback)
149
+ end
150
+
151
+ def generate_tag_from_sources(params)
152
+ source_name, sources, options, video_options, fallback = params.values_at(:source_name, :sources, :options, :video_options, :fallback)
153
+
154
+ cloudinary_tag(source_name, options) do |_source, tag_options|
155
+ content_tag('video', tag_options.merge(video_options)) do
156
+ source_tags = sources.map do |source|
157
+ type = source[:type]
158
+ transformation = source[:transformations] || {}
159
+ cloudinary_tag("#{source_name}.#{type}", options.merge(transformation)) do |url, _tag_options|
160
+ mime_type = "video/#{(type == 'ogv' ? 'ogg' : type)}"
161
+ if source[:codecs]
162
+ codecs = source[:codecs].is_a?(Array) ? source[:codecs].join(", ") : source[:codecs]
163
+ mime_type = "#{mime_type}; codecs=#{codecs}"
164
+ end
165
+ tag("source", :src => url, :type => mime_type)
166
+ end
167
+ end
168
+ source_tags.push(fallback.html_safe) unless fallback.blank?
169
+ safe_join(source_tags)
170
+ end
171
+ end
172
+ end
99
173
  end
100
174
 
101
175