UNPKG

@eluvio/elv-abr-profile

Version:

A library for generating Eluvio ABR Profiles

332 lines (293 loc) 9.8 kB
// ElvABRProfile.js // // Utility to generate an ABR Profile object for a given ingest video from a parametric ladder spec and // a list of standard aspect ratios. // // Also provides a default parametric ladder, default list of standard aspect ratios, and an example set of // video properties // -------------------------------------- // external modules // -------------------------------------- const liftA2 = require('crocks/helpers/liftA2') const liftA3 = require('crocks/helpers/liftA3') const {Ok, Err} = require('crocks/Result') const chain = require('crocks/pointfree/chain') const R = require('ramda') // -------------------------------------- // internal modules // -------------------------------------- const AR = require('./lib/aspectRatio') const ARLM = require('./models/aspectRatioListModel') const M = require('./lib/models') const PL = require('./lib/parametricLadder') const PLM = require('./models/parametricLadderModel') const LS = require('./lib/ladderSpecs') const LSM = require('./models/ladderSpecsModel') const V = require('./lib/videoProps') const VPM = require('./models/videoPropsModel') // -------------------------------------- // internal functions // -------------------------------------- const ABR_PROFILE_TEMPLATE = { drm_optional: true, store_clear: false, ladder_specs: { '{"media_type":"audio","channels":1}': { rung_specs: [ { bit_rate: 192000, media_type: 'audio', pregenerate: true } ] }, '{"media_type":"audio","channels":2}': { rung_specs: [ { bit_rate: 256000, media_type: 'audio', pregenerate: true } ] }, '{"media_type":"audio","channels":6}': { rung_specs: [ { bit_rate: 384000, media_type: 'audio', pregenerate: true } ] } }, playout_formats: { 'dash-widevine': { drm: { content_id: '', enc_scheme_name: 'cenc', license_servers: [], type: 'DrmWidevine' }, protocol: { min_buffer_length: 2, type: 'ProtoDash' } }, 'hls-aes128': { drm: { enc_scheme_name: 'aes-128', type: 'DrmAes128' }, protocol: { type: 'ProtoHls' } }, 'hls-fairplay': { drm: { enc_scheme_name: 'cbcs', license_servers: [], type: 'DrmFairplay' }, protocol: { type: 'ProtoHls' } }, 'hls-sample-aes': { drm: { enc_scheme_name: 'cbcs', type: 'DrmSampleAes' }, protocol: { type: 'ProtoHls' } }, 'dash-clear': { drm: null, protocol: { min_buffer_length: 2, type: 'ProtoDash' } }, 'hls-clear': { drm: null, protocol: { type: 'ProtoHls' } } }, segment_specs: { audio: { segs_per_chunk: 15, target_dur: 2 }, video: { segs_per_chunk: 15, target_dur: 2 } } } // Shorthand accessor for modifying ABR Profile ladder_specs const LADDER_SPECS_LENS = R.lensProp('ladder_specs') // Shorthand accessor for modifying ABR Profile playout_formats const PLAYOUT_FORMATS_LENS = R.lensProp('playout_formats') // Shorthand accessor for modifying ABR Profile segment_specs const SEGMENT_SPECS_LENS = R.lensProp('segment_specs') const _abrProfileForVariant = (prodMasterSources, prodMasterVariant, abrProfile, standardAspectRatios) => { // assemble videoProps from prodMaster info // find the (first) video stream let videoStreamInfo = null for (const streamKey in prodMasterVariant.streams) { const firstSource = prodMasterVariant.streams[streamKey].sources[0] const filePath = firstSource.files_api_path const streamIndex = firstSource.stream_index if (prodMasterSources[filePath].streams[streamIndex].type === 'StreamVideo') { videoStreamInfo = prodMasterSources[filePath].streams[streamIndex] break } } if (videoStreamInfo === null) { // return audio-only profile return _profileExcludeVideo(abrProfile) } else { const videoProps = { avgBitrate: videoStreamInfo.bit_rate, duration: videoStreamInfo.duration, frameRate: videoStreamInfo.frame_rate, height: videoStreamInfo.height, sampleAspectRatio: videoStreamInfo.sample_aspect_ratio, width: videoStreamInfo.width } const parametricLadder = abrProfile.video_parametric_ladder || PL.DEFAULT const vidLadderSpecs = _videoLadderSpecs(videoProps, parametricLadder, standardAspectRatios) return vidLadderSpecs.map(_mergeVidLSIntoProfile(abrProfile)) } } // _assertResult :: (a -> Boolean) -> String -> a -> Result [String] | a // Checks that testFn(value) is true. If so, returns Ok(value) else returns Err([errStr]) const _assertResult = R.curry( (testFn, errStr, value) => testFn(value) ? Ok(value) : Err([errStr]) ) const _mergeVidLSIntoProfile = R.curry( (abrProfile, vidLadderSpecs) => R.omit( ['video_parametric_ladder'], R.mergeDeepRight( abrProfile, {ladder_specs: vidLadderSpecs} ) ) ) // _profileRemove :: Lens -> ([k,v] -> Boolean) -> ABRProfile -> ABRProfile // Replaces sub-object within abrProfile with a filtered shallow copy where each [key,value] pair returns false when // evaluated with rejectFn() const _profileRemove = R.curry( (lens, rejectFn, abrProfile) => R.over( lens, R.pipe(R.toPairs, R.reject(rejectFn), R.fromPairs), abrProfile ) ) const _profileExcludeAudio = R.pipe( _profileRemove( LADDER_SPECS_LENS, (kvPair) => kvPair[0].includes('"media_type":"audio"') ), _profileRemove( SEGMENT_SPECS_LENS, (kvPair) => kvPair[0] === 'audio' ), _assertResult(R.pipe(R.view(LADDER_SPECS_LENS), R.keys, R.length, R.lt(0)), 'ABR Profile has no non-audio ladder_specs'), chain(_assertResult(R.pipe(R.view(SEGMENT_SPECS_LENS), R.keys, R.length, R.lt(0)), 'ABR Profile has no non-audio segment_specs')) ) const _profileExcludeClear = R.pipe( _profileRemove( PLAYOUT_FORMATS_LENS, (kvPair) => R.isNil(kvPair[1].drm) ), _assertResult(R.pipe(R.view(PLAYOUT_FORMATS_LENS), R.keys, R.length, R.lt(0)), 'ABR Profile has no non-clear playout_formats') ) const _profileExcludeDRM = R.pipe( _profileRemove( PLAYOUT_FORMATS_LENS, (kvPair) => !R.isNil(kvPair[1].drm) ), _assertResult(R.pipe(R.view(PLAYOUT_FORMATS_LENS), R.keys, R.length, R.lt(0)), 'ABR Profile has no non-DRM playout_formats') ) const _profileExcludeVideo = R.pipe( _profileRemove( LADDER_SPECS_LENS, (kvPair) => kvPair[0].includes('\\"media_type\\":\\"video\\"') ), _profileRemove( SEGMENT_SPECS_LENS, (kvPair) => kvPair[0] === 'video' ), _assertResult(R.pipe(R.view(LADDER_SPECS_LENS), R.keys, R.length, R.lt(0)), 'ABR Profile has no non-video ladder_specs'), chain(_assertResult(R.pipe(R.view(SEGMENT_SPECS_LENS), R.keys, R.length, R.lt(0)), 'ABR Profile has no non-video segment_specs')) ) const _resultToPOJO = result => result.either( errVal => Object({ok: false, errors: R.uniq(R.flatten(errVal.map(R.split('\n'))))}), okVal => Object({ok: true, result: okVal}) ) const _videoLadderSpecs = (videoProps, parametricLadder, standardAspectRatios) => { const checkedVideoProps = M.validator(VPM.VideoPropsModel)(videoProps) const checkedParametricLadder = M.validator(PLM.ParametricLadderModel)(parametricLadder) const checkedStandardAspectRatios = M.validator(ARLM.LandscapeAspectRatioListModel)(standardAspectRatios) return liftA2( LS.fromParametricLadder, checkedVideoProps, liftA3( PL.computeValues, checkedStandardAspectRatios, checkedParametricLadder, checkedVideoProps ) ).chain(M.validator(LSM.LadderSpecsModel)) } // -------------------------------------- // exported functions // -------------------------------------- const ABRProfileForVariant = (prodMasterSources, prodMasterVariant, abrProfile = DEFAULT_PARAMETRIC_ABR_PROFILE, standardAspectRatios = AR.STANDARDS) => _resultToPOJO( _abrProfileForVariant(prodMasterSources, prodMasterVariant, abrProfile, standardAspectRatios) ) const DEFAULT_PARAMETRIC_ABR_PROFILE = R.mergeDeepRight( ABR_PROFILE_TEMPLATE, {video_parametric_ladder: PL.DEFAULT} ) const ProfileExcludeAudio = abrProfile => _resultToPOJO(_profileExcludeAudio(abrProfile)) const ProfileExcludeClear = abrProfile => _resultToPOJO(_profileExcludeClear(abrProfile)) const ProfileExcludeDRM = abrProfile => _resultToPOJO(_profileExcludeDRM(abrProfile)) const ProfileExcludeVideo = abrProfile => _resultToPOJO(_profileExcludeVideo(abrProfile)) // VideoLadderSpecs :: VideoProps => ParametricLadder => AspectRatioList => Object // // Tries to prepare an object with only a 'ladder_specs' property with a single entry (with key specifically // for aspect ratio computed from videoProps), suitable for merging into an ABR Profile. // // If the preparation succeeds, then returns: // { // ok: true, // result: {ladder_specs: OBJECT_WITH_1_VIDEO_LADDER_SPEC} // } // // If the preparation fails, then returns: // { // ok: false, // errors: ARRAY_OF_ERROR_MESSAGES // } // const VideoLadderSpecs = (videoProps = V.EXAMPLE, parametricLadder = PL.DEFAULT, standardAspectRatios = AR.STANDARDS) => _resultToPOJO( _videoLadderSpecs(videoProps, parametricLadder, standardAspectRatios) ) module.exports = { ABRProfileForVariant, DEFAULT_PARAMETRIC_ABR_PROFILE, DEFAULT_PARAMETRIC_LADDER: PL.DEFAULT, DEFAULT_STANDARD_ASPECT_RATIOS: AR.STANDARDS, DEFAULT_VIDEO_PROPERTIES: V.EXAMPLE, ProfileExcludeAudio, ProfileExcludeClear, ProfileExcludeDRM, ProfileExcludeVideo, VideoLadderSpecs }