diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 1308cf4..96f4b2b 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -67,4 +67,17 @@ module TagsHelper end.join("\n").html_safe end + def linked_article_tags(article) + if @profile + # We are rendering a page inside a profile, so link to the profile tag search. + url = { :controller => 'profile', :profile => @profile.identifier, :action => 'tags' } + tagname_option = :id + else + # We are rendering a page outside a profile, so link to the global tag search. + url = { :action => 'tag' } + tagname_option = :tag + end + article.tags.map { |t| link_to(t, url.merge(tagname_option=>t.name) ) }.join("\n") + end + end diff --git a/app/models/uploaded_file.rb b/app/models/uploaded_file.rb index abae67a..18e09d2 100644 --- a/app/models/uploaded_file.rb +++ b/app/models/uploaded_file.rb @@ -9,6 +9,12 @@ class UploadedFile < Article attr_accessible :uploaded_data, :title + include Noosfero::Plugin::HotSpot + + def environment + profile.environment + end + def self.type_name _('File') end diff --git a/features/step_definitions/noosfero_steps.rb b/features/step_definitions/noosfero_steps.rb index 56f66e0..4e2649c 100644 --- a/features/step_definitions/noosfero_steps.rb +++ b/features/step_definitions/noosfero_steps.rb @@ -118,6 +118,7 @@ Given /^the following (articles|events|blogs|folders|forums|galleries|uploaded f language = item.delete("language") category = item.delete("category") filename = item.delete("filename") + mime = item.delete("mime") || 'binary/octet-stream' translation_of_id = nil if item["translation_of"] if item["translation_of"] != "nil" @@ -131,7 +132,7 @@ Given /^the following (articles|events|blogs|folders|forums|galleries|uploaded f :language => language, :translation_of_id => translation_of_id) if !filename.blank? - item.merge!(:uploaded_data => fixture_file_upload("/files/#{filename}", 'binary/octet-stream')) + item.merge!(:uploaded_data => fixture_file_upload("/files/#{filename}", mime)) end result = klass.new(item) if !parent.blank? @@ -366,6 +367,10 @@ Then /^The page should not contain "(.*)"$/ do |selector| page.should have_no_css("#{selector}") end +Then /^The page should contain only (\d+) "(.*)"$/ do |count, selector| + page.should have_css(selector, :count => count) +end + Given /^the mailbox is empty$/ do ActionMailer::Base.deliveries = [] end @@ -653,6 +658,15 @@ Given /^the environment is configured to (.*) after signup$/ do |option| environment.save end +When /^I click "(.*?)"$/ do |selector| + find(selector).click +end + +Then /^the element "(.*)" has class "(.*)"$/ do |el_selector, el_class| + class_list = find(el_selector)[:class].split(' ') + class_list.should include(el_class) +end + When /^wait for the captcha signup time$/ do environment = Environment.default sleep environment.min_signup_delay + 1 @@ -671,3 +685,7 @@ Given /^the field (.*) is public for all users$/ do |field| person.save! end end + +When(/^I press "(.*?)" by selector$/) do |selector| + page.execute_script("jQuery('#{selector}').click();") +end diff --git a/lib/file_presenter.rb b/lib/file_presenter.rb index cc169fe..3f925d4 100644 --- a/lib/file_presenter.rb +++ b/lib/file_presenter.rb @@ -124,3 +124,8 @@ end Dir.glob(File.join('app', 'presenters', '*.rb')) do |file| load file end + +# Preload FilePresenters from plugins to allow `FilePresenter.for()` to work +Dir.glob(File.join('plugins', '*', 'lib', 'presenters', '*.rb')) do |file| + load file +end diff --git a/lib/noosfero/plugin/settings.rb b/lib/noosfero/plugin/settings.rb index ab6f8e7..433971d 100644 --- a/lib/noosfero/plugin/settings.rb +++ b/lib/noosfero/plugin/settings.rb @@ -10,7 +10,8 @@ class Noosfero::Plugin::Settings end def settings - @base.settings["#{@plugin.public_name}_plugin".to_sym] ||= {} + settings_field = @base.class.settings_field + @base.send(settings_field)["#{@plugin.public_name}_plugin".to_sym] ||= {} end def method_missing(method, *args, &block) diff --git a/plugins/html5_video/features/video_player.feature b/plugins/html5_video/features/video_player.feature new file mode 100644 index 0000000..6c41a64 --- /dev/null +++ b/plugins/html5_video/features/video_player.feature @@ -0,0 +1,20 @@ +Feature: video player + As a noosfero visitor + I want to view a video page and play it + + Background: + Given plugin Html5Video is enabled on environment + And the following users + | login | name | + | joaosilva | Joao Silva | + And the following uploaded files + | owner | filename | mime | + | joaosilva | ../videos/old-movie.mpg | video/mpeg | + And there are no pending jobs + + @selenium + Scenario: controls must work + Given I am on /joaosilva/old-movie.mpg?view=true + Then The page should contain only 2 ".video-player .quality li.ui-button" + When I click ".video-player .video-box .zoom" + Then the element ".video-player" has class "zoom-in" diff --git a/plugins/html5_video/lib/file_presenter/video.rb b/plugins/html5_video/lib/file_presenter/video.rb deleted file mode 100644 index 3dac290..0000000 --- a/plugins/html5_video/lib/file_presenter/video.rb +++ /dev/null @@ -1,10 +0,0 @@ -class FilePresenter::Video < FilePresenter - def self.accepts?(f) - return nil if !f.respond_to?(:content_type) || f.content_type.nil? - ( f.content_type[0..4] == 'video' ) ? 10 : nil - end - - def short_description - _('Video (%s)') % content_type.split('/')[1].upcase - end -end diff --git a/plugins/html5_video/lib/html5_video_plugin.rb b/plugins/html5_video/lib/html5_video_plugin.rb index 953e511..3b02913 100644 --- a/plugins/html5_video/lib/html5_video_plugin.rb +++ b/plugins/html5_video/lib/html5_video_plugin.rb @@ -1,13 +1,54 @@ class Html5VideoPlugin < Noosfero::Plugin - FilePresenter::Video - def self.plugin_name "HTML5 Video" end + def stylesheet? + true + end + + def js_files + ['video-channel.js'] + end + def self.plugin_description _("A plugin to enable the video suport, with auto conversion for the web.") end + def content_types + [Html5VideoPlugin::VideoChannel] + end + + def view_page_layout(controller, page) + if FilePresenter.for(page).is_a? FilePresenter::Video and controller.params[:display] == 'iframe' + 'html5_video_plugin_iframe' + end + end + + def uploaded_file_after_create_callback(uploaded_file) + full_filename = uploaded_file.full_filename + file_presenter = FilePresenter.for(uploaded_file) + if file_presenter.is_a? FilePresenter::Video + job = Html5VideoPlugin::CreateVideoPreviewJob.new + job.file_type = uploaded_file.class.name + job.file_id = uploaded_file.id + job.full_filename = full_filename + Delayed::Job.enqueue job, priority: 10 + [ + [:OGV, :tiny, 11], + [:WEBM, :tiny, 12], + [:OGV, :nice, 13], + [:WEBM, :nice, 14], + ].each do |format, size, priority| + job = Html5VideoPlugin::CreateVideoForWebJob.new + job.file_type = uploaded_file.class.name + job.file_id = uploaded_file.id + job.full_filename = full_filename + job.format = format + job.size = size + Delayed::Job.enqueue job, priority: priority + end + end + end end diff --git a/plugins/html5_video/lib/html5_video_plugin/create_video_for_web_job.rb b/plugins/html5_video/lib/html5_video_plugin/create_video_for_web_job.rb new file mode 100644 index 0000000..26c2e84 --- /dev/null +++ b/plugins/html5_video/lib/html5_video_plugin/create_video_for_web_job.rb @@ -0,0 +1,161 @@ +class Html5VideoPlugin::CreateVideoForWebJob + + # TODO: we must to add timeout to ffmpeg to allow this object to set this on error: + # @video.video_info[:conversion] = :timeout + # TODO: must remove web folder when delete the video + + attr_accessor :file_type, :file_id, :full_filename, :format, :size + + def perform + return unless file_type.constantize.exists?(file_id) + @ffmpeg = Html5VideoPlugin::Ffmpeg.new + + @video = FilePresenter.for file_type.constantize.find(file_id) + throw "Expected file #{file_id} to be a video" unless @video.is_a?(FilePresenter::Video) + + @video_file = full_filename + + @info = @ffmpeg.get_video_info @video_file + + if @info[:error][:code] == 0 + @video.original_video = @info.except :output + register_conv_status 'started' + @video.save! + else + register_conv_status 'error reading' + register_conv_error @info[:error] + @video.save! + Rails.logger.error "FFmpeg ERROR while reading '#{@video_file}': #{@info[:error][:message]}" + return + end + + @orig_size = @info.video_stream[0][:size] + @brate = @info.video_stream[0][:bitrate] || @info[:global_bitrate] || 400 + + convert_to_tiny if size == :tiny + convert_to_nice if size == :nice + @video.save! + 1 + end + + def register_conv_status( status ) + @video.web_versions[format] ||= {} + @video.web_versions[format][size] ||= {} + @video.web_versions[format][size][:status] = status + @video.save! + end + + def register_conv_conf( conf ) + @video.web_versions[format] ||= {} + @video.web_versions[format][size] ||= {} + @video.web_versions[format][size].merge! conf + @video.save! + end + + def register_conv_error( error ) + @video.web_versions[format] ||= {} + @video.web_versions[format][size] ||= {} + @video.web_versions[format][size][:error] = error + @video.save! + end + + def is_ogv + @info[:type] == 'ogv' && + @info.video_stream[0][:codec] == 'theora' && + @info.audio_stream[0][:codec] == 'vorbis' + end + + def is_mp4 + @info[:type] == 'mp4' && + @info.video_stream[0][:codec] == 'libx264' && + @info.audio_stream[0][:codec] == 'libfaac' + end + + def is_webm + @info[:type] == 'webm' && + @info.video_stream[0][:codec] == 'libvpx' + end + + def register_conversion response + conf = response[:conf].clone + if response[:error][:code] == 0 + conf[:path] = conf[:out].sub /^.*(\/articles)/, '\1' + conf.delete :in + conf.delete :out + register_conv_conf conf + register_conv_status 'done' + @video.save! + else + register_conv_status 'error converting' + register_conv_conf conf + error = response[:error].clone + error[:output] = response[:output] + register_conv_error error + Rails.logger.error "FFmpeg ERROR while converting '#{conf[:in]}' to #{conf[:type]}: #{response[:error][:message]}" + end + end + + def is_big + @orig_size[:w] > 400 || @brate >= 400 + end + + def is_toobig + @orig_size[:w] > 600 + end + + # The smaller version for slow connections + def convert_to_tiny + audio_stream = @video.original_video[:streams].find{|s| s[:type] == 'audio'} + abrate = audio_stream.nil? ? 64 : audio_stream[:bitrate] || 64 + abrate = 64 if abrate > 64 + conf = { :size_name=>'tiny', :in=>@video_file, + :fps=>12, :vbrate=>250, :abrate=>abrate } + if is_big + # Low weight video dimension for each Aspect Ratio: + # * 320x240 for 4:3 + # * 320x180 for 16:9 + # This are Best and Good values with the same width based on this page: + # http://www.flashsupport.com/books/fvst/files/tools/video_sizes.html + h = ( 320.0 / (@orig_size[:w].to_f/@orig_size[:h].to_f) ).round + h -= 1 if h % 2 == 1 + size = { :w=>320, :h=>h } + conf[:size] = size + end + if format == :OGV && ( is_big || ! is_ogv ) + conf[:file_name] = 'tiny.ogv' + register_conversion @ffmpeg.make_ogv_for_web(conf) + end + if format == :WEBM && ( is_big || ! is_webm ) + conf[:file_name] = 'tiny.webm' + register_conversion @ffmpeg.make_webm_for_web(conf) + end + end + + # The nicer common version + def convert_to_nice + if is_toobig + # Max video dimension for each Aspect Ratio: + # * 576x432 for 4:3 + # * 576x324 for 16:9 + # This are Best and Good values with the same width based on this page: + # http://www.flashsupport.com/books/fvst/files/tools/video_sizes.html + # Width 640 has Better result to 16:9, but that will also make bigger + # file weight. + h = ( 576.0 / (@orig_size[:w].to_f/@orig_size[:h].to_f) ).round + size = { :w=>576, :h=>h.to_i } + conf = { :in=>@video_file, :size=>size, :vbrate=>@brate } + else + conf = { :in=>@video_file, :vbrate=>@brate } + end + conf[:size_name] = 'nice' + if format == :OGV && ( is_toobig || ! is_ogv ) + conf[:file_name] = 'nice.ogv' + register_conversion @ffmpeg.make_ogv_for_web(conf) + end + if format == :WEBM && ( is_toobig || ! is_webm ) + conf[:file_name] = 'nice.webm' + register_conversion @ffmpeg.make_webm_for_web(conf) + end + end + +end diff --git a/plugins/html5_video/lib/html5_video_plugin/create_video_preview_job.rb b/plugins/html5_video/lib/html5_video_plugin/create_video_preview_job.rb new file mode 100644 index 0000000..8fdfc3d --- /dev/null +++ b/plugins/html5_video/lib/html5_video_plugin/create_video_preview_job.rb @@ -0,0 +1,27 @@ +class Html5VideoPlugin::CreateVideoPreviewJob + + attr_accessor :file_type, :file_id, :full_filename + + def perform + return unless file_type.constantize.exists?(file_id) + ffmpeg = Html5VideoPlugin::Ffmpeg.new + + video = FilePresenter.for file_type.constantize.find(file_id) + throw "Expected file #{file_id} to be a video" unless video.is_a? FilePresenter::Video + + video_file = full_filename + + response = ffmpeg.video_thumbnail(video_file) + + if response.kind_of?(Hash) && response[:error] && response[:error][:code] != 0 + video.previews = :fail + video.save! + Rails.logger.error "ERROR while generating '#{video_file}' image preview: #{response[:error][:message]}" + return + end + + video.previews = response + video.save! + end + +end diff --git a/plugins/html5_video/lib/html5_video_plugin/ffmpeg.rb b/plugins/html5_video/lib/html5_video_plugin/ffmpeg.rb new file mode 100644 index 0000000..0b7ee14 --- /dev/null +++ b/plugins/html5_video/lib/html5_video_plugin/ffmpeg.rb @@ -0,0 +1,354 @@ +# Works for ffmpeg version 2.8.6-1~bpo8 shiped by Debian Jessie Backports +# https://packages.debian.org/jessie-backports/ffmpeg +# Add this line to your /etc/apt/sources.list: +# deb http://http.debian.net/debian jessie-backports main +# then: aptitude install ffmpeg +class Html5VideoPlugin::Ffmpeg + + def run(*parameters) + parameters = parameters.flatten + cmd = ['ffmpeg'] + parameters.map do |p| + p.kind_of?(Symbol) ? '-'+p.to_s : p.to_s + end + io = IO.popen({'LANG'=>'C.UTF-8'}, cmd, err: [:child, :out]) + output = io.read + io.close + response = { + error: { :code => 0, :message => '' }, + parameters: parameters, + output: output + } + if $?.exitstatus != 0 then + if $?.exitstatus == 127 then + throw 'There is no FFmpeg installed!' + end + response[:error][:code] = -1 + response[:error][:message] = _('Unknow error') + if match = /\n\s*([^\n]*): No such file or directory\s*\n/i.match(output) + response[:error][:code] = 1 + response[:error][:message] = _('No such file or directory "%s".') % match[1] + elsif output =~ /At least one output file must be specified/i + response[:error][:code] = 2 + response[:error][:message] = _('No output defined.') + elsif match = /\n\s*Unknown encoder[\s']+([^\s']+)/i.match(output) + response[:error][:code] = 3 + response[:error][:message] = _('Unknown encoder "%s".') % match[1] + elsif output =~ /\n\s*Error while opening encoder for output/i + response[:error][:code] = 4 + response[:error][:message] = _('Error while opening encoder for output - maybe incorrect parameters such as bit rate, frame rate, width or height.') + elsif match = /\n\s*Could not open '([^\s']+)/i.match(output) + response[:error][:code] = 5 + response[:error][:message] = _('Could not open "%s".') % match[1] + elsif match = /\n\s*Unsupported codec (.*) for (.*) stream (.*)/i.match(output) + response[:error][:code] = 6 + response[:error][:message] = _('Unsupported codec %{codec} for %{act} stream %{id}.') % + { :codec=>match[1], :act=>match[2], :id=>match[3] } + elsif output =~ /Unable to find a suitable output format/i + response[:error][:code] = 7 + response[:error][:message] = _('Unable to find a suitable output format for %{file}.') % + { :file=>parameters[-1] } + elsif output =~ /Invalid data found when processing input/i + response[:error][:code] = 8 + response[:error][:message] = _('Invalid data found when processing input.') + end + end + return response + end + + def register_information + response = self.run(:formats)[:output] + @@version = /^\s*FFmpeg version ([0-9.]+)/i.match(response)[1] + @@formats = {} + response.split('--')[-1].strip.split("\n").each do |line| + if pieces = / (.)(.) ([^\s]+)\s+([^\s].*)/.match(line) + @@formats[pieces[3].to_sym] = { + demux: ( pieces[1] == 'D' ), + mux: ( pieces[2] == 'E' ), + description: pieces[4].strip + } + end + end + response = self.run(:codecs)[:output] + @@codecs = {} + response.split('--')[-1].strip.split("\n").each do |line| + if pieces = / (.)(.)(.)(.)(.)(.) ([^\s]+)\s+([^\s].*)/.match(line) + @@codecs[pieces[7].to_sym] = { + decode: ( pieces[1] == 'D' ), + encode: ( pieces[2] == 'E' ), + draw_horiz_band: ( pieces[4] == 'S' ), + direct_rendering: ( pieces[5] == 'D' ), + wf_trunc: ( pieces[6] == 'T' ), + type: ( + if pieces[3] == 'V'; :video + elsif pieces[3] == 'A'; :audio + elsif pieces[3] == 'S'; :subtitle + else :unknown; end + ), + description: pieces[8].strip + } + end + end + { + version: @@version, + formats: @@formats, + codecs: @@codecs + } + end + + def timestr_to_secs str + return nil if ! /^[0-9]{2}:[0-9]{2}:[0-9]{2}$/.match str + t = str.split(':').map(&:to_i) + t[0]*60*60 + t[1]*60 + t[2] + end + + def get_stream_info(stream_str) + stream_info = { :type => 'undefined' } + stream_info[:type] = 'video' if / Video:/.match(stream_str) + stream_info[:type] = 'audio' if / Audio:/.match(stream_str) + { + id: [ /^\s*Stream ([^:(]+)/ ], + codec: [ / (?:Audio|Video):\s*([^,\s]+)/x ], + bitrate: [ / ([0-9]+) kb\/s\b/ , :to_i ], + frequency: [ / ([0-9]+) Hz\b/ , :to_i ], + channels: [ / ([0-9]+) channels\b/ , :to_i ], + framerate: [ / ([0-9.]+) (fps|tbr)\b/ , :to_f ], + size: [ / ([0-9]+x[0-9]+)[, ]/ ], + }.each do |att, params| + re = params[0] + method = params[1] || :strip + if match = re.match(stream_str) then + stream_info[att] = match[1].send(method) + end + end + if stream_info[:size] + size_array = stream_info[:size].split('x').map{|s| s.to_i} + stream_info[:size] = { :w=>size_array[0], :h=>size_array[1] } + end + stream_info + end + + def webdir_for_original_video(video) + webdir = File.dirname(video) +'/web' + Dir.mkdir(webdir) if ! File.exist?(webdir) + webdir + end + + def valid_abrate_for_web(info) + if brate = info.audio_stream[0][:bitrate] + brate = 8 if brate < 8 + (brate>128)? 128 : brate + else + 48 + end + end + + def valid_vbrate_for_web(info) + if brate = info.video_stream[0][:bitrate] || info[:global_bitrate] + brate = 128 if brate < 128 + (brate>1024)? 1024 : brate + else + 400 + end + end + + def valid_size_for_web(info) + orig_size = info.video_stream[0][:size] + if info.video_stream[0][:size][:w] > 640 + h = 640.0 / (orig_size[:w].to_f/orig_size[:h].to_f) + { :w=>640, :h=>h.to_i } + else + { :w=>orig_size[:w].to_i, :h=>orig_size[:h].to_i } + end + end + + def validate_conversion_conf_for_web(conf, file_type) + conf = conf.clone + if conf[:abrate].nil? || conf[:vbrate].nil? || conf[:size].nil? + info = get_video_info(conf[:in]) + if info[:error][:code] == 0 + conf[:abrate] ||= valid_abrate_for_web info + conf[:vbrate] ||= valid_vbrate_for_web info + conf[:size] ||= valid_size_for_web info + end + end + result_dir = webdir_for_original_video conf[:in] + size_str = "#{conf[:size][:w]}x#{conf[:size][:h]}" + unless conf[:file_name] + conf[:file_name] = "#{size_str}_#{conf[:vbrate]}.#{file_type}" + end + conf[:out] = result_dir+'/'+conf[:file_name] + conf + end + + public + + def get_video_info(file) + response = self.run :i, file + if response[:error][:code] == 2 + # No output is not an error on this context + response[:error][:code] = 0 + response[:error][:message] = _('Success.') + end + response[:metadata] = {} + { + author: /\n\s*author[\t ]*:([^\n]*)\n/i, + title: /\n\s*title[\t ]*:([^\n]*)\n/i, + comment: /\n\s*comment[\t ]*:([^\n]*)\n/i + }.each do |att, re| + if match = re.match(response[:output]) then + response[:metadata][att] = match[1].strip + end + end + { + type: /\nInput #0, ([a-z0-9]+), from/, + duration: /\n\s*Duration:[\t ]*([0-9:]+)[^\n]* bitrate:/, + global_bitrate: /\n\s*Duration:[^\n]* bitrate:[\t ]*([0-9]+)/ + }.each do |att, re| + if match = re.match(response[:output]) then + response[att] = match[1].strip + end + end + response[:duration] = timestr_to_secs response[:duration] + response[:global_bitrate] = response[:global_bitrate].to_i + response[:streams] = [] + response[:output].split("\n").grep(/^\s*Stream /).each do |stream| + response[:streams] << get_stream_info(stream) + end + def response.video_stream + self[:streams].select {|s| s[:type] == 'video' } + end + def response.audio_stream + self[:streams].select {|s| s[:type] == 'audio' } + end + return response + end + + def convert2ogv(conf) + conf[:type] = :OGV + conf[:vbrate] ||= 600 + parameters = [ :i, conf[:in], :y, :'b:v', "#{conf[:vbrate]}k", + :f, 'ogg', :acodec, 'libvorbis', :vcodec, 'libtheora' + ] + parameters << :s << "#{conf[:size][:w]}x#{conf[:size][:h]}" if conf[:size] + parameters << :'b:a' << "#{conf[:abrate]}k" if conf[:abrate] + # Vorbis dá pau com -ar 8000Hz ??? + parameters << :r << conf[:fps] if conf[:fps] + parameters << conf[:out] + response = self.run parameters + response[:conf] = conf + response + end + + def convert2mp4(conf) + conf[:type] = :MP4 + conf[:vbrate] ||= 600 + parameters = [ :i, conf[:in], :y, :'b:v', "#{conf[:vbrate]}k", + :preset, 'slow', :f, 'mp4', :acodec, 'aac', :vcodec, 'libx264', + :strict, '-2' + ] + parameters << :s << "#{conf[:size][:w]}x#{conf[:size][:h]}" if conf[:size] + parameters << :'b:a' << "#{conf[:abrate]}k" if conf[:abrate] + parameters << :r << conf[:fps] if conf[:fps] + parameters << conf[:out] + response = self.run parameters + response[:conf] = conf + response + end + + def convert2webm(conf) + conf[:type] = :WEBM + conf[:vbrate] ||= 600 + parameters = [ :i, conf[:in], :y, :'b:v', "#{conf[:vbrate]}k", + :f, 'webm', :acodec, 'libvorbis', :vcodec, 'libvpx' + ] + parameters << :s << "#{conf[:size][:w]}x#{conf[:size][:h]}" if conf[:size] + parameters << :'b:a' << "#{conf[:abrate]}k" if conf[:abrate] + parameters << :r << conf[:fps] if conf[:fps] + parameters << conf[:out] + response = self.run parameters + response[:conf] = conf + response + end + + def make_ogv_for_web(conf) + conf = validate_conversion_conf_for_web conf, :ogv + convert2ogv(conf) + end + + def make_mp4_for_web(conf) + conf = validate_conversion_conf_for_web conf, :mp4 + convert2mp4(conf) + end + + def make_webm_for_web(conf) + conf = validate_conversion_conf_for_web conf, :webm + convert2webm(conf) + end + + # video_thumbnail creates 2 preview images on the sub directory web + # from the video file parent dir. This preview images are six concatenated + # frames in one image each. The frames have fixed dimension. The bigger + # preview has frames with in 160x120, and smaller has frames whit in 107x80. + # Use this helper only on the original movie to have only one "web" sub-dir. + def video_thumbnail(video) + result_dir = webdir_for_original_video video + info = get_video_info(video) + if info[:duration] < 15 + pos = 1 + duration = info[:duration] - 2 + frate = ( 7.0 / duration ).ceil + else + pos = ( info[:duration] / 2.5 ).ceil + duration = 7 + frate = 1 + end + response = self.run :i, video, :ss, pos, :t, duration, :r, frate, + :s, '320x240', result_dir+'/f%d.png' + img_names = [ '/preview_160x120.jpg', '/preview_107x80.jpg' ] + if response[:error][:code] == 0 + imgs = (2..7).map { |num| + img = result_dir+"/f#{num}.png" + File.exists?(img) ? img : nil + }.compact + if imgs.size != 6 + Rails.logger.error "Problem to create thumbs for video #{video} ???" + end + imgs = Magick::ImageList.new *imgs + imgs.montage{ + self.geometry='160x120+0+0' + self.tile="1x#{imgs.size}" + self.frame = "0x0+0+0" + }.write result_dir+img_names[0] + imgs.montage{ + self.geometry='107x80+0+0' + self.tile="1x#{imgs.size}" + self.frame = "0x0+0+0" + }.write result_dir+img_names[1] + end + + f_num = 1 + while File.exists? result_dir+"/f#{f_num}.png" do + File.delete result_dir+"/f#{f_num}.png" + f_num += 1 + end + + if response[:error][:code] == 0 + return { big: '/web'+img_names[0], thumb: '/web'+img_names[1] } + else + return response + end + end + + def version + @@version ||= register_information[:version] + end + + def formats + @@formats ||= register_information[:formats] + end + + def codecs + @@codecs ||= register_information[:codecs] + end + +end diff --git a/plugins/html5_video/lib/html5_video_plugin/video_channel.rb b/plugins/html5_video/lib/html5_video_plugin/video_channel.rb new file mode 100644 index 0000000..2b0bbc2 --- /dev/null +++ b/plugins/html5_video/lib/html5_video_plugin/video_channel.rb @@ -0,0 +1,30 @@ +class Html5VideoPlugin::VideoChannel < Folder + + def self.short_description + _('Video Channel') + end + + def self.description + _('A video channel, where you can make your own web TV.') + end + + include ActionView::Helpers::TagHelper + def to_html(options={}) + article = self + lambda do + render :file => 'content_viewer/video_channel', :locals => {:article => article} + end + end + + def video_channel? + true + end + + def self.icon_name(article = nil) + 'videochannel' + end + + def accept_article? + false + end +end diff --git a/plugins/html5_video/lib/presenters/video.rb b/plugins/html5_video/lib/presenters/video.rb new file mode 100644 index 0000000..f923539 --- /dev/null +++ b/plugins/html5_video/lib/presenters/video.rb @@ -0,0 +1,145 @@ +class FilePresenter::Video < FilePresenter + + def self.accepts?(f) + return nil if !f.respond_to?(:content_type) || f.content_type.nil? + ( f.content_type[0..4] == 'video' ) ? 10 : nil + end + + def short_description + _('Video (%s)') % content_type.split('/')[1].upcase + end + + def meta_data #video_info + Noosfero::Plugin::Settings.new(encapsulated_file, Html5VideoPlugin) + end + + def original_video + meta_data.original_video ||= {} + end + + def original_video=(hash) + meta_data.original_video = hash + end + + def web_versions + meta_data.web_versions ||= {} + end + + def web_versions=(hash) + meta_data.web_versions = hash + end + + # adds the orig version tho the web_versions if that is a valid to HTML5 + def web_versions! + list = web_versions.clone + streams = original_video.empty? ? [] : original_video[:streams] + video_stream = streams.find{|s| s[:type] == 'video' } + audio_stream = streams.find{|s| s[:type] == 'audio' } + return list unless video_stream && audio_stream + type = original_video[:type].to_s.upcase.to_sym + type = :OGV if video_stream[:codec]=='theora' && original_video[:type]=='ogg' + if [:OGV, :MP4, :WEBM].include? type + vb = video_stream[:bitrate] || original_video[:global_bitrate] || 0 + ab = audio_stream[:bitrate] || 0 + info = { + :original => true, + :file_name => File.basename(public_filename), + :abrate => ab, + :vbrate => vb, + :size => video_stream[:size], + :size_name => 'orig', + :status => 'done', + :type => type, + :path => public_filename + } + list[type][:orig] = info + end + list + end + + def ready_web_versions + ready = {} + web_versions!.select do |type, type_block| + ready[type] = {} + type_block.select do |size, size_block| + ready[type][size] = size_block if size_block[:status] == 'done' + end + end + ready + end + + def has_ogv_version + not ready_web_versions[:OGV].blank? + end + + def has_mp4_version + not ready_web_versions[:MP4].blank? + end + + def has_webm_version + not ready_web_versions[:WEBM].blank? + end + + def has_web_version + ready = ready_web_versions + not (ready[:OGV].blank? and ready[:MP4].blank? and ready[:WEBM].blank?) + end + + def tiniest_web_version( type ) + return nil if ready_web_versions[type].nil? + video = ready_web_versions[type]. + select{|size,data| data[:status] == 'done' }. + sort_by{|v| v[1][:vbrate] }.first + video ? video[1] : nil + end + + #TODO: add this to the user interface: + def web_version_jobs + #FIXME: in a newer version, the Delayed::Job may be searcheable in a uglyless way. + Delayed::Job.where("handler LIKE '%CreateVideoForWebJob%file_id: #{self.id}%'").all + #Delayed::Job.all :conditions => ['handler LIKE ?', + # "%CreateVideoForWebJob%file_id: #{self.id}%"] + end + + def web_preview_jobs + #FIXME: in a newer version, the Delayed::Job may be searcheable in a uglyless way. + Delayed::Job.where("handler LIKE '%CreateVideoPreviewJob%file_id: #{self.id}%'").all + #Delayed::Job.all :conditions => ['handler LIKE ?', + # "%CreateVideoPreviewJob%file_id: #{self.id}%"] + end + + def has_previews? + not(previews.nil?) && previews.kind_of?(Hash) && !previews.empty? + end + + def previews + meta_data.image_previews + end + + def previews=(hash) + meta_data.image_previews = hash + end + + def image_preview(size=nil) + if has_previews? && previews[size] + File.dirname( public_filename ) + previews[size] + else + "/plugins/html5_video/images/video-preview-#{size}.png" + end + end + + def conversion_errors + errors = {} + web_versions!.select do |type, type_block| + type_block.select do |size, conv_info| + if conv_info[:status] == 'error converting' + errors[type] ||= {} + err_base = {:message=>_('Undefined'), :code=>-2, :output=>'undefined'} + errors[type][size] = err_base.merge( conv_info[:error] || {} ) + end + end + end + errors + end + +end diff --git a/plugins/html5_video/po/html5_video.pot b/plugins/html5_video/po/html5_video.pot index fc09a17..6aea000 100644 --- a/plugins/html5_video/po/html5_video.pot +++ b/plugins/html5_video/po/html5_video.pot @@ -6,9 +6,10 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: 1.3~rc2-1-ga15645d\n" -"POT-Creation-Date: 2015-10-30 16:35-0300\n" -"PO-Revision-Date: 2015-08-06 17:21-0300\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-08-19 19:54+0000\n" +"PO-Revision-Date: 2016-08-19 19:54+0000\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -17,10 +18,128 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: plugins/html5_video/lib/file_presenter/video.rb:8 +#: ../lib/html5_video_plugin.rb:16 +msgid "A plugin to enable the video suport, with auto conversion for the web." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:26 +msgid "Unknow error" +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:29 +msgid "No such file or directory \"%s\"." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:32 +msgid "No output defined." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:35 +msgid "Unknown encoder \"%s\"." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:38 +msgid "" +"Error while opening encoder for output - maybe incorrect parameters such as bi" +"t rate, frame rate, width or height." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:41 +msgid "Could not open \"%s\"." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:44 +msgid "Unsupported codec %{codec} for %{act} stream %{id}." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:48 +msgid "Unable to find a suitable output format for %{file}." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:52 +msgid "Invalid data found when processing input." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:190 +msgid "Success." +msgstr "" + +#: ../lib/html5_video_plugin/video_channel.rb:4 +msgid "Video Channel" +msgstr "" + +#: ../lib/html5_video_plugin/video_channel.rb:8 +msgid "A video channel, where you can make your own web TV." +msgstr "" + +#: ../lib/presenters/video.rb:9 msgid "Video (%s)" msgstr "" -#: plugins/html5_video/lib/html5_video_plugin.rb:10 -msgid "A plugin to enable the video suport, with auto conversion for the web." +#: ../lib/presenters/video.rb:137 +msgid "Undefined" +msgstr "" + +#: ../views/content_viewer/_video_player.html.erb:21 +msgid "Sorry, your browser doesn’t support video." +msgstr "" + +#: ../views/content_viewer/_video_player.html.erb:22 +msgid "Please try the new %s or %s." +msgstr "" + +#: ../views/content_viewer/_video_player.html.erb:27 +msgid "Download" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:7 +msgid "This channel contains no videos yet" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:14 +#: ../views/file_presenter/_video.html.erb:15 +msgid "Quality options" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:18 +msgid "Tags" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:22 +#: ../views/file_presenter/_video.html.erb:17 +msgid "Description" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:66 +msgid "This channel has one video waiting to be converted" +msgid_plural "This channel has %d videos waiting to be converted" +msgstr[0] "" +msgstr[1] "" + +#: ../views/content_viewer/video_channel.html.erb:83 +msgid "Non video files" +msgstr "" + +#: ../views/file_presenter/_video.html.erb:7 +msgid "Queued to generate the web version. Come back soon." +msgstr "" + +#: ../views/file_presenter/_video.html.erb:11 +msgid "This video is not queued to the video conversor. Contact the site admin." +msgstr "" + +#: ../views/file_presenter/_video.html.erb:34 +msgid "Video conversion errors" +msgstr "" + +#: ../views/file_presenter/_video.html.erb:44 +msgid "Error while converting %{orig_type} to %{new_type}, %{size} size." +msgstr "" + +#: ../views/file_presenter/_video.html.erb:47 +msgid "Code %s" +msgstr "" + +#: ../views/file_presenter/_video.html.erb:49 +msgid "display full output" msgstr "" diff --git a/plugins/html5_video/po/pt/html5_video.po b/plugins/html5_video/po/pt/html5_video.po index 321a237..6eeb22e 100644 --- a/plugins/html5_video/po/pt/html5_video.po +++ b/plugins/html5_video/po/pt/html5_video.po @@ -12,11 +12,11 @@ msgid "" msgstr "" "Project-Id-Version: 1.3~rc2-1-ga15645d\n" -"POT-Creation-Date: 2015-10-30 16:35-0300\n" +"POT-Creation-Date: 2016-08-19 19:54+0000\n" "PO-Revision-Date: 2014-12-18 18:40-0200\n" "Last-Translator: Luciano Prestes Cavalcanti \n" -"Language-Team: Portuguese \n" +"Language-Team: Portuguese \n" "Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -24,11 +24,128 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 2.0\n" -#: plugins/html5_video/lib/file_presenter/video.rb:8 +#: ../lib/html5_video_plugin.rb:16 +msgid "A plugin to enable the video suport, with auto conversion for the web." +msgstr "Um plugin para habilitar suporte de video, com conversão de audio para a web." + +#: ../lib/html5_video_plugin/ffmpeg.rb:26 +msgid "Unknow error" +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:29 +msgid "No such file or directory \"%s\"." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:32 +msgid "No output defined." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:35 +msgid "Unknown encoder \"%s\"." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:38 +msgid "" +"Error while opening encoder for output - maybe incorrect parameters such as bi" +"t rate, frame rate, width or height." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:41 +msgid "Could not open \"%s\"." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:44 +msgid "Unsupported codec %{codec} for %{act} stream %{id}." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:48 +msgid "Unable to find a suitable output format for %{file}." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:52 +msgid "Invalid data found when processing input." +msgstr "" + +#: ../lib/html5_video_plugin/ffmpeg.rb:190 +msgid "Success." +msgstr "" + +#: ../lib/html5_video_plugin/video_channel.rb:4 +msgid "Video Channel" +msgstr "" + +#: ../lib/html5_video_plugin/video_channel.rb:8 +msgid "A video channel, where you can make your own web TV." +msgstr "" + +#: ../lib/presenters/video.rb:9 msgid "Video (%s)" msgstr "Vídeo (%s)" -#: plugins/html5_video/lib/html5_video_plugin.rb:10 -msgid "A plugin to enable the video suport, with auto conversion for the web." +#: ../lib/presenters/video.rb:137 +msgid "Undefined" +msgstr "" + +#: ../views/content_viewer/_video_player.html.erb:21 +msgid "Sorry, your browser doesn’t support video." +msgstr "" + +#: ../views/content_viewer/_video_player.html.erb:22 +msgid "Please try the new %s or %s." +msgstr "" + +#: ../views/content_viewer/_video_player.html.erb:27 +msgid "Download" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:7 +msgid "This channel contains no videos yet" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:14 +#: ../views/file_presenter/_video.html.erb:15 +msgid "Quality options" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:18 +msgid "Tags" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:22 +#: ../views/file_presenter/_video.html.erb:17 +msgid "Description" +msgstr "" + +#: ../views/content_viewer/video_channel.html.erb:66 +msgid "This channel has one video waiting to be converted" +msgid_plural "This channel has %d videos waiting to be converted" +msgstr[0] "" +msgstr[1] "" + +#: ../views/content_viewer/video_channel.html.erb:83 +msgid "Non video files" +msgstr "" + +#: ../views/file_presenter/_video.html.erb:7 +msgid "Queued to generate the web version. Come back soon." +msgstr "" + +#: ../views/file_presenter/_video.html.erb:11 +msgid "This video is not queued to the video conversor. Contact the site admin." +msgstr "" + +#: ../views/file_presenter/_video.html.erb:34 +msgid "Video conversion errors" +msgstr "" + +#: ../views/file_presenter/_video.html.erb:44 +msgid "Error while converting %{orig_type} to %{new_type}, %{size} size." +msgstr "" + +#: ../views/file_presenter/_video.html.erb:47 +msgid "Code %s" +msgstr "" + +#: ../views/file_presenter/_video.html.erb:49 +msgid "display full output" msgstr "" -"Um plugin para habilitar suporte de video, com conversão de audio para a web." diff --git a/plugins/html5_video/public/images/video-preview-big.png b/plugins/html5_video/public/images/video-preview-big.png new file mode 100644 index 0000000..daffb30 Binary files /dev/null and b/plugins/html5_video/public/images/video-preview-big.png differ diff --git a/plugins/html5_video/public/images/video-preview-thumb.png b/plugins/html5_video/public/images/video-preview-thumb.png new file mode 100644 index 0000000..1627117 Binary files /dev/null and b/plugins/html5_video/public/images/video-preview-thumb.png differ diff --git a/plugins/html5_video/public/style.css b/plugins/html5_video/public/style.css new file mode 100644 index 0000000..11ce481 --- /dev/null +++ b/plugins/html5_video/public/style.css @@ -0,0 +1,175 @@ +.video-list { + margin: 0px; + padding: 0px; + text-align: left; +} + +.video-list-item { + margin: 2px 1px; + padding: 0px; + list-style: none; + display: inline-block; + width: 160px; + height: 120px; + overflow: hidden; +} + +.video-list-item a { + display: block; + height: 100%; + background-position: 50% -120px; + background-repeat: no-repeat; + position: relative; +} + +.video-list-item .frame-fade { + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + display: block; +} + +@keyframes fadein { + from { opacity: 0.0 } + to { opacity: 1.0 } +} +@-moz-keyframes fadein { + from { opacity: 0.0 } + to { opacity: 1.0 } +} +@-webkit-keyframes fadein { + from { opacity: 0.0 } + to { opacity: 1.0 } +} + +.video-list-item a strong { + display: block; + width: 100%; + padding: 3px 5px; + position: absolute; + bottom: 0px; + background: rgba(0,0,0,0.5); + color: #FFF; +} + +.video-list-item ul, .video-list-item div { + display: none; +} + +.non-video-list { + border-top: 1px solid #DDD; + margin-top: 15px; +} + +.video-player { + padding-bottom: 10px; +} + +.video-player .vjs-no-video { + background: #888; + padding: 3px 5px; +} + +.video-box { + display: inline-block; + position: relative; +} + +.video-channel .video-box { + float: left; + width: 400px; +} +.video-channel .zoom-in .video-box { + float: none; + width: 100%; +} + +.video-box .video-ctrl { + position: absolute; + top: 5px; + right: 5px; + display: none; +} +.video-box:hover .video-ctrl { + display: block; +} + +.video-player video, .video-box { + min-width: 256px; + max-width: 100%; + background: #000; +} + +.video-channel .video-player.zoom-out video, +.video-player.zoom-in .video-box, +.video-player.zoom-in video { + width: 100%; +} + +.video-player-info { + text-align: left; + margin-top: 10px; +} + +.video-channel .video-player-info { + margin-left: 415px; +} +.video-channel .zoom-in .video-player-info { + margin: 0px; +} + +.video-player-info > div { + padding: 3px 0px; +} + +.video-player-info .data { + display: inline-block; +} + +.video-player-info .quality ul { + display: inline-block; + margin: 0px -4px; + padding: 0px; +} +.video-player-info .quality li { + margin: 4px; + padding: 0px; +} + +.unconverted-videos p span { + cursor: pointer; +} + +.conversion-error { + text-align: left; + border: 1px solid #A44; + background: #EAA; +} + +#content .conversion-error h2 { + margin: 10px; + color: #FFF; + text-shadow: 1px 1px 1px #944; + text-align: center; +} + +.conversion-error ul { + margin: 10px; + padding: 0px; +} + +.conversion-error li { + list-style: none; + margin: 0px; + padding: 0px; +} + +#article .conversion-error pre { + border: none; + background: rgba(222,222,222,0.3); +} + +.icon-videochannel { background-image: url(/designs/icons/tango/Tango/16x16/mimetypes/video-x-generic.png) } +.icon-newvideochannel { background-image: url(/designs/icons/tango/Tango/16x16/mimetypes/video-x-generic.png) } diff --git a/plugins/html5_video/public/video-channel.js b/plugins/html5_video/public/video-channel.js new file mode 100644 index 0000000..6017aa8 --- /dev/null +++ b/plugins/html5_video/public/video-channel.js @@ -0,0 +1,215 @@ +/* +** Noosfero's Video Channel specific client script +** Released under the same Noosfero's license +*/ + +(function (exports, $) { +"use strict"; + +var vEl = document.createElement('video'); +var canPlay = { + webm: !!vEl.canPlayType('video/webm').replace(/no/,''), + ogg: !!vEl.canPlayType('video/ogg').replace(/no/,''), + mp4: !!vEl.canPlayType('video/mp4').replace(/no/,'') +}; + +exports.VideoChannel = function VideoChannel(baseEl) { + this.baseEl = baseEl; + if ($('.video-player', this.baseEl)[0]){ + this.player = new NoosferoVideoPlayer(this.baseEl, this); + this.init(); + } +}; + +VideoChannel.prototype.init = function() { + var me = this; + $('.video-list-item', this.baseEl).each( + function(num, item) { + me.initItem(item); + } + ); + if ( $('.video-list li', this.baseEl)[0] ) { + this.updatePlayer( $('li', this.baseEl).first() ); + } else { + log.info('there is no playable video yet.'); + $('.video-player', this.baseEl).hide(); + } +}; + +VideoChannel.prototype.initItem = function(item) { + var me = this; + $(item).click(function(){ me.updatePlayer(item, true); }); + var link = $('a', item)[0]; + link.onclick = function(){ return false }; + link.nextFrame = VideoChannel.nextFrame; + if ( !link.frameFade ) + link.frameFade = $('
').prependTo(link)[0]; + link.frameFade.style.backgroundImage = link.style.backgroundImage; + link.addEventListener("animationend", function(){ link.nextFrame() }, false); + link.addEventListener("webkitAnimationEnd", function(){ link.nextFrame() }, false); + link.nextFrame(); +}; + +VideoChannel.nextFrame = function(fade) { + if ( !fade ) { + this.frameFade.style.opacity = 0.0; + this.frameFade.style.animationName = ""; + this.frameFade.style.MozAnimationName = ""; + this.frameFade.style.webkitAnimationName = ""; + if ( !this.bgYPos ) this.bgYPos = 0; + this.style.backgroundPosition = "50% "+ ( this.bgYPos++ * -120 ) +"px"; + if ( this.bgYPos > 5 ) this.bgYPos = 0; + this.frameFade.style.backgroundPosition = "50% "+ ( this.bgYPos * -120 ) +"px"; + var link = this; + setTimeout( function(){ link.nextFrame(true) }, 10 ); + } else { + this.frameFade.style.animationDuration = "1s"; + this.frameFade.style.animationName = "fadein"; + this.frameFade.style.MozAnimationDuration = "1s"; + this.frameFade.style.MozAnimationName = "fadein"; + this.frameFade.style.webkitAnimationDuration = "1s"; + this.frameFade.style.webkitAnimationName = "'fadein'"; + } +}; + +VideoChannel.prototype.updatePlayer = function(item, autoplay) { + var json = $('a', item)[0].getAttribute("data-webversions"); + this.player.videoList = JSON.parse(json); + this.player.selectWebVersion(); + this.player.update( this.getItemData(item), autoplay ); +}; + +VideoChannel.prototype.getItemData = function(item) { + var link = $('a', item)[0]; + var spans = $('span', item); + var data = {}; + data.pageURL = link.href; + data.videoURL = link.getAttribute('data-download'); + data.posterURL = link.getAttribute('data-poster'); + data.title = $(link).text(); + data.abstract = $('.abstract', item)[0].innerHTML; + data.tags = $('.vli-data-tags > div', item)[0].innerHTML; + return data; +}; + +///////////// Video Player ///////////////////////////////////////////////////// + +exports.NoosferoVideoPlayer = function NoosferoVideoPlayer(place, channel) { + this.channel = channel; + this.divBase = $('.video-player', place)[0]; + if(!this.divBase) return; + this.info = { + title : $('h2', this.divBase)[0], + quality : $('.quality ul', this.divBase)[0], + tags : $('.tags div', this.divBase)[0], + abstract : $('.abstract div', this.divBase)[0], + videoCtrl : $('.video-ctrl', this.divBase)[0], + downloadBt : $('.download-bt', this.divBase) + .button({ icons: { primary: "ui-icon-circle-arrow-s" } })[0] + }; + this.videoBox = $('.video-box', this.divBase)[0]; + var me = this; + this.zoomBt = $('') + .button({ icons: { primary: "ui-icon-zoomin" }, text: false }) + .click(function(){ me.toggleZoom() }) + .appendTo(this.info.videoCtrl); + this.zoomBt[0].title = "Zoom in"; + this.videoEl = $('video', this.divBase)[0]; +}; + +NoosferoVideoPlayer.prototype.update = function(data, autoplay) { + this.info.title.innerHTML = data.title; + this.videoEl.src = data.videoURL; + this.videoEl.autoplay = autoplay; + this.poster = data.posterURL; + this.videoEl.load(); + var tags = data.tags || 'None' + $(this.info.tags).empty().append(tags); + var desc = data.abstract || 'None' + $(this.info.abstract).empty().append(desc); + this.info.downloadBt.href = data.videoURL; +}; + +NoosferoVideoPlayer.prototype.updateQualityOpts = function(type) { + var me = this; + $(this.info.quality).empty(); + for ( var size in this.videoList[type] ) { + var videoData = this.videoList[type][size]; + log.info( 'Quality option:', videoData ); + if ( videoData.status == "done" ) { + var txt = videoData.size_name; + if ( !this.channel ) { + txt = videoData.size.w +'x'+ videoData.size.h + + ' '+ videoData.vbrate +' KB/s'; + } + var bt = $( + '
  • '+ txt +'
  • ') + .button({ icons: { primary:"ui-icon-video" } }) + .click(function(){ me.load(this.video, true) }) + .appendTo(this.info.quality)[0]; + bt.video = videoData; + videoData.qualityBt = bt; + } + } +}; + +NoosferoVideoPlayer.prototype.load = function (video, userSelection) { + if ( this.currentVideo ) $(this.currentVideo.qualityBt).button().button("enable"); + $(video.qualityBt).button("disable"); + this.currentVideo = video; + this.videoEl.src = video.path; + this.videoEl.preload = "metadata"; + if ( userSelection ) + $.cookie( "video_quality", video.size_name, {path:'/'} ); + if ( $.cookie("video_zoom") == "true" ) this.zoomIn(); + else this.zoomOut(); +}; + +NoosferoVideoPlayer.prototype.toggleZoom = function () { + if ( $(this.divBase).hasClass("zoom-in") ) this.zoomOut(); + else this.zoomIn(); +}; + +NoosferoVideoPlayer.prototype.zoomIn = function () { + $.cookie( "video_zoom", "true", {path:'/'} ); + $(this.divBase).removeClass("zoom-out").addClass("zoom-in"); +}; + +NoosferoVideoPlayer.prototype.zoomOut = function () { + $.cookie( "video_zoom", "false", {path:'/'} ); + $(this.divBase).removeClass("zoom-in").addClass("zoom-out"); +}; + +NoosferoVideoPlayer.prototype.selectWebVersion = function () { + var video = null; + var me = this; + var q1 = $.cookie("video_quality") || "tiny"; + var q2 = ( q1 == "tiny" ) ? "nice" : "tiny"; + var type = canPlay.webm ? "WEBM" : canPlay.ogg ? "OGV" : "MP4"; + if ( (video = this.getVideoFromList(type, q1)) + || (video = this.getVideoFromList(type, q2)) + ) { + this.updateQualityOpts(video.type); + setTimeout( function(){ me.load(video) }, 10 ); + } +}; + +NoosferoVideoPlayer.prototype.getVideoFromList = function (type, quality) { + log.info( 'Trying to getVideoFromList', type, quality ); + if (!this.videoList && !this.videoList) { + log.info( 'The video list is empty' ); + return null; + } + if ( quality.toLowerCase() != "nice" ) quality = "tiny"; + var selected = this.videoList[type][quality]; + log.info( 'getVideoFromList find:', selected ); + if ( selected && selected.status == "done" ) { + log.info( 'getVideoFromList success' ); + return selected; + } else { + log.info( 'getVideoFromList fail' ); + return null; + } +}; + +}(window, jQuery)); diff --git a/plugins/html5_video/test/download_fixture.rb b/plugins/html5_video/test/download_fixture.rb new file mode 100644 index 0000000..e8d8308 --- /dev/null +++ b/plugins/html5_video/test/download_fixture.rb @@ -0,0 +1,34 @@ +require File.dirname(__FILE__) + '/../../../test/test_helper' + +fixture_path = File.dirname(__FILE__) + '/../../../test/fixtures/videos' +Dir.mkdir(fixture_path) unless File.exist?(fixture_path) + +base_url = 'http://noosfero.org/pub/Development/HTML5VideoPlugin' + +videos = ['old-movie.mpg', 'atropelamento.ogv', 'firebus.3gp'] + +def shutdown(fixture_path, videos) + videos.map do |v| + File.unlink(fixture_path+'/'+v) if File.exists?(fixture_path+'/'+v) + end + exit 1 +end + +signals = %w{EXIT HUP INT QUIT TERM} +signals.map{|s| Signal.trap(s) { shutdown fixture_path, videos } } + +unless videos.select{|v| !File.exists? fixture_path+'/'+v }.empty? + # Open3.capture2e is the right way, but needs ruby 1.9 + puts "\nDownloading video fixture..." + puts videos.map{|v| base_url+'/'+v}.join(' ') + output = `cd '#{fixture_path}'; + LANG=C wget -c #{videos.map{|v| base_url+'/'+v}.join(' ')} || echo '\nERROR'` + + if output[-7..-1] == "\nERROR\n" then + puts "wget fail. Try again." + exit 0 + end +end + +signals.map{|s| Signal.trap(s) { } } + diff --git a/plugins/html5_video/test/functional/content_viewer_controler_test.rb b/plugins/html5_video/test/functional/content_viewer_controler_test.rb index 76a6786..6e17fc1 100644 --- a/plugins/html5_video/test/functional/content_viewer_controler_test.rb +++ b/plugins/html5_video/test/functional/content_viewer_controler_test.rb @@ -1,6 +1,12 @@ -require 'test_helper' +require File.dirname(__FILE__) + '/../../../../test/test_helper' require 'content_viewer_controller' +class ContentViewerController + # Re-raise errors caught by the controller. + def rescue_action(e) raise e end + append_view_path File.join(File.dirname(__FILE__) + '/../../views') +end + class ContentViewerControllerTest < ActionController::TestCase all_fixtures @@ -10,11 +16,13 @@ class ContentViewerControllerTest < ActionController::TestCase @profile = create_user('testinguser').person @environment = @profile.environment + @environment.enable_plugin(Html5VideoPlugin) end attr_reader :profile, :environment should 'add html5 video tag to the page of file type video' do file = UploadedFile.create!(:uploaded_data => fixture_file_upload('/files/test.txt', 'video/ogg'), :profile => profile) + process_delayed_job_queue get :view_page, file.url.merge(:view=>:true) assert_select '#article video' end diff --git a/plugins/html5_video/test/unit/create_video_for_web_job_test.rb b/plugins/html5_video/test/unit/create_video_for_web_job_test.rb new file mode 100755 index 0000000..b1ff022 --- /dev/null +++ b/plugins/html5_video/test/unit/create_video_for_web_job_test.rb @@ -0,0 +1,100 @@ +require File.dirname(__FILE__) + '/../../../../test/test_helper' +require File.dirname(__FILE__) + '/../download_fixture' +$LOAD_PATH << File.dirname(__FILE__) + '/../../lib/' +require 'html5_video_plugin.rb' + +class CreateVideoForWebJobTest < ActiveSupport::TestCase + + ffmpeg = Html5VideoPlugin::Ffmpeg.new + + def setup + Environment.default.enable_plugin Html5VideoPlugin + # Create a temporary directory to write testing files + @temp = %x{ mktemp -d }[0..-2] + end + + def teardown + # Remove the temporary directory + %x{ rm -r '#{@temp}' } + end + + def run_CreateVideoForWebJobs_for_video(video) + jobs = video.web_version_jobs + # Run all CreateVideoForWebJob's for this created video: + print '['; STDOUT.flush + jobs.each do |job| + YAML.load(job.handler).perform + STDOUT.write '+'; STDOUT.flush # a progress to the user see something. + end + print ']'; STDOUT.flush + video.reload + end + + should 'create web compatible version to a uploaded MPEG video' do + video = FilePresenter.for UploadedFile.create!( + :uploaded_data => fixture_file_upload('/videos/old-movie.mpg', 'video/mpeg'), + :profile => fast_create(Person) ) + assert_equal({}, video.web_versions, 'video.web_versions starts as empty list') + + run_CreateVideoForWebJobs_for_video(video) + + video.web_versions.each do |format, format_block| + format_block.each { |size, video_conv| assert !video_conv[:original] } + end + assert_equal 'tiny.ogv', video.web_versions[:OGV][:tiny][:file_name] + assert_equal({:w=>320,:h=>212}, video.web_versions[:OGV][:tiny][:size]) + assert_equal 250, video.web_versions[:OGV][:tiny][:vbrate] + assert_equal 64, video.web_versions[:OGV][:tiny][:abrate] + assert_equal 'nice.ogv', video.web_versions[:OGV][:nice][:file_name] + assert_equal({:w=>576,:h=>384}, video.web_versions[:OGV][:nice][:size]) + assert_equal 104857, video.web_versions[:OGV][:nice][:vbrate] + assert_equal 128, video.web_versions[:OGV][:nice][:abrate] + assert_equal 'tiny.webm', video.web_versions[:WEBM][:tiny][:file_name] + assert_equal({:w=>320,:h=>212}, video.web_versions[:WEBM][:tiny][:size]) + assert_equal 250, video.web_versions[:WEBM][:tiny][:vbrate] + assert_equal 64, video.web_versions[:WEBM][:tiny][:abrate] + assert_equal 'nice.webm', video.web_versions[:WEBM][:nice][:file_name] + assert_equal({:w=>576,:h=>384}, video.web_versions[:WEBM][:nice][:size]) + assert_equal 104857, video.web_versions[:WEBM][:nice][:vbrate] + assert_equal 128, video.web_versions[:WEBM][:nice][:abrate] + + webdir = ffmpeg.webdir_for_original_video video.public_filename + assert File.exists?( webdir+'/tiny.ogv' ) + assert File.exists?( webdir+'/nice.ogv' ) + assert File.exists?( webdir+'/tiny.webm' ) + assert File.exists?( webdir+'/nice.webm' ) + end + + should 'create web compatible version to a uploaded OGG video' do + resp = ffmpeg.run [ :i, "#{fixture_path}/videos/firebus.3gp", + :t, 4, :f, 'ogg', + :vcodec, 'libtheora', :vb, '800k', + :acodec, 'libvorbis', :ar, 44100, :ab, '192k', + "#{@temp}/firebus.ogv" ] + assert_equal 0, resp[:error][:code], 'creating a valid OGV' + + video = FilePresenter.for UploadedFile.create!( + uploaded_data: Rack::Test::UploadedFile.new("#{@temp}/firebus.ogv", 'video/ogv'), + profile: fast_create(Person) ) + assert_equal({}, video.web_versions, 'video.web_versions starts as empty list') + + run_CreateVideoForWebJobs_for_video(video) + + video.web_versions.each do |format, format_block| + format_block.each { |size, video_conv| assert !video_conv[:original] } + end + web_versions = video.web_versions! + assert_equal 'tiny.ogv', web_versions[:OGV][:tiny][:file_name] + assert_equal({:w=>320,:h=>240}, web_versions[:OGV][:tiny][:size]) + assert_equal 250, web_versions[:OGV][:tiny][:vbrate] + assert_equal 64, web_versions[:OGV][:tiny][:abrate] + assert_equal 'tiny.webm', web_versions[:WEBM][:tiny][:file_name] + assert_equal({:w=>320,:h=>240}, web_versions[:WEBM][:tiny][:size]) + assert_equal 250, web_versions[:WEBM][:tiny][:vbrate] + assert_equal 64, web_versions[:WEBM][:tiny][:abrate] + assert_equal 'firebus.ogv', web_versions[:OGV][:orig][:file_name] + assert_equal({:w=>128,:h=>96}, web_versions[:OGV][:orig][:size]) + assert_equal 192, web_versions[:OGV][:orig][:abrate] + end + +end diff --git a/plugins/html5_video/test/unit/create_video_preview_job_test.rb b/plugins/html5_video/test/unit/create_video_preview_job_test.rb new file mode 100755 index 0000000..9d5685d --- /dev/null +++ b/plugins/html5_video/test/unit/create_video_preview_job_test.rb @@ -0,0 +1,48 @@ +require File.dirname(__FILE__) + '/../../../../test/test_helper' +require File.dirname(__FILE__) + '/../download_fixture' +$LOAD_PATH << File.dirname(__FILE__) + '/../../lib/' +require 'html5_video_plugin.rb' + +class CreateVideoPreviewJobTest < ActiveSupport::TestCase + + def setup + Environment.default.enable_plugin Html5VideoPlugin + end + + def run_CreateVideoPreviewJob_for_video(video) + jobs = video.web_preview_jobs + # Run all CreateVideoPreviewJob's for this created video: + print '['; STDOUT.flush + jobs.each do |job| + YAML.load(job.handler).perform + STDOUT.write '+'; STDOUT.flush # a progress to the user see something. + end + print ']'; STDOUT.flush + video.reload + end + + should 'create preview images to uploaded videos' do + + video = FilePresenter.for UploadedFile.create!( + :uploaded_data => fixture_file_upload('/videos/firebus.3gp', 'video/3gpp'), + :profile => fast_create(Person) ) + assert not(video.has_previews?) + + run_CreateVideoPreviewJob_for_video video + video.reload + + assert video.has_previews?, 'must have built preview images' + assert_equal({:big => '/web/preview_160x120.jpg', + :thumb => '/web/preview_107x80.jpg'}, + video.previews) + + video_path = File.dirname(video.full_filename) + + assert File.exist?(video_path + video.previews[:big]) + assert File.exist?(video_path + video.previews[:thumb]) + assert_match /^\/[^ ]*\/[0-9]+\/+web\/preview_160x120.jpg JPEG 160x720 /, `identify #{video_path + video.previews[:big]}` + assert_match /^\/[^ ]*\/[0-9]+\/+web\/preview_107x80.jpg JPEG 107x480 /, `identify #{video_path + video.previews[:thumb]}` + + end + +end diff --git a/plugins/html5_video/test/unit/ffmpeg_test.rb b/plugins/html5_video/test/unit/ffmpeg_test.rb new file mode 100644 index 0000000..c3ae52e --- /dev/null +++ b/plugins/html5_video/test/unit/ffmpeg_test.rb @@ -0,0 +1,267 @@ +#require File.dirname(__FILE__) + '/../../../../test/test_helper' +require 'test_helper' +#require File.dirname(__FILE__) + '/../download_fixture' +require_relative '../download_fixture' +$LOAD_PATH << File.dirname(__FILE__) + '/../../lib/' +require 'html5_video_plugin.rb' +require 'html5_video_plugin/ffmpeg.rb' + +class FfmpegTest < ActiveSupport::TestCase + + ffmpeg = Html5VideoPlugin::Ffmpeg.new + + def create_video(file, mime) + file = UploadedFile.create!( + :uploaded_data => fixture_file_upload('/videos/'+file, mime), + :profile => fast_create(Person)) + end + + def video_path(file='') + "#{fixture_path}/videos/#{file}" + end + + # Some tests wil create a "web" dir inside fixture videos dir, so we must remove it. + def rm_web_videos_dir + webdir = video_path 'web' + return unless Dir.exist? webdir + Dir.foreach(webdir) do|file| + File.unlink webdir +'/'+ file unless file.match /^\.+$/ + end + Dir.delete webdir + end + + def setup + Environment.default.enable_plugin Html5VideoPlugin + @temp = [] + rm_web_videos_dir + end + + def teardown + @temp.each do |file| + if File.exist? file + File.unlink file + end + end + rm_web_videos_dir + end + + # Create a temp filename, not a file. + # If a file with this name is created, it will be removed by the teardown. + def mkTempName(ext='') + ( @temp << "/tmp/#{SecureRandom.hex}.#{ext}" ).last + end + + should 'has the right version of ffmpeg' do + response = ffmpeg.run :version + assert_match /^ffmpeg version 3\.0/, response[:output] + end + + should 'complain about missing input' do + response = ffmpeg.run :i, 'ups-i-dont-exixt.ogv' + assert_equal 1, response[:error][:code] + end + + should 'complain about missing output' do + response = ffmpeg.run :i, video_path('old-movie.mpg') + assert_equal 2, response[:error][:code] + end + + should 'complain about unknown encoder' do + tmpogv = mkTempName :ogv + response = ffmpeg.run :i, video_path('old-movie.mpg'), :vcodec, 'noCodec', tmpogv + assert_equal 3, response[:error][:code] + end + + should 'complain about wrong encoder' do + tmpvid = mkTempName :mpg + response = ffmpeg.run :i, video_path('firebus.3gp'), :'b:v', 3, tmpvid + assert_equal 4, response[:error][:code] + end + +# #TODO: cant reproduce this error +# should 'complain about not being able to open encoder' do +# tmpvid = mkTempName :mpg +# response = run_ffmpeg [:i, video_path('old-movie.mpg'), tmpvid] +# assert_equal 5, response[:error][:code] +# end + +# #TODO: cant reproduce this error +# should 'complain about unsuported codec' do +# tmpvid = mkTempName :webm +# response = run_ffmpeg [:i, video_path('firebus.3gp'), :vcodec, 'libtheora', tmpvid] +# assert_equal 6, response[:error][:code] +# end + + should 'complain about unknown output format' do + tmpvid = mkTempName :nop + response = ffmpeg.run :i, video_path('old-movie.mpg'), tmpvid + assert_equal 7, response[:error][:code] + end + + should 'complain about invalid input data' do + tmpvid = mkTempName :mpg + fakevid = Tempfile.new ['fake', '.mpg'] + response = ffmpeg.run :i, fakevid.path, tmpvid + fakevid.close + fakevid.unlink + assert_equal 8, response[:error][:code] + end + + should 'read ffmpeg information and features' do + response = ffmpeg.register_information + assert_match /^[0-9]\.[0-9]\.[0-9]$/, response[:version] + formatWebM = /^\{demux:false,description:WebM,mux:true\}$/ + assert_match formatWebM, h2s(response[:formats][:webm]) + codecVorbis = /^\{decode:true,description:Vorbis[^,]+,direct_rendering:false,draw_horiz_band:false,encode:true,type:audio,wf_trunc:false\}$/ + assert_match codecVorbis, h2s(response[:codecs][:vorbis]) + end + + should 'convert time string to seconds int' do + assert_equal 30, ffmpeg.timestr_to_secs('00:00:30') + assert_equal 630, ffmpeg.timestr_to_secs('00:10:30') + assert_equal 7830, ffmpeg.timestr_to_secs('02:10:30') + assert_equal nil, ffmpeg.timestr_to_secs('invalid time string') + end + + should 'parse video stream info' do + response = ffmpeg.get_stream_info 'Stream #0:0[0x1e0]: Video: mpeg1video, yuv420p(tv), 720x480 [SAR 200:219 DAR 100:73], 104857 kb/s, 23.98 fps, 23.98 tbr, 90k tbn, 23.98 tbc' + + assert_equal 'video', response[:type] + assert_equal 'mpeg1video', response[:codec] + assert_equal 104857, response[:bitrate] + assert_equal 23.98, response[:framerate] + assert_equal 'video', response[:type] + assert_equal 720, response[:size][:w] + assert_equal 480, response[:size][:h] + end + + should 'parse audio stream info' do + response = ffmpeg.get_stream_info 'Stream #0:1[0x1c0]: Audio: mp2, 48000 Hz, 2 channels, stereo, s16p, 128 kb/s' + assert_equal 'audio', response[:type] + assert_equal 'mp2', response[:codec] + assert_equal 48000, response[:frequency] + assert_equal 128, response[:bitrate] + assert_equal 2, response[:channels] + end + + should 'fetch webdir' do + video = mkTempName :mpg + assert_equal '/tmp/web', ffmpeg.webdir_for_original_video(video) + end + + should 'validate conversion conf for web' do + conf = { in: video_path('old-movie.mpg') } + validConf = ffmpeg.validate_conversion_conf_for_web conf, :webm + assert_match /^\{abrate:128,file_name:640x426_1024.webm,in:[^:]+\/old-movie.mpg,out:[^:]+\/web\/640x426_1024.webm,size:\{h:426,w:640\},vbrate:1024\}$/, h2s(validConf) + end + + should 'validate conversion conf for web with given output filename' do + conf = { in: video_path('old-movie.mpg'), file_name: 'test.webm' } + validConf = ffmpeg.validate_conversion_conf_for_web conf, :webm + assert_match /^\/.+\/web\/test.webm$/, validConf[:out] + end + + should 'get video info' do + resp = ffmpeg.get_video_info video_path('old-movie.mpg') + assert_equal [:error, :parameters, :output, :metadata, :type, :duration, :global_bitrate, :streams], resp.keys + assert_equal '{code:0,message:Success.}', h2s(resp[:error]) + assert_equal 'mpeg', resp[:type] + assert_equal 5, resp[:duration] + assert_equal 2428, resp[:global_bitrate] + assert_equal '{}', h2s(resp[:metadata]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg\]$/, h2s(resp[:parameters]) + assert_match /^\{bitrate:104857,codec:mpeg1video,framerate:23.98,id:#0,size:\{h:480,w:720\},type:video\}$/, h2s(resp[:streams][0]) + assert_match /^\{bitrate:128,codec:mp2,frequency:48000,id:#0,type:audio\}$/, h2s(resp[:streams][1]) + end + + should 'get video info with metadata' do + resp = ffmpeg.get_video_info video_path('atropelamento.ogv') + assert_equal '{comment:Stop-motion movie,title:Atropelamento}', h2s(resp[:metadata]) + end + + should 'convert to OGV' do + out_video = mkTempName :ogv + resp = ffmpeg.convert2ogv in: video_path('old-movie.mpg'), out: out_video + assert_equal [:error, :parameters, :output, :conf], resp.keys + assert_equal '{code:0,message:}', h2s(resp[:error]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg,y,b:v,600k,f,ogg,acodec,libvorbis,vcodec,libtheora,\/tmp\/[^,\/]*.ogv\]$/, h2s(resp[:parameters]) + assert_match /^\{in:\/[^,]*\/videos\/old-movie.mpg,out:\/tmp\/[^,\/]*.ogv,type:OGV,vbrate:600\}$/, h2s(resp[:conf]) + assert File.exist? out_video + end + + should 'convert to MP4' do + out_video = mkTempName :mp4 + resp = ffmpeg.convert2mp4 in: video_path('old-movie.mpg'), out: out_video + assert_equal [:error, :parameters, :output, :conf], resp.keys + assert_equal '{code:0,message:}', h2s(resp[:error]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg,y,b:v,600k,preset,slow,f,mp4,acodec,aac,vcodec,libx264,strict,-2,\/tmp\/[^,\/]*.mp4\]$/, h2s(resp[:parameters]) + assert_match /^\{in:\/[^,]*\/videos\/old-movie.mpg,out:\/tmp\/[^,\/]*.mp4,type:MP4,vbrate:600\}$/, h2s(resp[:conf]) + assert File.exist? out_video + end + + should 'convert to WebM' do + out_video = mkTempName :webm + resp = ffmpeg.convert2webm in: video_path('old-movie.mpg'), out: out_video + assert_equal [:error, :parameters, :output, :conf], resp.keys + assert_equal '{code:0,message:}', h2s(resp[:error]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg,y,b:v,600k,f,webm,acodec,libvorbis,vcodec,libvpx,\/tmp\/[^,\/]*.webm\]$/, h2s(resp[:parameters]) + assert_match /^\{in:\/[^,]*\/videos\/old-movie.mpg,out:\/tmp\/[^,\/]*.webm,type:WEBM,vbrate:600\}$/, h2s(resp[:conf]) + assert File.exist? out_video + end + + should 'convert to OGV for the web' do + resp = ffmpeg.make_ogv_for_web in: video_path('old-movie.mpg') + assert_equal [:error, :parameters, :output, :conf], resp.keys + assert_equal '{code:0,message:}', h2s(resp[:error]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg,y,b:v,1024k,f,ogg,acodec,libvorbis,vcodec,libtheora,s,640x426,b:a,128k,\/[^,]*\/videos\/web\/640x426_1024.ogv\]$/, h2s(resp[:parameters]) + assert_match /^\/[^,]*\/videos\/web\/640x426_1024.ogv$/, resp[:conf][:out] + assert File.exist? resp[:conf][:out] + end + + should 'convert to MP4 for the web' do + resp = ffmpeg.make_mp4_for_web in: video_path('old-movie.mpg') + assert_equal [:error, :parameters, :output, :conf], resp.keys + assert_equal '{code:0,message:}', h2s(resp[:error]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg,y,b:v,1024k,preset,slow,f,mp4,acodec,aac,vcodec,libx264,strict,-2,s,640x426,b:a,128k,\/[^,]*\/videos\/web\/640x426_1024.mp4\]$/, h2s(resp[:parameters]) + assert_match /^\/[^,]*\/videos\/web\/640x426_1024.mp4$/, resp[:conf][:out] + assert File.exist? resp[:conf][:out] + end + + should 'convert to WebM for the web' do + resp = ffmpeg.make_webm_for_web in: video_path('old-movie.mpg') + assert_equal [:error, :parameters, :output, :conf], resp.keys + assert_equal '{code:0,message:}', h2s(resp[:error]) + assert_match /^\[i,\/[^,]*\/videos\/old-movie.mpg,y,b:v,1024k,f,webm,acodec,libvorbis,vcodec,libvpx,s,640x426,b:a,128k,\/[^,]*\/videos\/web\/640x426_1024.webm\]$/, h2s(resp[:parameters]) + assert_match /^\/[^,]*\/videos\/web\/640x426_1024.webm$/, resp[:conf][:out] + assert File.exist? resp[:conf][:out] + end + + should 'create video thumbnail' do + resp = ffmpeg.video_thumbnail video_path('old-movie.mpg') + assert_match /^\/web\/preview_160x120.jpg$/, resp[:big] + assert_match /^\/web\/preview_107x80.jpg$/, resp[:thumb] + assert File.exist?(video_path resp[:big]) + assert File.exist?(video_path resp[:thumb]) + assert_match /^\/[^ ]*\/videos\/+web\/preview_160x120.jpg JPEG 160x720 /, `identify #{video_path resp[:big]}` + assert_match /^\/[^ ]*\/videos\/+web\/preview_107x80.jpg JPEG 107x480 /, `identify #{video_path resp[:thumb]}` + end + + should 'recognize ffmpeg version' do + assert_match /^[0-9]\.[0-9]\.[0-9]$/, ffmpeg.version + end + + should 'list supported formats' do + formatMpeg = /^\{demux:true,description:MPEG-1 Systems[^}]+,mux:true\}$/ + formatWebM = /^\{demux:false,description:WebM,mux:true\}$/ + assert_match formatMpeg, h2s(ffmpeg.formats[:mpeg]) + assert_match formatWebM, h2s(ffmpeg.formats[:webm]) + end + + should 'list supported codecs' do + codecOpus = /^\{decode:true,description:Opus[^,]+,direct_rendering:false,draw_horiz_band:false,encode:true,type:audio,wf_trunc:false\}$/ + codecVorb = /^\{decode:true,description:Vorbis[^,]+,direct_rendering:false,draw_horiz_band:false,encode:true,type:audio,wf_trunc:false\}$/ + assert_match codecOpus, h2s(ffmpeg.codecs[:opus]) + assert_match codecVorb, h2s(ffmpeg.codecs[:vorbis]) + end + +end diff --git a/plugins/html5_video/test/unit/video_presenter_test.rb b/plugins/html5_video/test/unit/video_presenter_test.rb new file mode 100644 index 0000000..8316b4b --- /dev/null +++ b/plugins/html5_video/test/unit/video_presenter_test.rb @@ -0,0 +1,193 @@ +require File.dirname(__FILE__) + '/../../../../test/test_helper' +require File.dirname(__FILE__) + '/../download_fixture' +$LOAD_PATH << File.dirname(__FILE__) + '/../../lib/' +require 'html5_video_plugin.rb' + +class VideoPresenterTest < ActiveSupport::TestCase + + #include Html5VideoPlugin::Ffmpeg + + def create_video(file, mime) + file = UploadedFile.create!( + :uploaded_data => fixture_file_upload('/videos/'+file, mime), + :profile => fast_create(Person)) + end + + def process_video(file) + process_delayed_job_queue + file.reload + FilePresenter::Video.new file + end + + def create_and_proc_video(file, mime) + process_video(create_video(file, mime)) + end + + def setup + Environment.default.enable_plugin Html5VideoPlugin + end + + should 'accept to encapsulate a video file' do + file = create_video 'old-movie.mpg', 'video/mpeg' + assert_equal 10, FilePresenter::Video.accepts?(file) + end + + should 'retrieve meta-data' do + video = create_and_proc_video('old-movie.mpg', 'video/mpeg') + assert_equal 'Video (MPEG)', video.short_description, 'describe the file type' + assert_equivalent video.meta_data.settings.keys, + [:image_previews, :original_video, :web_versions] + assert_equivalent video.original_video.keys, + [:metadata, :type, :streams, :global_bitrate, :error, :duration, :parameters] + assert_equal h2s([:OGV, :WEBM]), h2s(video.web_versions.keys) + end + + should 'retrieve all web versions' do + video1 = create_and_proc_video('old-movie.mpg', 'video/mpeg') + video2 = create_and_proc_video('atropelamento.ogv', 'video/ogg') + # make video2 as fake valid web video: + audio = video2.original_video[:streams].find{|s| s[:type] == 'audio' } + audio = audio[:codec] = 'vorbis' + assert_equal video1.web_versions, video1.web_versions!, 'all web versions (1)' + assert_equal h2s(video2.web_versions[:OGV].merge(:orig => { + :type => :OGV, + :status => "done", + :vbrate => 334, + :size_name => "orig", + :file_name => "atropelamento.ogv", + :size => {:h=>130, :w=>208}, + :original => true, + :path => video2.public_filename, + :abrate => 0 })), + h2s(video2.web_versions![:OGV]), 'all web versions (2)' + # test get the tiniest web version: + data = video1.tiniest_web_version(:OGV) + assert_equal h2s(data), h2s( + :type=>:OGV, :size_name=>"tiny", :status=>"done", + :fps=>12, :abrate=>64, :vbrate=>250, :size=>{:h=>212, :w=>320}, + :path=>File.join(Rails.root,"/test/tmp/0000/#{'%04d'%video1.id}/web/tiny.ogv"), + :file_name=>"tiny.ogv" ) + end + + should 'know if it has ready_web_versions' do + file = create_video 'old-movie.mpg', 'video/mpeg' + video = FilePresenter::Video.new file + assert_equal h2s(video.ready_web_versions), h2s({}) + video = process_video file + web_versions = video.ready_web_versions + assert_equal 'nice.ogv', web_versions[:OGV][:nice][:file_name] + assert_equal 'tiny.ogv', web_versions[:OGV][:tiny][:file_name] + assert_equal 'nice.webm', web_versions[:WEBM][:nice][:file_name] + assert_equal 'tiny.webm', web_versions[:WEBM][:tiny][:file_name] + end + + should 'know its tiniest_web_version' do + video = create_and_proc_video 'atropelamento.ogv', 'video/ogg' + tiniestOGV = video.tiniest_web_version :OGV + tiniestWEBM = video.tiniest_web_version :WEBM + assert_equal h2s({w:208,h:130}), h2s(tiniestOGV[:size]) + assert_equal :OGV, tiniestOGV[:type] + assert_equal h2s({w:208,h:130}), h2s(tiniestWEBM[:size]) + assert_equal :WEBM, tiniestWEBM[:type] + assert_equal nil, video.tiniest_web_version(:MP4) + end + + should 'know if it has_ogv_version' do + video = create_and_proc_video 'old-movie.mpg', 'video/mpeg' + assert video.has_ogv_version + end + + should 'know if it has_mp4_version' do + video = create_and_proc_video 'old-movie.mpg', 'video/mpeg' + assert not(video.has_mp4_version), 'must NOT to list MP4' + video.web_versions[:MP4] = {SIZE: {file_name:'sized.mp4', status:'done'} } + assert video.has_mp4_version, 'must to list MP4' + end + + should 'know if it has_webm_version' do + video = create_and_proc_video 'old-movie.mpg', 'video/mpeg' + assert video.has_webm_version + end + + should 'list its web_version_jobs' do + videoA = FilePresenter::Video.new create_video 'old-movie.mpg', 'video/mpeg' + videoB = FilePresenter::Video.new create_video 'atropelamento.ogv', 'video/ogg' + jobA = videoA.web_version_jobs.map &:payload_object + jobB = videoB.web_version_jobs.map &:payload_object + # TODO: jobA.length must be 4. + # `Html5VideoPlugin::uploaded_file_after_create_callback` is been called two times + #assert_equal 4, jobA.length + assert_equal Html5VideoPlugin::CreateVideoForWebJob, jobA[0].class + assert_equal :OGV, jobA[0].format + assert_equal :tiny, jobA[0].size + assert_match /.*\/old-movie.mpg$/, jobA[0].full_filename + assert_equal Html5VideoPlugin::CreateVideoForWebJob, jobA[1].class + assert_equal :WEBM, jobA[1].format + assert_equal :tiny, jobA[1].size + assert_match /.*\/old-movie.mpg$/, jobA[1].full_filename + assert_equal Html5VideoPlugin::CreateVideoForWebJob, jobA[2].class + assert_equal :OGV, jobA[2].format + assert_equal :nice, jobA[2].size + assert_match /.*\/old-movie.mpg$/, jobA[1].full_filename + assert_equal Html5VideoPlugin::CreateVideoForWebJob, jobA[3].class + assert_equal :WEBM, jobA[3].format + assert_equal :nice, jobA[3].size + assert_match /.*\/old-movie.mpg$/, jobA[1].full_filename + assert_equal Html5VideoPlugin::CreateVideoForWebJob, jobB[0].class + assert_equal :OGV, jobB[0].format + assert_equal :tiny, jobB[0].size + assert_match /.*\/atropelamento.ogv$/, jobB[0].full_filename + assert_equal Html5VideoPlugin::CreateVideoForWebJob, jobB[1].class + assert_equal :WEBM, jobB[1].format + assert_equal :tiny, jobB[1].size + assert_match /.*\/atropelamento.ogv$/, jobB[1].full_filename + end + + should 'list its web_preview_jobs' do + videoA = FilePresenter::Video.new create_video 'old-movie.mpg', 'video/mpeg' + videoB = FilePresenter::Video.new create_video 'atropelamento.ogv', 'video/ogg' + jobA = videoA.web_preview_jobs.map &:payload_object + jobB = videoB.web_preview_jobs.map &:payload_object + # TODO: jobA.length must be 1. + # `Html5VideoPlugin::uploaded_file_after_create_callback` is been called two times + #assert_equal 1, jobA.length + assert_equal Html5VideoPlugin::CreateVideoPreviewJob, jobA[0].class + assert_match /.*\/old-movie.mpg$/, jobA[0].full_filename + assert_equal Html5VideoPlugin::CreateVideoPreviewJob, jobB[0].class + assert_match /.*\/atropelamento.ogv$/, jobB[0].full_filename + end + + should 'know if it has_previews' do + video = create_and_proc_video 'old-movie.mpg', 'video/mpeg' + assert video.has_previews? + end + + should 'list its image previews' do + video = create_and_proc_video 'old-movie.mpg', 'video/mpeg' + assert_equal h2s(big:'/web/preview_160x120.jpg', thumb:'/web/preview_107x80.jpg'), h2s(video.previews) + end + + should 'set its image previews' do + video = FilePresenter::Video.new create_video 'old-movie.mpg', 'video/mpeg' + assert_equal nil, video.previews + video.previews = {big:'big.jpg', thumb:'thumb.jpg'} + assert_equal h2s(big:'big.jpg', thumb:'thumb.jpg'), h2s(video.previews) + end + + should 'get image_preview for a processed video' do + video = create_and_proc_video 'old-movie.mpg', 'video/mpeg' + assert_match /\/[0-9]+\/web\/preview_160x120\.jpg/, video.image_preview(:big) + end + + should 'get default image_preview for non processed video' do + video = FilePresenter::Video.new create_video 'old-movie.mpg', 'video/mpeg' + assert_match /\/html5_video\/images\/video-preview-big\.png/, video.image_preview(:big) + end + + should 'list its conversion_errors' do + video = FilePresenter::Video.new create_video 'old-movie.mpg', 'video/mpeg' + video.web_versions[:MP4] = {nice: {status:'error converting', error:{code:-99,message:'some error',output:'abcde'}} } + assert_equal h2s(MP4:{nice:{code:-99,message:'some error',output:'abcde'}}), h2s(video.conversion_errors) + end + +end diff --git a/plugins/html5_video/views/content_viewer/_video_player.html.erb b/plugins/html5_video/views/content_viewer/_video_player.html.erb new file mode 100644 index 0000000..08a9e17 --- /dev/null +++ b/plugins/html5_video/views/content_viewer/_video_player.html.erb @@ -0,0 +1,29 @@ +
    + <% + if video + video_ogv = video.tiniest_web_version :OGV + video_mp4 = video.tiniest_web_version :MP4 + video_webm = video.tiniest_web_version :WEBM + video_ogv_path = video_ogv ? video_ogv[:path] : '' + video_mp4_path = video_mp4 ? video_mp4[:path] : '' + video_webm_path = video_webm ? video_webm[:path] : '' + embed_video_path = video_webm ? video_webm_path : \ + video_ogv ? video_ogv_path : video_mp4_path + end + %> + + +
    diff --git a/plugins/html5_video/views/content_viewer/video_channel.html.erb b/plugins/html5_video/views/content_viewer/video_channel.html.erb new file mode 100644 index 0000000..2a66b6a --- /dev/null +++ b/plugins/html5_video/views/content_viewer/video_channel.html.erb @@ -0,0 +1,93 @@ +
    + <%= @page.body %> +
    + +
    + <% if ! @page.children.all.find {|f| f.content_type =~ /^video\//} %> +

    <%= _('This channel contains no videos yet') %>

    + <% else %> +
    +

    + <%= render :partial => 'video_player', :locals => {:video => nil} %> +
    +
    + <%=_('Quality options')%>: +
      +
      +
      + <%=_('Tags')%>: +
      +
      +
      + <%=_('Description')%>: +
      +
      +
      +
      +
      +
        + <% unconverted_videos = [] + @page.children. + map{|f| FilePresenter.for f }. + select{|f| f.class == FilePresenter::Video}. + sort_by{|f| - f.created_at.to_i}.each do |f| + unless f.has_web_version + unconverted_videos << f + else %> +
      • + <%= link_to( + content_tag('strong', + f.title. + gsub(/([a-z0-9])_+([a-z0-9])/i, '\1 \2'). + gsub(/\.[a-z0-9]{2,4}$/i, '') + ), + f.view_url, + { + 'data-poster' => f.image_preview(:big), + 'data-download' => f.public_filename, + 'data-webversions' => CGI::escapeHTML(f.web_versions!.to_json), + :style => "background-image:url(#{f.image_preview(:big)})" + }) %> +
          +
        • + tags: +
          <%= linked_article_tags f %>
          +
        • +
        +
        <%= f.abstract %>
        +
      • + <% end end %> +
      + <% end %> + + <% if unconverted_videos && !unconverted_videos.empty? %> +
      +

      + <%= n_( + 'This channel has one video waiting to be converted', + 'This channel has %d videos waiting to be converted', + unconverted_videos.length + ) % unconverted_videos.length + %> +

      +
        + <% unconverted_videos.each do |f| %> +
      • <%= link_to f.title, f.view_url %>
      • + <% end %> +
      +
      + <% end %> + + <% if @page.children.all.find {|f| f.content_type !~ /^video\//} %> +
      +

      <%=_('Non video files')%>

      + <%= list_articles( @page.children.all.select {|f| f.content_type !~ /^video\// } ) %> +
      + <% end %> + +
      + + + diff --git a/plugins/html5_video/views/file_presenter/_video.html.erb b/plugins/html5_video/views/file_presenter/_video.html.erb index 04a40b7..9dd82e9 100644 --- a/plugins/html5_video/views/file_presenter/_video.html.erb +++ b/plugins/html5_video/views/file_presenter/_video.html.erb @@ -1,8 +1,58 @@ - +<% pub_path = __FILE__.sub /.*(\/plugins\/[^\/]+\/).*/, '\1' %> +
      + <% if video.has_web_version %> + <%= render :partial => 'video_player', :locals => {:video => video} %> + <% elsif not video.web_version_jobs.empty? %> +
      + <%=_('Queued to generate the web version. Come back soon.')%> +
      + <% else %> +
      + <%=_('This video is not queued to the video conversor. Contact the site admin.')%> +
      + <% end %> +
      +
      <%=_('Quality options')%>:
        +
        + <%=_('Description')%>: +
        <%= video.abstract %>
        +
        +
        +
        +
        -
        - <%= video.abstract %> -
        +<% if video.has_web_version %> + +<% end %> +<% if video.allow_edit?(user) && !video.conversion_errors.blank? %> +
        +

        <%=_('Video conversion errors')%>

        +
          + <% video.conversion_errors.each do |type, type_block| %> + <% type_block.each do |size, size_block| %> + <% + message, code, output = size_block[:message], size_block[:code], size_block[:output] + # hide version header to non admins + message.sub!(/^([^\n]*\n){2}/,'') unless user.is_admin? + %> +
        • +

          <%= _('Error while converting %{orig_type} to %{new_type}, %{size} size.') % { + :orig_type=>video.content_type.split('/')[1], :new_type=>type, :size=>size + } %>

          + <%= _('Code %s') % content_tag('strong',code) +' — '+ message.to_s %> + + <%= link_to _('display full output'), '#', + :class => 'show-output', + :onclick => 'jQuery(".output",this.parentNode).show(); jQuery(this).hide(); return false' + %> +
        • + <% end %> + <% end %> +
        +
        +<% end %> diff --git a/test/test_helper.rb b/test/test_helper.rb index 697e006..63937cf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -137,6 +137,18 @@ class ActiveSupport::TestCase assert reference.blank?, "The following elements are not in the collection: #{reference.inspect}" end + def h2s(value) # make a string from ordered hash to simplify tests + case value + when Hash, HashWithIndifferentAccess + '{'+ value.stringify_keys.to_a.sort{|a,b|a[0]<=>b[0]}.map{ |k,v| k+':'+h2s(v) }.join(',') +'}' + when Array + '['+ value.map{|i|h2s(i)}.join(',') +']' + when NilClass + '' + else value.to_s + end + end + # For models that render views (blocks, articles, ...) def self.action_view @action_view ||= begin diff --git a/test/unit/plugin_hot_spot_test.rb b/test/unit/plugin_hot_spot_test.rb index ab1b0de..2d0e057 100644 --- a/test/unit/plugin_hot_spot_test.rb +++ b/test/unit/plugin_hot_spot_test.rb @@ -17,9 +17,13 @@ class PluginHotSpotTest < ActiveSupport::TestCase Noosfero::Plugin::HotSpot::CALLBACK_HOTSPOTS.each do |callback| should "call #{callback} hotspot" do - class CoolPlugin < Noosfero::Plugin; end + class CoolPlugin < Noosfero::Plugin + include Noosfero::Plugin::HotSpot + end - Noosfero::Plugin.stubs(:all).returns([CoolPlugin.name]) + CoolPlugin.any_instance.stubs("comment_#{callback}_callback".to_sym).returns(";)") + + Noosfero::Plugin.stubs(:all).returns(['PluginHotSpotTest::CoolPlugin']) Environment.default.enable_plugin(CoolPlugin) CoolPlugin.any_instance.expects("comment_#{callback}_callback".to_sym) -- libgit2 0.21.2