Convert mp3 files to a streaming-ready format
with Ruby SDK for AWS MediaConvert
In my yesterday's post, I described why I decided to take advantage of the HLS streaming in my new project which is a platform for audiobook streaming.
I explained why a file conversion is required. The simplest way, I discovered, to get this done, is using the AWS Elemental MediaConvert service. It was an obvious choice for me as I am a bit familiar with the AWS stack.
Are you even going to show me the code??
Sure! There will be a lot of code this time as kicking off a convert job requires passing extensive configuration hash. My implementation wraps the official MediaConvert SDK for Ruby. You can get it having aws-sdk-mediaconvert
listed in your Gemfile.
class Converting::MediaConverterWrapper
protected
def client
Aws::MediaConvert::Client.new(
region: Rails.configuration.aws[:region],
access_key_id: Rails.application.credentials.aws[:access_key_id],
secret_access_key: Rails.application.credentials.aws[:secret_access_key],
endpoint: Rails.configuration.aws[:endpoint]
)
end
end
class Converting::MediaConverterJobInitializer < Converting::MediaConverterWrapper
def initialize source_s3_key
@source_s3_key = source_s3_key
end
def call
source_S3_bucket = Rails.configuration.aws[:source_S3_bucket]
source_S3 = "s3://#{source_S3_bucket}/#{@source_s3_key}"
destination_S3_bucket = "s3://#{Rails.configuration.aws[:destination_S3_bucket]}/"
queue = Rails.configuration.aws[:queue]
role = Rails.configuration.aws[:role]
# Kicking off a job
@response = client.create_job(create_job_request_body(source_S3, queue, role, destination_S3_bucket))
nil
end
def job_id
@response[:job][:id]
end
private
def create_job_request_body source_S3, queue, role, destination_S3
{
:queue => queue,
:user_metadata => {},
role: role,
:settings => {
:timecode_config => {
:source => 'ZEROBASED'
},
:output_groups => [
{
:name => 'Apple HLS',
:outputs => [
{
:container_settings => {
:container => 'M3U8',
:m3u_8_settings => {
:audio_frames_per_pes => 4,
:pcr_control => 'PCR_EVERY_PES_PACKET',
:pmt_pid => 480,
:private_metadata_pid => 503,
:program_number => 1,
:pat_interval => 0,
:pmt_interval => 0,
:scte_35_source => 'NONE',
:nielsen_id_3 => 'NONE',
:timed_metadata => 'NONE',
:video_pid => 481,
:audio_pids => [482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492],
:audio_duration => 'DEFAULT_CODEC_DURATION' }
},
:audio_descriptions => [
{
:audio_type_control => 'FOLLOW_INPUT',
:audio_source_name => 'Audio Selector 1',
:codec_settings => {
:codec => 'AAC',
:aac_settings => {
:audio_description_broadcaster_mix => 'NORMAL',
:bitrate => 96_000,
:rate_control_mode => 'CBR',
:codec_profile => 'LC',
:coding_mode => 'CODING_MODE_2_0',
:raw_format => 'NONE',
:sample_rate => 48_000,
:specification => 'MPEG4'
}
},
:language_code_control => 'FOLLOW_INPUT'
}
],
:output_settings => {
:hls_settings => {
:audio_group_id => 'program_audio',
:audio_only_container => 'AUTOMATIC',
:i_frame_only_manifest => 'EXCLUDE'
}
},
:name_modifier => '_converted'
}
],
:output_group_settings => {
:type => 'HLS_GROUP_SETTINGS',
:hls_group_settings => {
:manifest_duration_format => 'INTEGER',
:segment_length => 10,
:timed_metadata_id_3_period => 10,
:caption_language_setting => 'OMIT',
:destination => destination_S3,
:timed_metadata_id_3_frame => 'PRIV',
:codec_specification => 'RFC_4281',
:output_selection => 'MANIFESTS_AND_SEGMENTS',
:program_date_time_period => 600,
:min_segment_length => 0,
:min_final_segment_length => 0,
:directory_structure => 'SINGLE_DIRECTORY',
:program_date_time => 'EXCLUDE',
:segment_control => 'SEGMENTED_FILES',
:manifest_compression => 'NONE',
:client_cache => 'ENABLED',
:audio_only_header => 'INCLUDE',
:stream_inf_resolution => 'INCLUDE'
}
}
}
],
:ad_avail_offset => 0,
:inputs => [
{
:audio_selectors => {
'Audio Selector 1' => {
:offset => 0,
:default_selection => 'DEFAULT',
:program_selection => 1
}
},
:filter_enable => 'AUTO',
:psi_control => 'USE_PSI',
:filter_strength => 0,
:deblock_filter => 'DISABLED',
:denoise_filter => 'DISABLED',
:input_scan_type => 'AUTO',
:timecode_source => 'ZEROBASED',
:file_input => source_S3
}
]
},
:acceleration_settings => { :mode => 'DISABLED' },
:status_update_interval => 'SECONDS_60',
:priority => 0
}
end
end
Take note of several options for this advanced configuration:
queue
passing this should be optional as there is such a thing as a "default queue"role
identifier of AWS IAM role with permissions to access both: source and destination, S3 bucketsbitrate
specifies output .aac files bitrate. You can specify multipleAudioDescriptions
with different bitrates to have an adaptive streamsegment_length
duration in seconds of .aac segment filesdestination
identifier of S3 bucket for storing converted filesname_modifier
specifies output file name addonfile_input
S3 identifier of source file
What's next?
Remember to save obtained job_id
, to be able to check the status of initialized convert job later.
Of course, I also have a service class for this purpose.
class Converting::MediaConverterJobInspector < Converting::MediaConverterWrapper
def initialize media_convert_job_id
@media_convert_job_id = media_convert_job_id
end
def call
@response = client.get_job({ id: @media_convert_job_id })
nil
end
# returns job status
def status
@response[:job][:status]
end
# ...
end