2014-01-08 21:49:04 +00:00
require 'json'
require 'resque'
2014-01-11 04:57:07 +00:00
require 'resque-retry'
require 'net/http'
require 'digest/md5'
2014-01-08 21:49:04 +00:00
module JamRuby
2014-01-13 22:48:55 +00:00
# executes a mix of tracks, creating a final output mix
2014-01-08 21:49:04 +00:00
class AudioMixer
2014-12-30 23:10:16 +00:00
extend JamRuby :: ResqueStats
2014-01-08 21:49:04 +00:00
2014-01-11 04:57:07 +00:00
@queue = :audiomixer
2014-01-09 02:53:04 +00:00
@@log = Logging . logger [ AudioMixer ]
2014-02-04 20:28:00 +00:00
attr_accessor :mix_id , :manifest , :manifest_file , :output_filename , :error_out_filename ,
:postback_ogg_url , :postback_mp3_url ,
2014-01-11 04:57:07 +00:00
:error_reason , :error_detail
2014-01-08 21:49:04 +00:00
2014-02-04 20:28:00 +00:00
def self . perform ( mix_id , postback_ogg_url , postback_mp3_url )
2014-01-15 22:15:33 +00:00
JamWebEventMachine . run_wait_stop do
audiomixer = AudioMixer . new ( )
2014-02-04 20:28:00 +00:00
audiomixer . postback_ogg_url = postback_ogg_url
audiomixer . postback_mp3_url = postback_mp3_url
2014-01-15 22:15:33 +00:00
audiomixer . mix_id = mix_id
audiomixer . run
end
2014-01-08 21:49:04 +00:00
end
2014-01-19 02:20:44 +00:00
def self . queue_jobs_needing_retry
2014-10-23 04:10:49 +00:00
Mix . find_each ( :conditions = > " completed = FALSE AND (should_retry = TRUE OR (started_at IS NOT NULL AND NOW() - started_at > '1 hour'::INTERVAL)) " , :batch_size = > 100 ) do | mix |
2014-10-27 18:51:29 +00:00
mix . enqueue
2014-01-19 02:20:44 +00:00
end
end
2014-01-09 02:53:04 +00:00
def initialize
2014-01-11 04:57:07 +00:00
#@s3_manager = S3Manager.new(APP_CONFIG.aws_bucket, APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key)
2014-01-09 02:53:04 +00:00
end
def validate
2014-01-11 04:57:07 +00:00
raise " no manifest specified " unless @manifest
2014-01-09 02:53:04 +00:00
raise " no files specified " if ! @manifest [ :files ] || @manifest [ :files ] . length == 0
@manifest [ :files ] . each do | file |
2014-01-09 13:53:47 +00:00
codec = file [ :codec ]
raise " no codec specified " unless codec
offset = file [ :offset ]
raise " no offset specified " unless offset
2014-01-09 02:53:04 +00:00
filename = file [ :filename ]
2014-01-09 13:53:47 +00:00
raise " no filename specified " unless filename
end
raise " no output specified " unless @manifest [ :output ]
raise " no output codec specified " unless @manifest [ :output ] [ :codec ]
2014-01-11 04:57:07 +00:00
raise " no timeline specified " unless @manifest [ :timeline ]
raise " no recording_id specified " unless @manifest [ :recording_id ]
raise " no mix_id specified " unless @manifest [ :mix_id ]
2014-01-13 13:44:28 +00:00
2014-01-11 04:57:07 +00:00
end
2014-01-09 13:53:47 +00:00
2014-01-11 04:57:07 +00:00
def fetch_audio_files
@manifest [ :files ] . each do | file |
filename = file [ :filename ]
if filename . start_with? " http "
# fetch it from wherever, put it somewhere on disk, and replace filename in the file parameter with the local disk one
2014-01-11 05:23:29 +00:00
download_filename = Dir :: Tmpname . make_tmpname ( [ " #{ Dir . tmpdir } /audiomixer-file " , '.ogg' ] , nil )
2014-01-11 04:57:07 +00:00
uri = URI ( filename )
2014-01-11 05:23:29 +00:00
open download_filename , 'wb' do | io |
2014-01-11 04:57:07 +00:00
begin
Net :: HTTP . start ( uri . host , uri . port ) do | http |
request = Net :: HTTP :: Get . new uri
http . request request do | response |
response_code = response . code . to_i
unless response_code > = 200 && response_code < = 299
raise " bad status code: #{ response_code } . body: #{ response . body } "
end
response . read_body do | chunk |
io . write chunk
end
end
end
rescue Exception = > e
@error_reason = " unable to download "
@error_detail = " url #{ filename } , error= #{ e } "
raise e
end
end
2014-01-13 13:44:28 +00:00
@@log . debug ( " downloaded #{ download_filename } " )
2014-01-11 04:57:07 +00:00
filename = download_filename
file [ :filename ] = download_filename
end
2014-01-09 13:53:47 +00:00
2014-01-11 04:57:07 +00:00
raise " no file located at: #{ filename } " unless File . exist? filename
2014-01-09 02:53:04 +00:00
end
end
2014-01-11 04:57:07 +00:00
def prepare
# make sure there is a place to write the .ogg mix
prepare_output
# make sure there is a place to write the error_out json file (if audiomixer fails this is needed)
prepare_error_out
prepare_manifest
2014-01-09 13:53:47 +00:00
end
2014-01-11 04:57:07 +00:00
# write the manifest object to file, to pass into audiomixer
def prepare_manifest
2014-01-09 02:53:04 +00:00
2014-01-13 13:44:28 +00:00
@manifest_file = Dir :: Tmpname . make_tmpname ( [ " #{ Dir . tmpdir } /audiomixer-manifest- #{ @manifest [ :recording_id ] } " , '.json' ] , nil )
2014-01-11 04:57:07 +00:00
File . open ( @manifest_file , " w " ) do | f |
f . write ( @manifest . to_json )
end
2014-01-09 02:53:04 +00:00
2014-01-11 04:57:07 +00:00
@@log . debug ( " manifest: #{ @manifest } " )
end
2014-01-09 02:53:04 +00:00
2014-01-11 04:57:07 +00:00
# make a suitable location to store the output mix, and pass the chosen filepath into the manifest
def prepare_output
2014-02-04 20:28:00 +00:00
@output_ogg_filename = Dir :: Tmpname . make_tmpname ( [ " #{ Dir . tmpdir } /audiomixer-output- #{ @manifest [ :recording_id ] } " , '.ogg' ] , nil )
@output_mp3_filename = Dir :: Tmpname . make_tmpname ( [ " #{ Dir . tmpdir } /audiomixer-output- #{ @manifest [ :recording_id ] } " , '.mp3' ] , nil )
2014-01-09 02:53:04 +00:00
2014-01-11 04:57:07 +00:00
# update manifest so that audiomixer writes here
2014-02-04 20:28:00 +00:00
@manifest [ :output ] [ :filename ] = @output_ogg_filename
2014-10-23 04:10:49 +00:00
# this is not used by audiomixer today; since today the Ruby code handles this
@manifest [ :output ] [ :filename_mp3 ] = @output_mp3_filename
2014-01-11 04:57:07 +00:00
2014-02-04 20:28:00 +00:00
@@log . debug ( " output ogg file: #{ @output_ogg_filename } , output mp3 file: #{ @output_mp3_filename } " )
2014-01-11 04:57:07 +00:00
end
# make a suitable location to store an output error file, which will be populated on failure to help diagnose problems.
def prepare_error_out
2014-01-13 13:44:28 +00:00
@error_out_filename = Dir :: Tmpname . make_tmpname ( [ " #{ Dir . tmpdir } /audiomixer-error-out- #{ @manifest [ :recording_id ] } " , '.ogg' ] , nil )
2014-01-11 04:57:07 +00:00
# update manifest so that audiomixer writes here
@manifest [ :error_out ] = @error_out_filename
@@log . debug ( " error_out: #{ @error_out_filename } " )
end
# read in and parse the error file that audiomixer pops out
def parse_error_out
error_out_data = File . read ( @error_out_filename )
begin
@error_out = JSON . parse ( error_out_data )
rescue
@error_reason = " unable-parse-error-out "
@@log . error ( " unable to parse error_out_data: #{ error_out_data } from error_out: #{ @error_out_filename } " )
end
@error_reason = @error_out [ :reason ]
@error_reason = " unspecified-reason " unless @error_reason
2014-10-13 14:49:52 +00:00
@error_detail = @error_out [ :detail ]
2014-01-11 04:57:07 +00:00
end
def postback
2014-02-04 20:28:00 +00:00
@@log . debug ( " posting ogg mix to #{ @postback_ogg_url } " )
2014-01-11 04:57:07 +00:00
2014-02-04 20:28:00 +00:00
uri = URI . parse ( @postback_ogg_url )
2014-01-11 04:57:07 +00:00
http = Net :: HTTP . new ( uri . host , uri . port )
request = Net :: HTTP :: Put . new ( uri . request_uri )
response = nil
2014-02-04 20:28:00 +00:00
File . open ( @output_ogg_filename , " r " ) do | f |
2014-01-11 04:57:07 +00:00
request . body_stream = f
request [ " Content-Type " ] = " audio/ogg "
2014-02-04 20:28:00 +00:00
request . add_field ( 'Content-Length' , File . size ( @output_ogg_filename ) )
2014-01-11 04:57:07 +00:00
response = http . request ( request )
end
response_code = response . code . to_i
unless response_code > = 200 && response_code < = 299
2014-02-04 20:28:00 +00:00
@error_reason = " postback-ogg-mix-to-s3 "
raise " unable to put to url: #{ @postback_ogg_url } , status: #{ response . code } , body: #{ response . body } "
end
@@log . debug ( " posting mp3 mix to #{ @postback_mp3_url } " )
uri = URI . parse ( @postback_mp3_url )
http = Net :: HTTP . new ( uri . host , uri . port )
request = Net :: HTTP :: Put . new ( uri . request_uri )
response = nil
File . open ( @output_mp3_filename , " r " ) do | f |
request . body_stream = f
2014-02-18 22:58:15 +00:00
request [ " Content-Type " ] = " audio/mpeg "
2014-02-04 20:28:00 +00:00
request . add_field ( 'Content-Length' , File . size ( @output_mp3_filename ) )
response = http . request ( request )
end
response_code = response . code . to_i
unless response_code > = 200 && response_code < = 299
@error_reason = " postback-mp3-mix-to-s3 "
raise " unable to put to url: #{ @postback_mp3_url } , status: #{ response . code } , body: #{ response . body } "
2014-01-11 04:57:07 +00:00
end
end
2014-10-27 22:49:14 +00:00
def cleanup_files ( )
2014-11-02 02:10:12 +00:00
File . delete ( @output_ogg_filename ) if File . exists? ( @output_ogg_filename )
File . delete ( @output_mp3_filename ) if File . exists? ( @output_mp3_filename )
File . delete ( @manifest_file ) if File . exists? ( @manifest_file )
File . delete ( @error_out_filename ) if File . exists? ( @error_out_filename )
2014-10-27 22:52:56 +00:00
@manifest [ :files ] . each do | file |
filename = file [ :filename ]
2014-11-02 02:10:12 +00:00
File . delete ( filename ) if File . exists? ( filename )
2014-10-27 22:52:56 +00:00
end
2014-10-27 22:49:14 +00:00
end
2014-01-11 04:57:07 +00:00
def post_success ( mix )
2014-02-04 20:28:00 +00:00
ogg_length = File . size ( @output_ogg_filename )
ogg_md5 = Digest :: MD5 . new
File . open ( @output_ogg_filename , 'rb' ) . each { | line | ogg_md5 . update ( line ) }
mp3_length = File . size ( @output_mp3_filename )
mp3_md5 = Digest :: MD5 . new
File . open ( @output_mp3_filename , 'rb' ) . each { | line | mp3_md5 . update ( line ) }
2014-01-11 04:57:07 +00:00
2014-02-04 20:28:00 +00:00
mix . finish ( ogg_length , ogg_md5 . to_s , mp3_length , mp3_md5 . to_s )
2014-01-11 04:57:07 +00:00
end
2014-10-27 22:49:14 +00:00
2014-01-11 04:57:07 +00:00
def post_error ( mix , e )
begin
# if error_reason is null, assume this is an unhandled error
unless @error_reason
@error_reason = " unhandled-job-exception "
@error_detail = e . to_s
end
mix . errored ( error_reason , error_detail )
2014-10-23 04:10:49 +00:00
rescue Exception = > e
@@log . error " unable to post back to the database the error #{ e } "
2014-01-08 21:49:04 +00:00
end
2014-01-11 04:57:07 +00:00
end
def run
@@log . info ( " audiomixer job starting. mix_id #{ mix_id } " )
mix = Mix . find ( mix_id )
begin
2014-01-13 13:44:28 +00:00
# bailout check
if mix . completed
@@log . debug ( " mix is already completed. bailing " )
return
end
2014-01-13 22:48:55 +00:00
@manifest = symbolize_keys ( mix . manifest )
2014-01-11 04:57:07 +00:00
@manifest [ :mix_id ] = mix_id # slip in the mix_id so that the job can add it to the ogg comments
# sanity check the manifest
validate
# if http files are specified, bring them local
fetch_audio_files
# write the manifest to file, so that it can be passed to audiomixer as an filepath argument
prepare
2014-01-08 21:49:04 +00:00
2014-01-13 13:44:28 +00:00
execute ( @manifest_file )
2014-02-04 20:28:00 +00:00
postback
2014-10-23 04:10:49 +00:00
2014-02-04 20:28:00 +00:00
post_success ( mix )
2014-10-23 04:10:49 +00:00
2014-10-27 22:49:14 +00:00
# only cleanup files if we manage to get this far
cleanup_files
2014-02-04 20:28:00 +00:00
@@log . info ( " audiomixer job successful. mix_id #{ mix_id } " )
2014-01-11 04:57:07 +00:00
rescue Exception = > e
post_error ( mix , e )
raise
end
end
def manifest = ( value )
@manifest = symbolize_keys ( value )
end
2014-01-08 21:49:04 +00:00
2014-01-11 04:57:07 +00:00
private
2014-01-08 21:49:04 +00:00
2014-01-11 04:57:07 +00:00
def execute ( manifest_file )
2014-01-13 13:44:28 +00:00
unless File . exist? APP_CONFIG . audiomixer_path
@@log . error ( " unable to find audiomixer " )
2014-02-04 20:28:00 +00:00
error_msg = " audiomixer job failed status= #{ $? } error_reason= #{ @error_reason } error_detail= #{ @error_detail } "
2014-01-13 13:44:28 +00:00
@@log . info ( error_msg )
@error_reason = " unable-find-appmixer "
@error_detail = APP_CONFIG . audiomixer_path
raise error_msg
end
2014-01-09 02:53:04 +00:00
audiomixer_cmd = " #{ APP_CONFIG . audiomixer_path } #{ manifest_file } "
@@log . debug ( " executing #{ audiomixer_cmd } " )
2014-01-08 21:49:04 +00:00
2014-01-09 02:53:04 +00:00
system ( audiomixer_cmd )
2014-02-04 20:28:00 +00:00
unless $? == 0
parse_error_out
error_msg = " audiomixer job failed status= #{ $? } error_reason= #{ @error_reason } error_detail= #{ @error_detail } "
@@log . info ( error_msg )
raise error_msg
end
raise " no output ogg file after mix " unless File . exist? @output_ogg_filename
2014-10-29 18:51:33 +00:00
ffmpeg_cmd = " #{ APP_CONFIG . ffmpeg_path } -i \" #{ @output_ogg_filename } \" -ab 192k -metadata JamRecordingId= #{ @manifest [ :recording_id ] } -metadata JamMixId= #{ @mix_id } -metadata JamType=Mix \" #{ @output_mp3_filename } \" "
2014-02-04 20:28:00 +00:00
system ( ffmpeg_cmd )
unless $? == 0
@error_reason = 'ffmpeg-failed'
@error_detail = $? . to_s
error_msg = " ffmpeg failed status= #{ $? } error_reason= #{ @error_reason } error_detail= #{ @error_detail } "
@@log . info ( error_msg )
raise error_msg
end
raise " no output mp3 file after conversion " unless File . exist? @output_mp3_filename
2014-10-29 16:28:13 +00:00
# time to normalize both mp3 and ogg files
2014-11-02 02:21:42 +00:00
normalize_ogg_cmd = " #{ APP_CONFIG . normalize_ogg_path } --bitrate 128 \" #{ @output_ogg_filename } \" "
2014-10-29 16:28:13 +00:00
system ( normalize_ogg_cmd )
unless $? == 0
@error_reason = 'normalize-ogg-failed'
@error_detail = $? . to_s
error_msg = " normalize-ogg failed status= #{ $? } error_reason= #{ @error_reason } error_detail= #{ @error_detail } "
@@log . info ( error_msg )
raise error_msg
end
raise " no output ogg file after normalization " unless File . exist? @output_ogg_filename
2014-11-02 02:21:42 +00:00
normalize_mp3_cmd = " #{ APP_CONFIG . normalize_mp3_path } --bitrate 128 \" #{ @output_mp3_filename } \" "
2014-10-29 16:28:13 +00:00
system ( normalize_mp3_cmd )
unless $? == 0
@error_reason = 'normalize-mp3-failed'
@error_detail = $? . to_s
error_msg = " normalize-mp3 failed status= #{ $? } error_reason= #{ @error_reason } error_detail= #{ @error_detail } "
@@log . info ( error_msg )
raise error_msg
end
raise " no output mp3 file after conversion " unless File . exist? @output_mp3_filename
2014-01-08 21:49:04 +00:00
end
2014-01-09 13:53:47 +00:00
def symbolize_keys ( obj )
case obj
when Array
obj . inject ( [ ] ) { | res , val |
res << case val
when Hash , Array
symbolize_keys ( val )
else
val
end
res
}
when Hash
obj . inject ( { } ) { | res , ( key , val ) |
nkey = case key
when String
key . to_sym
else
key
end
nval = case val
when Hash , Array
symbolize_keys ( val )
else
val
end
res [ nkey ] = nval
res
}
else
obj
end
end
2014-01-08 21:49:04 +00:00
end
end